TRANSFER
概述
当您的应用通过 POST /api/dict/pix 发起的 PIX 转账被处理后,TRANSFER Webhook 会发送到在 cash_out 事件下以 PIX BACEN 格式登记的 URL。它告知结果:成功(LIQUIDATED)或失败(ERROR)。
发送时机
- PIX 转账已结算(
LIQUIDATED) - PIX 转账被拒绝或失败(
ERROR)
最终状态保证
每一笔已被接受(HTTP 201)但被支付提供商最终拒绝的转账,都会发出 status: ERROR 并带有 data.errorCode 的 TRANSFER Webhook。 提供商的拒绝绝不会静默发生。
由此得出 POST /api/dict/pix 同步响应的解读约定:
- 同步 4xx = 明确的拒绝,是所发请求的最终状态。要么请求在转账存在之前就被拒绝(例如
400 PIX_INSUFFICIENT_BALANCE、400 PIX_DUPLICATE_EXTERNAL_ID、422 PIX_KEY_NOT_FOUND),此时不会有 Webhook,因为没有创建任何转账;要么提供商拒绝了已创建的转账,此时ERRORWebhook 也会到达。 两种情况下您都可以立即关闭该请求。使用新的x-idempotency-key修正后重试仅适用于创建前的拒绝;当拒绝来自提供商时,请先等待ERRORWebhook 再进行任何新尝试。 例外:429 TOO_MANY_PIX_KEYS_NOT_FOUND以及带details.cached: true的422 PIX_KEY_NOT_FOUND属于请求创建前的拒绝。没有创建任何内容,可在Retry-After之后重新发送。 - 同步 5xx 绝不是最终状态。 它表示"不确定":付款可能已经发出。不要取消请求,也不要视为失败。请等待
TRANSFERWebhook(LIQUIDATED或ERROR),并通过data.idempotencyKey关联。在这里使用其他幂等键重试会带来重复付款的风险。 - 最终状态只来自 Webhook,绝不从同步 5xx 推断。
上述保证涵盖提供商的最终拒绝。平台内部故障可能会以 ERROR 结束转账而不发出 Webhook。如果您收到同步 5xx 且几分钟内没有 Webhook 到达,请通过 GET https://api.fypay.me/api/pix/transaction/{id}(使用 x-idempotency-key)查询交易,或联系支持 suporte@fypay.me [待定] 并提供 x-idempotency-key。在得到确认之前,不要通过其他方式重试。
负载结构
{
"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"
}重要字段
typestring始终为 "TRANSFER"。
data.idnumberFyPay 中的交易 ID。与 POST /api/dict/pix 响应中的 id 值相同。如果转账在创建前就被拒绝,可能为 null;此时请通过 idempotencyKey 关联。
data.txIdstring请求中发送的 x-idempotency-key 的值。
data.idempotencyKeystring请求中发送的 x-idempotency-key 的值。用此字段将 Webhook 与您的转账关联。
data.endToEndIdstringPIX 的 End to End ID。转账到达提供商后可用;失败时可能为 null。
data.statusstring转账状态:
LIQUIDATED:转账已确认ERROR:转账失败PENDING:仍在处理中
data.paymentobjectdata.creditorAccountobject收款人数据。
data.debtorAccountobject您的账户。所有字段均为 null。
data.creditDebitTypestring始终为 "DEBIT"。
data.errorCodestringstatus 为 ERROR 时的错误码;其他情况为 null。错误码集合取决于结算提供商。
data.remittanceInformationstring转账描述(请求中发送的 description 字段)。
处理 Webhook
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(`未找到转账:${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,
});
}
}Python 示例
def handle_transfer(webhook: dict):
data = webhook['data']
transfer = find_transfer_by_idempotency_key(data['idempotencyKey'])
if not transfer:
print(f"未找到转账:{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'],
)与请求关联
使用 idempotencyKey 将 Webhook 与原始请求关联:
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);在 TRANSFER Webhook 中,按 data.idempotencyKey 查找。请在调用 API 之前保存该键,这样即使同步响应丢失也能完成关联。
错误处理
当 status 为 ERROR 时,记录 errorCode 并将转账视为最终失败。错误码因提供商而异;不要依赖固定列表。如需重试,请使用新的 x-idempotency-key。
if (data.status === 'ERROR') {
await markTransferFailed(data.idempotencyKey, data.errorCode);
await notifyUser(data.idempotencyKey, data.errorCode);
}幂等性
同一个 Webhook 可能会到达多次。请按幂等键和状态去重:
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);
}