PAYATHON 2026

如何通过 API 判断信用卡交易资金何时到账

支付老李

结论

没有一种 API 能够适用于所有银行和支付网关,并根据信用卡交易直接、准确地判断资金是否已经记入商户银行账户。

信用卡授权成功只代表发卡行同意这笔交易,不代表资金已经到账。要自动分配资金,需要结合以下信息:

  1. 支付网关或收单机构提供的结算批次、余额和 payout 状态;
  2. 商户银行提供的账户交易查询、余额查询或 webhook;
  3. 内部账本,用来关联信用卡交易、结算批次、银行入账和后续 ACH 转账。

如果银行没有提供实时交易 API,只能通过银行流水、Open Banking 接口、BAI2 文件或其他对账文件确认到账。能够使用哪些方式,取决于所在国家、收单机构、银行和商户账户协议。

为什么授权状态不能作为到账依据

一笔信用卡付款通常会经历以下阶段:

authorized
    ↓
captured
    ↓
submitted for settlement
    ↓
settled by acquirer
    ↓
included in merchant payout
    ↓
posted to merchant bank account

每个阶段都可能出现延迟或异常:

  • 交易只有授权记录,但尚未 capture;
  • capture 被撤销或失败;
  • 多笔交易被合并到同一个结算批次;
  • 手续费、退款、拒付或准备金从 payout 中扣除;
  • 周末、节假日和银行处理周期造成延迟;
  • 收单机构显示 payout 已发送,但收款银行还没有记账;
  • 银行流水中的入账金额与单笔信用卡交易金额不一致。

所以,收到 authorized、captured,甚至普通的 settled 状态,都不足以证明相同金额已经进入指定银行账户。

推荐的实现方式

1. 获取支付平台的结算和 payout 数据

先确认支付平台是否提供以下功能:

  • settlement batch report;
  • merchant balance API;
  • payout/disbursement API;
  • payout webhook;
  • payout reconciliation report;
  • payout 与原始 charge、refund、fee 的关联数据。

不同平台使用的字段名称并不相同,常见状态包括:

pending
in_transit
paid
failed
canceled

需要注意,paid 往往只表示支付平台认为转账已经成功发送或完成,未必代表银行已经完成最终记账。具体含义应以该平台对状态的正式定义为准。

2. 查询商户银行账户的实际入账记录

如果业务要求确认银行已经入账后才能发起 ACH,最终判断依据应来自银行侧,例如:

  • 银行账户交易 API;
  • available balance API;
  • bank transaction webhook;
  • Open Banking/account information API;
  • BAI2、CAMT.053、CAMT.054 等对账文件;
  • 银行提供的日终流水文件。

查询时不能只匹配金额。不同 payout 的金额可能相同,手续费也可能导致入账金额与原交易金额不一致。匹配时应综合考虑:

  • payout ID;
  • settlement batch ID;
  • 银行流水 reference;
  • trace number;
  • 金额;
  • 币种;
  • 预计到账日期;
  • 收单机构名称。

如果支付平台无法将 payout reference 传递到银行流水,只能根据金额、日期和批次信息做近似匹配。这种情况下,应保留人工复核流程。

3. 建立内部资金账本

不要把银行余额直接视为每笔交易的可分配金额。内部账本至少应保存以下对象:

CardTransaction
SettlementBatch
Payout
BankTransaction
Allocation
AchTransfer

一个 payout 通常包含多笔信用卡交易,还可能包含费用、退款和调整项。内部账本需要记录它们之间的关系:

CardTransaction ─┐
CardTransaction ─┼─> SettlementBatch ─> Payout ─> BankTransaction
CardTransaction ─┘                                  │
                                                    └─> ACH allocations

只有在 payout 与银行入账完成对账,并确认对应资金没有被其他分配任务占用后,才能将金额标记为可分配。

状态机示例

可以为 payout 设置清晰的状态机:

CREATED
  -> PROCESSOR_CONFIRMED
  -> BANK_POSTED
  -> ALLOCATION_RESERVED
  -> ACH_SUBMITTED
  -> ACH_SETTLED

异常状态可单独处理:

PAYOUT_FAILED
RECONCILIATION_REQUIRED
ACH_RETURNED
ACH_FAILED

其中,BANK_POSTED 应由银行交易记录触发,不能由信用卡授权通知触发。

代码示例

以下代码不依赖具体供应商,仅用于说明实现方式。实际字段、签名算法和状态名称需要按照所选支付平台及银行 API 的文档调整。

type PayoutStatus =
  | "CREATED"
  | "PROCESSOR_CONFIRMED"
  | "BANK_POSTED"
  | "ALLOCATION_RESERVED"
  | "ACH_SUBMITTED"
  | "ACH_SETTLED"
  | "RECONCILIATION_REQUIRED";

interface Payout {
  id: string;
  processorPayoutId: string;
  expectedAmount: number;
  currency: string;
  expectedArrivalDate?: string;
  bankTransactionId?: string;
  status: PayoutStatus;
}

async function handleProcessorWebhook(
  rawBody: Buffer,
  signature: string
): Promise<void> {
  verifyProcessorSignature(rawBody, signature);

  const event = JSON.parse(rawBody.toString("utf8"));

  // webhook 可能重复发送,必须进行幂等处理
  if (await eventAlreadyProcessed(event.id)) {
    return;
  }

  if (event.type === "payout.paid") {
    await updatePayout(event.data.id, {
      status: "PROCESSOR_CONFIRMED",
      expectedAmount: event.data.amount,
      currency: event.data.currency,
      expectedArrivalDate: event.data.arrival_date
    });
  }

  await markEventProcessed(event.id);
}

银行交易同步任务用于确认资金是否实际入账:

async function reconcileBankTransactions(): Promise<void> {
  const transactions = await bankApi.listTransactions({
    accountId: process.env.MERCHANT_BANK_ACCOUNT_ID!,
    since: getLastSynchronizationTime()
  });

  for (const transaction of transactions) {
    if (transaction.status !== "posted" || transaction.direction !== "credit") {
      continue;
    }

    const payout = await findMatchingPayout({
      reference: transaction.reference,
      amount: transaction.amount,
      currency: transaction.currency,
      postedAt: transaction.postedAt
    });

    if (!payout) {
      await createReconciliationException(transaction);
      continue;
    }

    await runInDatabaseTransaction(async () => {
      const lockedPayout = await lockPayoutForUpdate(payout.id);

      if (lockedPayout.status === "BANK_POSTED") {
        return;
      }

      await updatePayout(lockedPayout.id, {
        status: "BANK_POSTED",
        bankTransactionId: transaction.id
      });

      await createAllocationReservations(lockedPayout.id);
    });
  }
}

确认资金到账并完成预留后,再发起 ACH:

async function submitAllocations(payoutId: string): Promise<void> {
  const payout = await getPayout(payoutId);

  if (payout.status !== "ALLOCATION_RESERVED") {
    throw new Error("Payout is not ready for ACH allocation");
  }

  const allocations = await getPendingAllocations(payoutId);

  for (const allocation of allocations) {
    const idempotencyKey = `allocation:${allocation.id}`;

    const transfer = await achProvider.createTransfer(
      {
        sourceAccountId: allocation.sourceAccountId,
        destinationAccountId: allocation.destinationAccountId,
        amount: allocation.amount,
        currency: allocation.currency,
        description: `Allocation for payout ${payoutId}`
      },
      { idempotencyKey }
    );

    await recordAchTransfer(allocation.id, transfer.id, transfer.status);
  }

  await updatePayout(payoutId, {
    status: "ACH_SUBMITTED"
  });
}

资金分配时还要检查可用余额

即使已经匹配到银行入账,也不能只看账面余额就发起转账。至少需要按以下方式计算:

可分配金额
= 已确认入账金额
- 已预留分配金额
- 预计手续费
- 退款及拒付准备金
- 未完成 ACH 金额
- 最低安全余额

每次分配都应先在数据库事务中预留资金,防止多个任务同时使用同一笔钱。向 ACH 服务商提交请求时,还应使用稳定的 idempotency key,避免超时重试造成重复转账。

更稳妥的替代方案

如果业务需要在付款后立即拆分资金,可以优先考虑支付平台提供的 marketplace、split payment、connected accounts 或 multi-party settlement 功能。支付平台可以在内部维护子账户余额并完成分账,通常比资金先进入一个银行账户,再由该账户发起 N 笔 ACH 更容易对账。

这类产品可能涉及支付服务商、资金传输、KYC、AML、受益所有人核验和账户托管等合规要求。是否适用,应由支付平台和合规顾问根据业务所在地及资金流向确认。

注意事项

  • 不要将 authorization 视为可用资金。
  • 不要认为 capture、settled 或 payout.paid 必然表示银行已经记账。
  • 使用 available balance 时,需要确认其中是否已经扣除 hold、pending debit 和其他不可用资金。
  • webhook 必须验证签名,并按事件 ID 做幂等处理。
  • 银行和支付平台的 webhook 都可能延迟、乱序或重复。
  • ACH 提交成功不代表已经完成最终结算,之后仍可能发生 return。
  • 无法唯一匹配的入账记录应标记为 RECONCILIATION_REQUIRED,不能自动转出。
  • 保留完整的审计记录,包括原始事件、状态变更、匹配依据和操作人。
  • 正式上线前,需要向收单机构确认 payout 的状态含义,并向开户银行确认交易查询接口、记账时间和余额定义。

最可靠的判断条件是:支付平台已经确认 payout,银行侧出现已记账的 credit,并且内部账本完成了唯一匹配和资金预留。仅凭信用卡授权结果,无法安全判断何时可以发起后续 ACH 分配。

备注:内容仅供参考。