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 webhookERRORtambém chega. Em ambos os casos você pode fechar o pedido imediatamente. Corrigir e tentar de novo com um novox-idempotency-keyvale apenas para a rejeição anterior à criação; quando a rejeição veio do provedor, aguarde o webhookERRORantes de qualquer nova tentativa. Exceção:429 TOO_MANY_PIX_KEYS_NOT_FOUNDe422 PIX_KEY_NOT_FOUNDcomdetails.cached: truesão recusas anteriores à criação do pedido. Nada foi criado e o envio pode ser repetido depois doRetry-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(LIQUIDATEDouERROR), correlacionando pelodata.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
typestringSempre "TRANSFER".
data.idnumberID 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.txIdstringO valor do x-idempotency-key enviado na requisição.
data.idempotencyKeystringO valor do x-idempotency-key enviado na requisição. Use este campo para correlacionar o webhook com a sua transferência.
data.endToEndIdstringEnd to End ID do PIX. Disponível quando a transferência chega ao provedor; pode ser null em falhas.
data.statusstringStatus da transferência:
LIQUIDATED: transferência confirmadaERROR: falha na transferênciaPENDING: ainda em processamento
data.paymentobjectdata.creditorAccountobjectDados de quem recebeu.
data.debtorAccountobjectSua conta. Todos os campos vêm null.
data.creditDebitTypestringSempre "DEBIT".
data.errorCodestringCódigo de erro quando status é ERROR; null nos demais casos. O conjunto de códigos depende do provedor de liquidação.
data.remittanceInformationstringDescriçã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);
}