docs
Webhooks V2

TRANSFER

Visão Geral

O webhook TRANSFER é enviado para as URLs cadastradas no evento cash_out com o formato PIX BACEN, quando uma transferência PIX iniciada pela sua aplicação em POST /api/dict/pix é processada. Ele informa o resultado: sucesso (LIQUIDATED) ou falha (ERROR).

Quando é enviado

  • Transferência PIX liquidada (LIQUIDATED)
  • Transferência PIX recusada ou com falha (ERROR)

Garantia de estado final

Toda transferência aceita (HTTP 201) que for rejeitada de forma definitiva pelo provedor de pagamento emite um webhook TRANSFER com status: ERROR e data.errorCode. Rejeição do provedor nunca é silenciosa.

Disso decorre o contrato de leitura da resposta síncrona do POST /api/dict/pix:

  • 4xx síncrono = rejeição declarada, estado FINAL do pedido enviado. Ou o pedido foi recusado antes de a transferência existir (ex.: 400 PIX_INSUFFICIENT_BALANCE, 400 PIX_DUPLICATE_EXTERNAL_ID, 422 PIX_KEY_NOT_FOUND), e nesse caso não há webhook, porque nenhuma transferência foi criada, ou o provedor recusou a transferência já criada, e então o webhook ERROR também chega. Em ambos os casos você pode fechar o pedido imediatamente. Corrigir e tentar de novo com um novo x-idempotency-key vale apenas para a rejeição anterior à criação; quando a rejeição veio do provedor, aguarde o webhook ERROR antes de qualquer nova tentativa. Exceção: 429 TOO_MANY_PIX_KEYS_NOT_FOUND e 422 PIX_KEY_NOT_FOUND com details.cached: true são recusas anteriores à criação do pedido. Nada foi criado e o envio pode ser repetido depois do Retry-After.
  • 5xx síncrono nunca é estado final. Significa "indeterminado": o pagamento pode ter saído. Não cancele o pedido nem trate como falha. Aguarde o webhook TRANSFER (LIQUIDATED ou ERROR), correlacionando pelo data.idempotencyKey. Tentar de novo com outra chave de idempotência cria risco de pagamento em dobro.
  • O estado final vem somente do webhook, nunca é inferido de um 5xx síncrono.

A garantia acima cobre a rejeição definitiva do provedor. Falhas internas da plataforma podem encerrar a transferência como ERROR sem emitir webhook. Se você recebeu um 5xx síncrono e nenhum webhook chegou em alguns minutos, consulte a transação em GET https://api.fypay.me/api/pix/transaction/{id} (com o x-idempotency-key) ou acione o suporte em suporte@fypay.me [A DEFINIR] informando o x-idempotency-key. Não tente de novo por outro meio sem essa confirmação.

Estrutura do Payload

{
  "data": {
    "id": 12345,
    "txId": "550e8400-e29b-41d4-a716-446655440000",
    "pixKey": "destino@email.com",
    "status": "LIQUIDATED",
    "payment": {
      "amount": "100.5",
      "currency": "BRL"
    },
    "mismatchAmount": null,
    "refunds": [],
    "createdAt": "2024-01-15T10:30:00.000Z",
    "errorCode": null,
    "endToEndId": "E12345678202401151030abcdefghijk",
    "providerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "ticketData": {},
    "webhookType": "TRANSFER",
    "debtorAccount": {
      "ispb": null,
      "name": null,
      "bankName": null,
      "issuer": null,
      "number": null,
      "document": null,
      "accountType": null
    },
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "creditDebitType": "DEBIT",
    "creditorAccount": {
      "ispb": "12345678",
      "name": "João Silva",
      "bankName": "Banco Exemplo S.A.",
      "issuer": "000",
      "number": "123456",
      "document": "12345678909",
      "accountType": null
    },
    "localInstrument": "DICT",
    "transactionType": "PIX",
    "remittanceInformation": "Pagamento referente a NF 12345"
  },
  "type": "TRANSFER"
}

Campos Importantes

typestring

Sempre "TRANSFER".

data.idnumber

ID da transação na FyPay. Mesmo valor de id na resposta de POST /api/dict/pix. Pode ser null quando a transferência foi recusada antes de ser criada; nesse caso correlacione pelo idempotencyKey.

data.txIdstring

O valor do x-idempotency-key enviado na requisição.

data.idempotencyKeystring

O valor do x-idempotency-key enviado na requisição. Use este campo para correlacionar o webhook com a sua transferência.

data.endToEndIdstring

End to End ID do PIX. Disponível quando a transferência chega ao provedor; pode ser null em falhas.

data.statusstring

Status da transferência:

  • LIQUIDATED: transferência confirmada
  • ERROR: falha na transferência
  • PENDING: ainda em processamento
data.paymentobject

data.creditorAccountobject

Dados de quem recebeu.

data.debtorAccountobject

Sua conta. Todos os campos vêm null.

data.creditDebitTypestring

Sempre "DEBIT".

data.errorCodestring

Código de erro quando status é ERROR; null nos demais casos. O conjunto de códigos depende do provedor de liquidação.

data.remittanceInformationstring

Descrição da transferência (campo description enviado na requisição).

Processando o Webhook

Exemplo Node.js

interface TransferWebhook {
  type: 'TRANSFER';
  data: {
    id: number | null;
    status: 'PENDING' | 'LIQUIDATED' | 'ERROR';
    payment: {
      amount: string;
      currency: 'BRL';
    };
    endToEndId: string | null;
    idempotencyKey: string | null;
    creditorAccount: {
      name: string | null;
      document: string | null;
    };
    errorCode: string | null;
  };
}

async function handleTransfer(webhook: TransferWebhook) {
  const { data } = webhook;

  const transfer = await findTransferByIdempotencyKey(data.idempotencyKey);

  if (!transfer) {
    console.warn(`Transferência não encontrada: ${data.idempotencyKey}`);
    return;
  }

  if (data.status === 'LIQUIDATED') {
    await updateTransfer(transfer.id, {
      status: 'COMPLETED',
      endToEndId: data.endToEndId,
    });
    return;
  }

  if (data.status === 'ERROR') {
    await updateTransfer(transfer.id, {
      status: 'FAILED',
      errorCode: data.errorCode,
    });
  }
}

Exemplo Python

def handle_transfer(webhook: dict):
    data = webhook['data']

    transfer = find_transfer_by_idempotency_key(data['idempotencyKey'])

    if not transfer:
        print(f"Transferência não encontrada: {data['idempotencyKey']}")
        return

    if data['status'] == 'LIQUIDATED':
        update_transfer(
            transfer_id=transfer.id,
            status='COMPLETED',
            e2e_id=data['endToEndId'],
        )
    elif data['status'] == 'ERROR':
        update_transfer(
            transfer_id=transfer.id,
            status='FAILED',
            error_code=data['errorCode'],
        )

Correlação com a Requisição

Use idempotencyKey para correlacionar o webhook com a requisição original:

const idempotencyKey = crypto.randomUUID();

await saveTransfer({ idempotencyKey, status: 'PENDING' });

const transfer = await createTransfer(idempotencyKey, {
  pixKey: 'destino@email.com',
  payment: { amount: 100.50 },
});

await updateTransferId(idempotencyKey, transfer.id);

No webhook TRANSFER, busque por data.idempotencyKey. Salve a chave antes de chamar a API, para conseguir correlacionar mesmo se a resposta síncrona se perder.

Tratamento de Erros

Quando status é ERROR, registre o errorCode e trate a transferência como falha definitiva. Os códigos variam conforme o provedor; não dependa de uma lista fixa. Para tentar de novo, use um novo x-idempotency-key.

if (data.status === 'ERROR') {
  await markTransferFailed(data.idempotencyKey, data.errorCode);
  await notifyUser(data.idempotencyKey, data.errorCode);
}

Idempotência

O mesmo webhook pode chegar mais de uma vez. Deduplique pela chave de idempotência e pelo status:

async function handleWebhook(webhook: TransferWebhook) {
  const key = `transfer:${webhook.data.idempotencyKey}:${webhook.data.status}`;

  const isNew = await redis.set(`webhook:${key}`, '1', 'NX', 'EX', 60 * 60 * 24 * 7);
  if (!isNew) return;

  await handleTransfer(webhook);
}

Boas Práticas

Próximos Passos

Nesta página