docs
Webhooks V2

Visão Geral

O que são Webhooks V2?

Webhooks V2 são notificações enviadas para a sua aplicação quando uma transação PIX muda de estado. O formato V2 usa um envelope {data, type}, separa pagador (debtorAccount) e recebedor (creditorAccount) e usa os status LIQUIDATED e REFUNDED.

Como receber no formato V2

O formato é escolhido por URL de webhook. No painel (https://banking.fypay.me), em Configurações → Webhooks, ao cadastrar a URL escolha a versão do payload PIX BACEN. URLs cadastradas como Padrão (V1) continuam recebendo o formato V1.

O formato V2 vale para os eventos de transação:

Evento cadastradotype do webhook V2
cash_inRECEIVE
cash_outTRANSFER
refund_inREFUND (você devolveu um PIX recebido)
refund_outREFUND (você recebeu de volta um PIX enviado)

Os eventos de MED (med_created, med_accepted, med_rejected) não mudam com a versão: são sempre entregues no formato descrito em Webhooks de MED.

Estrutura Base

Todos os webhooks V2 seguem esta estrutura:

{
  "data": {
    "...": "dados do evento"
  },
  "type": "RECEIVE"
}

type é RECEIVE, TRANSFER ou REFUND.

Tipos de Evento

TypeDescriçãoEquivalente V1
RECEIVEPIX recebido (Cash-In)CashIn
TRANSFERPIX enviado (Cash-Out)CashOut
REFUNDDevolução (de recebimento ou de envio)CashInReversal / CashOutReversal

Estrutura Completa do Data

interface WebhookV2Data {
  id: number | null;
  txId: string | null;
  endToEndId: string | null;
  providerId: string | null;

  pixKey: string | null;

  status: 'PENDING' | 'LIQUIDATED' | 'REFUNDED' | 'ERROR';

  payment: {
    amount: string;
    currency: 'BRL';
  };
  mismatchAmount?: string | null;

  refunds: RefundInfo[];

  createdAt: string;
  errorCode: string | null;

  webhookType: 'RECEIVE' | 'TRANSFER' | 'REFUND';
  creditDebitType: 'CREDIT' | 'DEBIT';
  transactionType: 'PIX';
  localInstrument: 'DICT';

  debtorAccount: AccountInfo;
  creditorAccount: AccountInfo;

  idempotencyKey: string | null;

  ticketData: Record<string, unknown>;
  remittanceInformation: string | null;
}

interface AccountInfo {
  ispb: string | null;
  name: string | null;
  bankName: string | null;
  issuer: string | null;
  number: string | null;
  document: string | null;
  accountType: string | null;
}

interface RefundInfo {
  status: 'PENDING' | 'LIQUIDATED' | 'ERROR';
  payment: {
    amount: number;
    currency: 'BRL';
  };
  errorCode: string | null;
  eventDate: string;
  endToEndId: string | null;
  information: string | null;
}
CampoDescrição
idID da transação na FyPay. Em REFUND, é o ID da transação original. Em TRANSFER, pode ser null quando a transferência foi recusada antes de ser criada
txIdIdentificador externo da transação: o txid da cobrança em RECEIVE, o x-idempotency-key em TRANSFER e o da transação original em REFUND
endToEndIdEnd to End ID do PIX (em REFUND, o do PIX original)
providerIdID da transação no provedor de liquidação, ou null se ainda não atribuído
payment.amountValor em reais como string decimal, sem número fixo de casas (ex.: "100.5", "100", "0.01")
mismatchAmountSó em RECEIVE e TRANSFER. Diferença entre o valor cobrado e o efetivamente pago, quando houver; senão null
refundsVazio em RECEIVE e TRANSFER. Em REFUND, traz um item com a devolução que gerou o webhook
errorCodeCódigo de erro quando status é ERROR; senão null. O conjunto de códigos depende do provedor
debtorAccount / creditorAccountDados da contraparte. Os dados da sua própria conta vêm sempre com todos os campos null
name / bankNameNome do titular da conta / nome da instituição
documentCPF/CNPJ da contraparte, como informado pelo provedor
ticketDataSempre {}

Diferenças V1 vs V2

AspectoV1V2
FormatoCampos na raizEnvelope {data, type}
Tipo eventoevent: "CashIn"type: "RECEIVE"
AspectoV1V2
Sucesso PIXCONFIRMEDLIQUIDATED
Sucesso devoluçãoCONFIRMEDREFUNDED
ErroERRORERROR
AspectoV1V2
CampocounterpartdebtorAccount / creditorAccount
Bancobank.bankNamebankName
ISPBbank.bankISPBispb
AspectoV1V2
Tiponumberstring (exceto refunds[].payment.amount, que é number)
Formato100.5"100.5"

Mapeamento de Contas

PIX Recebido (RECEIVE)

debtorAccount = Quem pagou (contraparte)
creditorAccount = Sua conta (campos null)
creditDebitType = CREDIT

PIX Enviado (TRANSFER)

debtorAccount = Sua conta (campos null)
creditorAccount = Quem recebeu (contraparte)
creditDebitType = DEBIT

Devolução de Recebimento (REFUND, evento refund_in)

debtorAccount = Sua conta (campos null)
creditorAccount = Quem recebe de volta (contraparte)
creditDebitType = DEBIT

Devolução de Envio (REFUND, evento refund_out)

debtorAccount = Quem está devolvendo (contraparte)
creditorAccount = Sua conta (campos null)
creditDebitType = CREDIT

Entrega

  • URL HTTPS; até 3 URLs por evento
  • Método POST, Content-Type: application/json
  • Timeout de 10 segundos; só respostas 2xx contam como sucesso
  • Até 5 headers customizados por URL, cadastrados no painel
  • IPs de origem: 184.197.16.190, 54.85.39.76, 184.197.130.162, 184.196.180.161, 52.0.159.232, 54.88.75.254

Autenticação

Depois que você gera a chave de assinatura no painel, todo webhook passa a trazer o header X-Avista-Signature com o HMAC-SHA256 (hex) do corpo bruto da requisição. Enquanto a chave não for gerada, os webhooks são entregues sem esse header.

O nome do header é mesmo X-Avista-Signature. Valide a assinatura sobre os bytes recebidos, antes de fazer o parse do JSON. Veja como validar.

A FyPay não envia Basic Auth por conta própria. Se o seu endpoint exige Authorization ou outro segredo, cadastre-o como header customizado.

Exemplo de Handler

import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.use(express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.FYPAY_WEBHOOK_SECRET!;
const processed = new Set<string>();

function isValidSignature(rawBody: Buffer, signature: string | undefined) {
  if (!signature) return false;
  const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function dedupKey(webhook: WebhookV2) {
  const { type, data } = webhook;
  if (type === 'REFUND') {
    const refund = data.refunds[0];
    return `REFUND:${refund?.endToEndId ?? data.providerId}:${refund?.status}`;
  }
  return `${type}:${data.id ?? data.idempotencyKey}:${data.status}`;
}

app.post('/webhooks/pix', (req, res) => {
  if (!isValidSignature(req.body, req.header('x-avista-signature'))) {
    return res.status(401).end();
  }

  const webhook: WebhookV2 = JSON.parse(req.body.toString('utf8'));
  res.status(200).json({ acknowledged: true });

  const key = dedupKey(webhook);
  if (processed.has(key)) return;
  processed.add(key);

  switch (webhook.type) {
    case 'RECEIVE':
      handleReceive(webhook.data);
      break;
    case 'TRANSFER':
      handleTransfer(webhook.data);
      break;
    case 'REFUND':
      handleRefund(webhook.data);
      break;
  }
});

function handleReceive(data: WebhookV2Data) {
  if (data.status === 'LIQUIDATED') {
    console.log(`PIX recebido: R$ ${data.payment.amount}`);
  }
}

function handleTransfer(data: WebhookV2Data) {
  if (data.status === 'LIQUIDATED') {
    console.log(`PIX enviado: ${data.endToEndId}`);
  } else if (data.status === 'ERROR') {
    console.log(`PIX falhou: ${data.errorCode}`);
  }
}

function handleRefund(data: WebhookV2Data) {
  const refund = data.refunds[0];
  if (data.status === 'REFUNDED') {
    console.log(`Devolução: R$ ${refund.payment.amount}`);
  }
}

Em produção, use um armazenamento persistente (banco ou Redis) para a deduplicação. O mesmo webhook pode chegar mais de uma vez por causa das retentativas ou de um reenvio manual.

Retentativas

Se o seu endpoint não responder com HTTP 2xx em 10 segundos, a entrega é repetida:

TentativaIntervaloAcumulado
1ªImediato0
2ª30 segundos30 s
3ª1 minuto1 min 30 s
4ª3 minutos4 min 30 s
5ª5 minutos9 min 30 s
6ª15 minutos24 min 30 s

Após 6 tentativas sem sucesso, o webhook não é mais reenviado automaticamente. Você pode pedir um reenvio com POST https://api.fypay.me/api/resend-webhook/{transactionIdentifier} (limite de 60 requisições por minuto). O reenvio usa a versão de payload cadastrada em cada URL. Veja Reenvio de webhook.

Implemente uma consulta periódica como fallback para garantir que nenhuma transação seja perdida.

Próximos Passos

Nesta página