PAYATHON 2026

Infusionsoft Payment API 如何检查订阅付款失败?

支付阿杰

结论

订阅创建接口不会返回后续月份的扣款结果。addRecurringAdv() 只用于创建周期订单,之后的扣款由 Infusionsoft 在后台异步执行。因此,创建订阅时的返回值不能用来判断未来是否会付款失败。

一般有两种处理方式:

  1. 在 Infusionsoft/Keap 的自动化流程中配置”付款失败”触发器,通过 HTTP Post 通知业务系统。这种方式适合及时更新用户状态。
  2. 定时通过 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:订阅已经终止。

较稳妥的做法是通过付款失败回调及时处理状态,再每天定时查询发票余额和周期订单状态作为补偿,避免因回调丢失而遗漏用户状态更新。

备注:内容仅供参考。