Avistadocs
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.paymentobject

data.idempotencyKeystring

在原始请求的 x-idempotency-key 请求头中发送的幂等键。

data.creditorAccountobject

收款方(接收方)的数据。

data.creditDebitTypestring

发送转账时始终为 "DEBIT"

data.errorCodestring

status === '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_KEYPIX 密钥无效与用户确认密钥
KEY_NOT_FOUNDDICT 中未找到密钥请求有效的密钥
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);
}

最佳实践

后续步骤

本页目录