Infusionsoft Payment API 如何检查订阅付款失败?
结论
订阅创建接口不会返回后续月份的扣款结果。addRecurringAdv() 只用于创建周期订单,之后的扣款由 Infusionsoft 在后台异步执行。因此,创建订阅时的返回值不能用来判断未来是否会付款失败。
一般有两种处理方式:
- 在 Infusionsoft/Keap 的自动化流程中配置”付款失败”触发器,通过 HTTP Post 通知业务系统。这种方式适合及时更新用户状态。
- 定时通过 API 查询订阅产生的发票和未付金额,用于补偿检查和对账。
推荐方案:付款失败后回调业务系统
在 Campaign Builder 或当前账户提供的自动化功能中配置付款失败触发条件,再通过 HTTP Post 将联系人、订阅或发票标识发送到你的接口。
接收端可以这样处理:
<?php
$contactId = filter_input(INPUT_POST, 'contact_id', FILTER_VALIDATE_INT);
$subscriptionId = filter_input(INPUT_POST, 'subscription_id', FILTER_VALIDATE_INT);
$invoiceId = filter_input(INPUT_POST, 'invoice_id', FILTER_VALIDATE_INT);
if (!$contactId) {
http_response_code(400);
exit('Missing contact_id');
}
// 应先验证请求签名、共享密钥或来源,再更新状态。
updateUserPaymentStatus($contactId, 'payment_failed', [
'subscription_id' => $subscriptionId,
'invoice_id' => $invoiceId,
]);
http_response_code(200);
echo 'OK';
不同版本的 Infusionsoft/Keap 后台可能使用不同的触发器名称,可发送的字段也可能不同。如果自动化界面没有直接提供”付款失败”事件,可以使用失败付款相关的标签、失败处理序列或可用的 webhook 事件,再通过这些事件触发 HTTP Post。
API 轮询方案
如果无法配置回调,可以定时查询订阅状态和对应发票。判断付款结果时,应以发票的实际余额为准,不能只看周期订单是否仍处于启用状态。
使用旧版 PHP SDK 时,查询逻辑可以写成:
<?php
$invoiceId = $app->recurringInvoice($recurringOrderId);
$rows = $app->dsQuery(
'Invoice',
1,
0,
['Id' => $invoiceId],
[
'Id',
'ContactId',
'PayStatus',
'InvoiceTotal',
'TotalPaid',
'TotalDue'
]
);
if (!$rows) {
throw new RuntimeException('Invoice not found');
}
$invoice = $rows[0];
$totalDue = (float) $invoice['TotalDue'];
if ($totalDue > 0) {
// 发票仍有欠款,但此时可能还处于自动重试阶段。
markPaymentAsOutstanding(
(int) $invoice['ContactId'],
(int) $invoice['Id'],
$totalDue
);
} else {
markPaymentAsPaid(
(int) $invoice['ContactId'],
(int) $invoice['Id']
);
}
需要注意,TotalDue > 0 只表示发票尚未结清,并不一定是永久付款失败。Infusionsoft 可能会根据账户中配置的失败付款重试策略再次扣款。只有重试结束、订阅被停止,或者业务规定的宽限期已经过去,才适合将用户改为最终停用状态。
还可以同时查询周期订单:
<?php
$subscriptions = $app->dsQuery(
'RecurringOrder',
1,
0,
['Id' => $subscriptionId],
[
'Id',
'ContactId',
'Status',
'LastBillDate',
'NextBillDate',
'ReasonStopped'
]
);
if ($subscriptions) {
$subscription = $subscriptions[0];
if ((int) $subscription['Status'] === 0) {
suspendUserAccess(
(int) $subscription['ContactId'],
$subscription['ReasonStopped'] ?? null
);
}
}
旧版 SDK 和不同账户配置可能会让 RecurringOrder.Status 返回数字或字符串。生产代码应先记录真实响应,再根据实际返回值判断状态。
主动扣款时检查返回结果
如果程序直接调用 chargeInvoice(),应立即检查返回值:
<?php
$result = $app->chargeInvoice(
$invoiceId,
'API Upsell Payment',
$creditCardId,
$merchantAccountId,
false
);
if (is_array($result)) {
$successful = !empty($result['Successful']);
if (!$successful) {
$message = $result['Message'] ?? 'Payment failed';
markInvoicePaymentFailed($invoiceId, $message);
}
} elseif ($result === false) {
markInvoicePaymentFailed($invoiceId, 'Payment request failed');
}
旧版 SDK 对 chargeInvoice() 返回结果的封装可能不同,不能直接假定字段名称一定是 Successful。可以先安全记录一次脱敏后的实际返回结构,再按照当前 SDK 的结果格式处理。不要记录信用卡号、CVV 或其他敏感支付数据。
另外,示例中的 chargeInvoice(16, ..., 2, 1, false) 使用了固定 ID。实际代码应传入本次生成的 $invoiceID、对应的 $ccId 和正确的 merchant account ID,否则检查到的可能不是当前订阅的付款结果。
实际状态设计
用户状态不宜只分为”正常”和”失败”,至少应区分:
active:发票已付清;payment_retrying:扣款失败或仍有欠款,但系统还在重试或宽限期内;suspended:重试已经结束或宽限期已经届满;cancelled:订阅已经终止。
较稳妥的做法是通过付款失败回调及时处理状态,再每天定时查询发票余额和周期订单状态作为补偿,避免因回调丢失而遗漏用户状态更新。
备注:内容仅供参考。
