docs
Webhooks V2

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,因为没有创建任何转账;要么提供商拒绝了已创建的转账,此时 ERROR Webhook 也会到达。 两种情况下您都可以立即关闭该请求。使用新的 x-idempotency-key 修正后重试仅适用于创建前的拒绝;当拒绝来自提供商时,请先等待 ERROR Webhook 再进行任何新尝试。 例外:429 TOO_MANY_PIX_KEYS_NOT_FOUND 以及带 details.cached: true 的 422 PIX_KEY_NOT_FOUND 属于请求创建前的拒绝。没有创建任何内容,可在 Retry-After 之后重新发送。
  • 同步 5xx 绝不是最终状态。 它表示"不确定":付款可能已经发出。不要取消请求,也不要视为失败。请等待 TRANSFER Webhook(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.idnumber

FyPay 中的交易 ID。与 POST /api/dict/pix 响应中的 id 值相同。如果转账在创建前就被拒绝,可能为 null;此时请通过 idempotencyKey 关联。

data.txIdstring

请求中发送的 x-idempotency-key 的值。

data.idempotencyKeystring

请求中发送的 x-idempotency-key 的值。用此字段将 Webhook 与您的转账关联。

data.endToEndIdstring

PIX 的 End to End ID。转账到达提供商后可用;失败时可能为 null。

data.statusstring

转账状态:

  • LIQUIDATED:转账已确认
  • ERROR:转账失败
  • PENDING:仍在处理中
data.paymentobject

data.creditorAccountobject

收款人数据。

data.debtorAccountobject

您的账户。所有字段均为 null。

data.creditDebitTypestring

始终为 "DEBIT"。

data.errorCodestring

status 为 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);
}

最佳实践

下一步

本页目录