TRANSFER
Visión General
El webhook TRANSFER se envía a las URLs registradas en el evento cash_out con el formato PIX BACEN, cuando se procesa una transferencia PIX iniciada por su aplicación en POST /api/dict/pix. Informa el resultado: éxito (LIQUIDATED) o falla (ERROR).
Cuándo se envía
- Transferencia PIX liquidada (
LIQUIDATED) - Transferencia PIX rechazada o con falla (
ERROR)
Garantía de estado final
Toda transferencia aceptada (HTTP 201) que sea rechazada de forma definitiva por el proveedor
de pago emite un webhook TRANSFER con status: ERROR y data.errorCode. El rechazo
del proveedor nunca es silencioso.
De ahí se deriva el contrato de lectura de la respuesta síncrona de POST /api/dict/pix:
- 4xx síncrono = rechazo declarado, estado FINAL de la solicitud enviada. O la solicitud fue rechazada antes
de que existiera la transferencia (ej.:
400 PIX_INSUFFICIENT_BALANCE,400 PIX_DUPLICATE_EXTERNAL_ID,422 PIX_KEY_NOT_FOUND), y en ese caso no hay webhook, porque no se creó ninguna transferencia, o el proveedor rechazó la transferencia ya creada, y entonces el webhookERRORtambién llega. En ambos casos puede cerrar la solicitud de inmediato. Corregir e intentar de nuevo con un nuevox-idempotency-keyvale solo para el rechazo anterior a la creación; cuando el rechazo vino del proveedor, espere el webhookERRORantes de cualquier nuevo intento. Excepción:429 TOO_MANY_PIX_KEYS_NOT_FOUNDy422 PIX_KEY_NOT_FOUNDcondetails.cached: trueson rechazos anteriores a la creación de la solicitud. No se creó nada y el envío puede repetirse después delRetry-After. - 5xx síncrono nunca es estado final. Significa "indeterminado": el pago puede haber salido.
No cancele la solicitud ni la trate como falla. Espere el webhook
TRANSFER(LIQUIDATEDoERROR), correlacionando pordata.idempotencyKey. Intentar de nuevo con otra clave de idempotencia genera riesgo de pago duplicado. - El estado final viene solo del webhook, nunca se infiere de un 5xx síncrono.
La garantía anterior cubre el rechazo definitivo del proveedor. Fallas internas de la
plataforma pueden cerrar la transferencia como ERROR sin emitir webhook. Si recibió
un 5xx síncrono y ningún webhook llegó en algunos minutos, consulte la transacción en
GET https://api.fypay.me/api/pix/transaction/{id} (con el x-idempotency-key) o contacte al soporte en
suporte@fypay.me [POR DEFINIR] informando el x-idempotency-key. No intente de nuevo por otro medio sin esa confirmación.
Estructura del 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": "Pago referente a la factura 12345"
},
"type": "TRANSFER"
}Campos Importantes
typestringSiempre "TRANSFER".
data.idnumberID de la transacción en FyPay. Mismo valor de id en la respuesta de POST /api/dict/pix. Puede ser null cuando la transferencia fue rechazada antes de crearse; en ese caso correlacione por el idempotencyKey.
data.txIdstringEl valor del x-idempotency-key enviado en la solicitud.
data.idempotencyKeystringEl valor del x-idempotency-key enviado en la solicitud. Use este campo para correlacionar el webhook con su transferencia.
data.endToEndIdstringEnd to End ID del PIX. Disponible cuando la transferencia llega al proveedor; puede ser null en fallas.
data.statusstringEstado de la transferencia:
LIQUIDATED: transferencia confirmadaERROR: falla en la transferenciaPENDING: aún en procesamiento
data.paymentobjectdata.creditorAccountobjectDatos de quien recibió.
data.debtorAccountobjectSu cuenta. Todos los campos vienen null.
data.creditDebitTypestringSiempre "DEBIT".
data.errorCodestringCódigo de error cuando status es ERROR; null en los demás casos. El conjunto de códigos depende del proveedor de liquidación.
data.remittanceInformationstringDescripción de la transferencia (campo description enviado en la solicitud).
Procesando el Webhook
Ejemplo 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(`Transferencia no 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,
});
}
}Ejemplo Python
def handle_transfer(webhook: dict):
data = webhook['data']
transfer = find_transfer_by_idempotency_key(data['idempotencyKey'])
if not transfer:
print(f"Transferencia no 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'],
)Correlación con la Solicitud
Use idempotencyKey para correlacionar el webhook con la solicitud 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);En el webhook TRANSFER, busque por data.idempotencyKey. Guarde la clave antes de llamar a la API, para poder correlacionar incluso si la respuesta síncrona se pierde.
Manejo de Errores
Cuando status es ERROR, registre el errorCode y trate la transferencia como falla definitiva. Los códigos varían según el proveedor; no dependa de una lista fija. Para intentar de nuevo, use un nuevo x-idempotency-key.
if (data.status === 'ERROR') {
await markTransferFailed(data.idempotencyKey, data.errorCode);
await notifyUser(data.idempotencyKey, data.errorCode);
}Idempotencia
El mismo webhook puede llegar más de una vez. Deduplique por la clave de idempotencia y por el estado:
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);
}