Webhooks V2
TRANSFER
概述
TRANSFER Webhook 在您的应用程序发起的 PIX 转账处理完成时发送。此事件表示对 /dict/pix 端点调用的结果(成功或失败)。
发送时机
- PIX 转账处理成功(
LIQUIDATED) - PIX 转账处理失败(
ERROR)
负载结构
{
"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"
}
}重要字段
typestring发送 PIX 时始终为 "TRANSFER"。
data.idnumber交易 ID。与 POST /dict/pix 返回的值相同。
data.endToEndIdstring端到端 ID - PIX 交易在中央银行的唯一标识符。
data.statusstring转账状态:
LIQUIDATED:转账已确认(成功)ERROR:转账失败
data.paymentobjectdata.idempotencyKeystring在原始请求的 x-idempotency-key 请求头中发送的幂等键。
data.creditorAccountobject收款方(接收方)的数据。
data.creditDebitTypestring发送转账时始终为 "DEBIT"。
data.errorCodestringstatus === 'ERROR' 时的错误码。成功时可为 null。
data.remittanceInformationstring转账描述(请求中发送的 description 字段)。
处理 Webhook
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;
// 通过 idempotencyKey 查找转账
const transfer = await findTransferByIdempotencyKey(data.idempotencyKey);
if (!transfer) {
console.warn(`Transfer not found: ${data.idempotencyKey}`);
return;
}
if (data.status === 'LIQUIDATED') {
// 成功 - 确认转账
await updateTransfer(transfer.id, {
status: 'COMPLETED',
endToEndId: data.endToEndId,
completedAt: new Date(),
});
// 通知用户
await notifyTransferSuccess({
transferId: transfer.id,
amount: parseFloat(data.payment.amount),
recipient: data.creditorAccount.name,
});
} else if (data.status === 'ERROR') {
// 失败 - 回滚
await updateTransfer(transfer.id, {
status: 'FAILED',
errorCode: data.errorCode,
});
// 通知用户
await notifyTransferFailed({
transferId: transfer.id,
errorCode: data.errorCode,
});
// 释放冻结余额
await releaseBlockedBalance(transfer.id);
}
}Python 示例
from decimal import Decimal
def handle_transfer(webhook: dict):
data = webhook['data']
# 查找转账
transfer = find_transfer_by_idempotency_key(data['idempotencyKey'])
if not transfer:
print(f"Transfer not found: {data['idempotencyKey']}")
return
if data['status'] == 'LIQUIDATED':
# 成功
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':
# 失败
update_transfer(
transfer_id=transfer.id,
status='FAILED',
error_code=data['errorCode']
)
notify_transfer_failed(
transfer_id=transfer.id,
error_code=data['errorCode']
)
# 释放余额
release_blocked_balance(transfer.id)与请求的关联
使用 idempotencyKey 将 Webhook 与原始请求进行关联:
// 1. 创建转账
const idempotencyKey = crypto.randomUUID();
const transfer = await createTransfer(idempotencyKey, {
pixKey: 'destino@email.com',
amount: 100.50,
});
// 2. 保存关联
await saveTransfer({
id: transfer.id,
idempotencyKey,
status: 'PENDING',
});
// 3. 在 TRANSFER Webhook 中
const savedTransfer = await findByIdempotencyKey(webhook.data.idempotencyKey);
// savedTransfer.id 对应原始转账错误处理
常见错误码:
| 错误码 | 描述 | 建议操作 |
|---|---|---|
INSUFFICIENT_BALANCE | 余额不足 | 转账前检查余额 |
INVALID_KEY | PIX 密钥无效 | 与用户确认密钥 |
KEY_NOT_FOUND | DICT 中未找到密钥 | 请求有效的密钥 |
ACCOUNT_BLOCKED | 账户被冻结 | 联系技术支持 |
TIMEOUT | 处理超时 | 重试 |
if (data.status === 'ERROR') {
switch (data.errorCode) {
case 'INSUFFICIENT_BALANCE':
// 通知余额不足
await notifyInsufficientBalance(transfer);
break;
case 'INVALID_KEY':
case 'KEY_NOT_FOUND':
// 请求用户提供新密钥
await requestNewPixKey(transfer);
break;
case 'TIMEOUT':
// 可使用新的幂等键重试
await retryTransfer(transfer);
break;
default:
// 通用错误
await notifyGenericError(transfer, data.errorCode);
}
}余额变化流程
sequenceDiagram
participant App
participant API
participant Bank
Note over App,Bank: 余额:available=1000,blocked=0
App->>API: POST /dict/pix (R$ 100)
API-->>App: { type: PENDING }
Note over App,Bank: 余额:available=900,blocked=100
alt 成功
Bank->>API: Confirmation
API->>App: Webhook TRANSFER (LIQUIDATED)
Note over App,Bank: 余额:available=900,blocked=0
else 失败
Bank->>API: Error
API->>App: Webhook TRANSFER (ERROR)
Note over App,Bank: 余额:available=1000,blocked=0
end幂等性
使用 data.id 避免重复处理:
async function handleWebhook(webhook: TransferWebhook) {
const webhookId = `transfer:${webhook.data.id}`;
const isProcessed = await redis.sismember('processed', webhookId);
if (isProcessed) {
return; // 已处理
}
await redis.sadd('processed', webhookId);
await handleTransfer(webhook);
}