如何通过 API 判断信用卡交易资金何时到账
结论
没有一种 API 能够适用于所有银行和支付网关,并根据信用卡交易直接、准确地判断资金是否已经记入商户银行账户。
信用卡授权成功只代表发卡行同意这笔交易,不代表资金已经到账。要自动分配资金,需要结合以下信息:
- 支付网关或收单机构提供的结算批次、余额和 payout 状态;
- 商户银行提供的账户交易查询、余额查询或 webhook;
- 内部账本,用来关联信用卡交易、结算批次、银行入账和后续 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 分配。
备注:内容仅供参考。
