TRANSFER
Overview
The TRANSFER webhook is sent when a PIX transfer initiated by your application is processed. This event indicates the result (success or failure) of a call to the /dict/pix endpoint.
When it is sent
- PIX transfer processed successfully (
LIQUIDATED) - PIX transfer failed (
ERROR)
Payload Structure
{
"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"
}
}Important Fields
typestringAlways "TRANSFER" for sent PIX.
data.idnumberTransaction ID. Same value returned by POST /dict/pix.
data.endToEndIdstringEnd to End ID - unique identifier of the PIX transaction at the Central Bank.
data.statusstringTransfer status:
LIQUIDATED: Transfer confirmed (success)ERROR: Transfer failed
data.paymentobjectdata.idempotencyKeystringIdempotency key sent in the x-idempotency-key header of the original request.
data.creditorAccountobjectData of who received (the recipient).
data.creditDebitTypestringAlways "DEBIT" for sent transfers.
data.errorCodestringError code when status === 'ERROR'. Can be null on success.
data.remittanceInformationstringTransfer description (description field sent in the request).
Processing the Webhook
Node.js Example
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;
// Find transfer by idempotencyKey
const transfer = await findTransferByIdempotencyKey(data.idempotencyKey);
if (!transfer) {
console.warn(`Transfer not found: ${data.idempotencyKey}`);
return;
}
if (data.status === 'LIQUIDATED') {
// Success - confirm transfer
await updateTransfer(transfer.id, {
status: 'COMPLETED',
endToEndId: data.endToEndId,
completedAt: new Date(),
});
// Notify user
await notifyTransferSuccess({
transferId: transfer.id,
amount: parseFloat(data.payment.amount),
recipient: data.creditorAccount.name,
});
} else if (data.status === 'ERROR') {
// Failure - revert
await updateTransfer(transfer.id, {
status: 'FAILED',
errorCode: data.errorCode,
});
// Notify user
await notifyTransferFailed({
transferId: transfer.id,
errorCode: data.errorCode,
});
// Release blocked balance
await releaseBlockedBalance(transfer.id);
}
}Python Example
from decimal import Decimal
def handle_transfer(webhook: dict):
data = webhook['data']
# Find transfer
transfer = find_transfer_by_idempotency_key(data['idempotencyKey'])
if not transfer:
print(f"Transfer not found: {data['idempotencyKey']}")
return
if data['status'] == 'LIQUIDATED':
# Success
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':
# Failure
update_transfer(
transfer_id=transfer.id,
status='FAILED',
error_code=data['errorCode']
)
notify_transfer_failed(
transfer_id=transfer.id,
error_code=data['errorCode']
)
# Release balance
release_blocked_balance(transfer.id)Correlation with Request
Use idempotencyKey to correlate the webhook with your original request:
// 1. Create transfer
const idempotencyKey = crypto.randomUUID();
const transfer = await createTransfer(idempotencyKey, {
pixKey: 'destino@email.com',
amount: 100.50,
});
// 2. Save association
await saveTransfer({
id: transfer.id,
idempotencyKey,
status: 'PENDING',
});
// 3. In the TRANSFER webhook
const savedTransfer = await findByIdempotencyKey(webhook.data.idempotencyKey);
// savedTransfer.id corresponds to the original transferError Handling
Common error codes:
| Code | Description | Recommended Action |
|---|---|---|
INSUFFICIENT_BALANCE | Insufficient balance | Check balance before transferring |
INVALID_KEY | Invalid PIX key | Verify the key with the user |
KEY_NOT_FOUND | Key not found in DICT | Request a valid key |
ACCOUNT_BLOCKED | Blocked account | Contact support |
TIMEOUT | Processing timeout | Try again |
if (data.status === 'ERROR') {
switch (data.errorCode) {
case 'INSUFFICIENT_BALANCE':
// Notify insufficient balance
await notifyInsufficientBalance(transfer);
break;
case 'INVALID_KEY':
case 'KEY_NOT_FOUND':
// Request new key from user
await requestNewPixKey(transfer);
break;
case 'TIMEOUT':
// Can try again with a new idempotency key
await retryTransfer(transfer);
break;
default:
// Generic error
await notifyGenericError(transfer, data.errorCode);
}
}Balance Flow
sequenceDiagram
participant App
participant API
participant Bank
Note over App,Bank: Balance: available=1000, blocked=0
App->>API: POST /dict/pix (R$ 100)
API-->>App: { type: PENDING }
Note over App,Bank: Balance: available=900, blocked=100
alt Success
Bank->>API: Confirmation
API->>App: Webhook TRANSFER (LIQUIDATED)
Note over App,Bank: Balance: available=900, blocked=0
else Failure
Bank->>API: Error
API->>App: Webhook TRANSFER (ERROR)
Note over App,Bank: Balance: available=1000, blocked=0
endIdempotency
Use data.id to avoid duplicate processing:
async function handleWebhook(webhook: TransferWebhook) {
const webhookId = `transfer:${webhook.data.id}`;
const isProcessed = await redis.sismember('processed', webhookId);
if (isProcessed) {
return; // Already processed
}
await redis.sadd('processed', webhookId);
await handleTransfer(webhook);
}