TRANSFER
Descripción general
El webhook TRANSFER se envia cuando una transferencia PIX iniciada por su aplicación es procesada. Este evento indica el resultado (exito o fallo) de una llamada al endpoint /dict/pix.
Cuando se envia
- Transferencia PIX procesada exitosamente (
LIQUIDATED) - Transferencia PIX fallida (
ERROR)
Estructura del Payload
{
"type": "TRANSFER",
"data": {
"id": 456,
"txId": null,
"pixKey": "destino@email.com",
"status": "LIQUIDATED",
"payment": {
"amount": "100.50",
"currency": "BRL"
},
"refunds": [],
"createdAt": "2024-01-15T10:30:00.000Z",
"errorCode": null,
"endToEndId": "E12345678901234567890123456789012",
"ticketData": {},
"webhookType": "TRANSFER",
"debtorAccount": {
"ispb": null,
"name": null,
"issuer": null,
"number": null,
"document": null,
"accountType": null
},
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"creditDebitType": "DEBIT",
"creditorAccount": {
"ispb": "18236120",
"name": "NU PAGAMENTOS S.A.",
"issuer": "260",
"number": "12345-6",
"document": "123.xxx.xxx-xx",
"accountType": null
},
"localInstrument": "DICT",
"transactionType": "PIX",
"remittanceInformation": "Pagamento NF 12345"
}
}Campos Importantes
typestringSiempre "TRANSFER" para PIX enviados.
data.idnumberID de la transacción. Mismo valor retornado por POST /dict/pix.
data.endToEndIdstringEnd to End ID - identificador único de la transacción PIX en el Banco Central.
data.statusstringEstado de la transferencia:
LIQUIDATED: Transferencia confirmada (exito)ERROR: Transferencia fallida
data.paymentobjectdata.idempotencyKeystringClave de idempotencia enviada en el header x-idempotency-key de la solicitud original.
data.creditorAccountobjectDatos de quien recibio (el destinatario).
data.creditDebitTypestringSiempre "DEBIT" para transferencias enviadas.
data.errorCodestringCódigo de error cuando status === 'ERROR'. Puede ser null en caso de exito.
data.remittanceInformationstringDescripción de la transferencia (campo description enviado en la solicitud).
Procesamiento del Webhook
Ejemplo en Node.js
interface TransferWebhook {
type: 'TRANSFER';
data: {
id: number;
status: 'LIQUIDATED' | 'ERROR';
payment: {
amount: string;
currency: string;
};
endToEndId: string;
idempotencyKey: string;
creditorAccount: {
name: string | null;
document: string | null;
};
errorCode: string | null;
};
}
async function handleTransfer(webhook: TransferWebhook) {
const { data } = webhook;
// Buscar transferencia por idempotencyKey
const transfer = await findTransferByIdempotencyKey(data.idempotencyKey);
if (!transfer) {
console.warn(`Transferencia no encontrada: ${data.idempotencyKey}`);
return;
}
if (data.status === 'LIQUIDATED') {
// Exito - confirmar transferencia
await updateTransfer(transfer.id, {
status: 'COMPLETED',
endToEndId: data.endToEndId,
completedAt: new Date(),
});
// Notificar al usuario
await notifyTransferSuccess({
transferId: transfer.id,
amount: parseFloat(data.payment.amount),
recipient: data.creditorAccount.name,
});
} else if (data.status === 'ERROR') {
// Fallo - revertir
await updateTransfer(transfer.id, {
status: 'FAILED',
errorCode: data.errorCode,
});
// Notificar al usuario
await notifyTransferFailed({
transferId: transfer.id,
errorCode: data.errorCode,
});
// Liberar saldo bloqueado
await releaseBlockedBalance(transfer.id);
}
}Ejemplo en Python
from decimal import Decimal
def handle_transfer(webhook: dict):
data = webhook['data']
# Buscar transferencia
transfer = find_transfer_by_idempotency_key(data['idempotencyKey'])
if not transfer:
print(f"Transferencia no encontrada: {data['idempotencyKey']}")
return
if data['status'] == 'LIQUIDATED':
# Exito
update_transfer(
transfer_id=transfer.id,
status='COMPLETED',
e2e_id=data['endToEndId']
)
notify_transfer_success(
transfer_id=transfer.id,
amount=Decimal(data['payment']['amount']),
recipient=data['creditorAccount'].get('name')
)
elif data['status'] == 'ERROR':
# Fallo
update_transfer(
transfer_id=transfer.id,
status='FAILED',
error_code=data['errorCode']
)
notify_transfer_failed(
transfer_id=transfer.id,
error_code=data['errorCode']
)
# Liberar saldo
release_blocked_balance(transfer.id)Correlacion con la Solicitud
Use idempotencyKey para correlacionar el webhook con su solicitud original:
// 1. Crear transferencia
const idempotencyKey = crypto.randomUUID();
const transfer = await createTransfer(idempotencyKey, {
pixKey: 'destino@email.com',
amount: 100.50,
});
// 2. Guardar asociacion
await saveTransfer({
id: transfer.id,
idempotencyKey,
status: 'PENDING',
});
// 3. En el webhook TRANSFER
const savedTransfer = await findByIdempotencyKey(webhook.data.idempotencyKey);
// savedTransfer.id corresponde a la transferencia originalManejo de Errores
Códigos de error comunes:
| Código | Descripción | Acción Recomendada |
|---|---|---|
INSUFFICIENT_BALANCE | Saldo insuficiente | Verificar saldo antes de transferir |
INVALID_KEY | Clave PIX invalida | Verificar la clave con el usuario |
KEY_NOT_FOUND | Clave no encontrada en DICT | Solicitar una clave valida |
ACCOUNT_BLOCKED | Cuenta bloqueada | Contactar soporte |
TIMEOUT | Timeout de procesamiento | Intentar nuevamente |
if (data.status === 'ERROR') {
switch (data.errorCode) {
case 'INSUFFICIENT_BALANCE':
// Notificar saldo insuficiente
await notifyInsufficientBalance(transfer);
break;
case 'INVALID_KEY':
case 'KEY_NOT_FOUND':
// Solicitar nueva clave al usuario
await requestNewPixKey(transfer);
break;
case 'TIMEOUT':
// Puede intentar de nuevo con una nueva clave de idempotencia
await retryTransfer(transfer);
break;
default:
// Error generico
await notifyGenericError(transfer, data.errorCode);
}
}Flujo de Saldo
sequenceDiagram
participant App
participant API
participant Bank
Note over App,Bank: Saldo: available=1000, blocked=0
App->>API: POST /dict/pix (R$ 100)
API-->>App: { type: PENDING }
Note over App,Bank: Saldo: available=900, blocked=100
alt Exito
Bank->>API: Confirmacion
API->>App: Webhook TRANSFER (LIQUIDATED)
Note over App,Bank: Saldo: available=900, blocked=0
else Fallo
Bank->>API: Error
API->>App: Webhook TRANSFER (ERROR)
Note over App,Bank: Saldo: available=1000, blocked=0
endIdempotencia
Use data.id para evitar el procesamiento duplicado:
async function handleWebhook(webhook: TransferWebhook) {
const webhookId = `transfer:${webhook.data.id}`;
const isProcessed = await redis.sismember('processed', webhookId);
if (isProcessed) {
return; // Ya procesado
}
await redis.sadd('processed', webhookId);
await handleTransfer(webhook);
}