PAYATHON 2026

如何设置 Authorize.net ARB 的 Silent Post URL?

支付老李

结论

Silent Post URL 应设置为公开的 HTTPS POST 接口,用于接收每次 ARB 周期扣款的交易结果。系统可以通过 x_subscription_id、x_subscription_paynum 和 x_response_code 判断对应的订阅、扣款期数和交易结果。

不过,不能把 Silent Post 当作订阅计划的唯一数据源:

  • 下一次 billing date 应根据创建 ARB 时的 startDate、interval 和已完成期数,在本地计算并保存。
  • 创建或更新订阅时,应保存银行卡的到期月份和年份。之后通过 Authorize.Net 查询,通常只能取得掩码信息。
  • Silent Post 没有可靠的请求签名。收到通知后,应调用 Authorize.Net Transaction Details API 核验交易。
  • 新系统应优先评估 Authorize.Net Webhooks。Silent Post 可以用于兼容旧系统,但不适合单独承担全部订阅状态同步工作。

Silent Post 接口如何工作

在 Authorize.Net Merchant Interface 中配置 Silent Post URL 后,每当 ARB 产生一笔交易,Authorize.Net 都会以 application/x-www-form-urlencoded 格式向该地址发送交易字段。

ARB 通知中主要使用以下字段:

字段含义
x_subscription_idARB subscription ID
x_subscription_paynum当前是第几次周期付款
x_trans_id本次交易 ID
x_response_code交易响应码
x_response_reason_code更具体的原因码
x_response_reason_text原因说明
x_amount本次交易金额
x_invoice_num创建交易时关联的订单号,若有
x_cust_id客户标识,若创建订阅时提供

常见的 x_response_code 包括:

  • 1:Approved
  • 2:Declined
  • 3:Error
  • 4:Held for Review

业务逻辑不能只依赖 x_response_reason_text。程序应优先判断响应码,再通过 x_trans_id 查询实际交易状态。

PHP 接收示例

下面是一份简化示例。生产环境还需要加入数据库事务、日志脱敏和 Authorize.Net API 核验。

<?php

// Authorize.Net 发送的是 form-urlencoded POST 数据
$subscriptionId = $_POST['x_subscription_id'] ?? null;
$paymentNumber  = $_POST['x_subscription_paynum'] ?? null;
$transactionId  = $_POST['x_trans_id'] ?? null;
$responseCode   = $_POST['x_response_code'] ?? null;
$reasonCode     = $_POST['x_response_reason_code'] ?? null;
$amount         = $_POST['x_amount'] ?? null;

if ($subscriptionId === null || $transactionId === null) {
    http_response_code(400);
    exit('Missing required fields');
}

// 必须通过 subscription_id 找到本地订阅,不能根据姓名或金额匹配
$subscription = findSubscriptionByGatewayId($subscriptionId);

if ($subscription === null) {
    // 保存异常通知以便人工检查,但不要创建未知用户或自动授权
    recordUnmatchedNotification($_POST);
    http_response_code(200);
    exit('OK');
}

// 幂等处理:同一个交易通知即使重复到达,也只能处理一次
if (transactionAlreadyProcessed($transactionId)) {
    http_response_code(200);
    exit('OK');
}

/*
 * 推荐步骤:
 * 1. 使用 x_trans_id 调用 Authorize.Net getTransactionDetails。
 * 2. 核对 subscription ID、金额、merchant account 和交易状态。
 * 3. 核验通过后再修改用户权益。
 */
$verifiedTransaction = verifyTransactionWithAuthorizeNet($transactionId);

if (!$verifiedTransaction->isValid()) {
    recordVerificationFailure($subscriptionId, $transactionId);
    http_response_code(200);
    exit('OK');
}

beginDatabaseTransaction();

try {
    savePaymentResult([
        'subscription_id' => $subscriptionId,
        'payment_number'  => $paymentNumber,
        'transaction_id'  => $transactionId,
        'response_code'   => $responseCode,
        'reason_code'     => $reasonCode,
        'amount'          => $amount,
    ]);

    switch ((string) $responseCode) {
        case '1':
            activateOrExtendUserAccess($subscription->userId);
            updateNextBillingDate($subscription);
            break;

        case '2':
            markPaymentAsDeclined($subscription->id);
            notifyUserOfFailedPayment($subscription->userId);
            break;

        case '3':
            markPaymentAsError($subscription->id);
            queueSubscriptionForReview($subscription->id);
            break;

        case '4':
            markPaymentAsPendingReview($subscription->id);
            break;

        default:
            queueSubscriptionForReview($subscription->id);
    }

    commitDatabaseTransaction();
} catch (Throwable $e) {
    rollbackDatabaseTransaction();
    throw $e;
}

http_response_code(200);
echo 'OK';

不要把完整的 $_POST 直接写入日志。通知中可能包含姓名、地址等个人信息,只应记录排障所需的字段,并对这些信息进行脱敏。

为什么还要调用 Transaction Details API

Silent Post 本质上是发送到公开 URL 的表单请求。仅凭来源 IP、请求头或 POST 字段,通常无法确认通知一定来自 Authorize.Net。

收到 x_trans_id 后,可以使用 Authorize.Net SDK 调用 getTransactionDetails,并核对以下内容:

  • 交易是否属于自己的 merchant account;
  • 金额是否与本地订阅一致;
  • 交易状态是否符合预期;
  • 交易是否关联预期的 ARB subscription;
  • 该 x_trans_id 是否已经处理。

示意代码如下:

<?php

use net\authorize\api\contract\v1 as AnetAPI;
use net\authorize\api\controller as AnetController;

$merchantAuthentication = new AnetAPI\MerchantAuthenticationType();
$merchantAuthentication->setName($apiLoginId);
$merchantAuthentication->setTransactionKey($transactionKey);

$request = new AnetAPI\GetTransactionDetailsRequest();
$request->setMerchantAuthentication($merchantAuthentication);
$request->setTransId($transactionId);

$controller = new AnetController\GetTransactionDetailsController($request);
$response = $controller->executeWithApiResponse(
    AnetAPI\ANetEnvironment::PRODUCTION
);

if (
    $response === null ||
    $response->getMessages()->getResultCode() !== 'Ok'
) {
    throw new RuntimeException('Unable to verify Authorize.Net transaction');
}

$transaction = $response->getTransaction();

能够取得哪些 ARB 关联字段,取决于 API 响应和 SDK 版本。程序应检查实际返回值,不能假定每笔交易都包含完整的订阅资料。

下一次 billing date 应如何计算

创建 ARB subscription 时,系统已经提交以下计划信息:

startDate
interval.length
interval.unit
totalOccurrences
trialOccurrences

这些信息应与 Authorize.Net 返回的 subscriptionId 一起保存到本地数据库,例如:

gateway_subscription_id
start_date
interval_length
interval_unit
total_occurrences
completed_occurrences
next_billing_date
status
card_expiration_month
card_expiration_year

如果周期按月计算,应使用日历月份运算,不能简单增加 30 天:

<?php

$startDate = new DateTimeImmutable('2026-01-15');
$paymentNumber = 3;

// 第 1 次发生在 startDate,因此第 3 次增加 2 个月
$billingDate = $startDate->modify('+' . ($paymentNumber - 1) . ' months');

实际项目还要处理以下情况:

  • 每月 29、30、31 日遇到短月份;
  • trial period 是否计入付款期数;
  • 用户暂停、取消或修改订阅;
  • 扣款失败后,本地权益是否立即终止;
  • Authorize.Net 计划状态与本地状态不一致。

不要自行猜测日期边界规则。创建订阅后,应通过 ARB 查询接口读取订阅计划,并使用 Authorize.Net 返回的数据校准本地记录。通知文案最好写成”预计于某日扣款”,不要承诺具体的扣款时刻或入账时刻。

如何提前发送扣款提醒

Silent Post 只会在交易发生后到达,不能用来触发”即将扣款”提醒。

系统应运行本地定时任务,每天查询未来若干天内即将扣款的订阅:

SELECT *
FROM subscriptions
WHERE status = 'active'
  AND next_billing_date = CURRENT_DATE + INTERVAL 3 DAY
  AND reminder_sent_at IS NULL;

发送提醒后要记录状态,以免重复通知:

您的订阅预计将在 2026-09-01 自动续费,扣款金额为 ¥99.00。
如需更新付款方式,请在扣款日前完成操作。

扣款成功后,应根据当前计划计算下一期日期。订阅被更新、暂停或取消时,也要同步重新计算。

如何判断银行卡何时到期

创建或更新 ARB 付款资料时,应将非敏感的到期月份和年份保存到自己的系统中,例如:

expiration_month = 09
expiration_year  = 2027
card_last_four   = 1234

不要保存完整卡号、CVV 或其他敏感认证数据。授权完成后不得存储 CVV。银行卡数据的采集、传输和保存还必须符合 PCI DSS 要求。

更安全的方式是使用 Authorize.Net 托管表单或 Customer Profiles/token,避免服务器直接处理完整卡号。

可以通过定时任务生成到期提醒:

<?php

$expiration = new DateTimeImmutable('2027-09-01');
$expirationEnd = $expiration->modify('last day of this month');

if ($expirationEnd <= new DateTimeImmutable('+30 days')) {
    notifyUserToUpdateCard();
}

部分查询接口只返回掩码卡号或掩码到期信息,因此不能依赖事后查询来恢复原始到期年月。如果现有数据库没有保存这两个字段,需要让用户通过安全的付款方式更新流程重新提交。

账户升级和降级策略

建议分别管理”支付状态”和”用户权益状态”:

  • 扣款成功:记录付款并延长权益有效期。
  • 扣款拒绝:标记为 past_due,通知用户更新付款方式。
  • 系统错误:进入重试或人工检查队列,不要立即降级。
  • Held for Review:保持待处理状态,等待最终结果。
  • 连续失败或超过宽限期:再降级用户。
  • 订阅取消:停止未来续费。已支付权益是否立即终止,由业务规则决定。

不要因为一次网络异常、Silent Post 丢失或 x_response_code = 3 就立即关闭账户。可以设置宽限期,并定期通过 Authorize.Net API 对账。

注意事项

  • Silent Post endpoint 必须能从公网访问,并使用有效的 HTTPS 证书。
  • 接口应尽快返回 HTTP 200,耗时任务应放入队列处理。
  • 必须使用 x_trans_id 或”订阅 ID + 付款期数”实现幂等。
  • 不要假定通知只会发送一次,也不能依赖每次通知都能成功送达。
  • 不要仅凭 Silent Post 更新高价值账户权益,应先查询并核验交易。
  • Sandbox 和 Production 的凭据、endpoint、subscription ID 必须严格分开。
  • 定期主动对账,修复通知遗漏、本地任务失败或人工修改造成的状态差异。
  • 新实现应评估 Webhooks 是否能覆盖所需事件。Silent Post 主要用于兼容交易结果通知,预扣款提醒仍需依靠本地订阅日历和定时任务。

备注:内容仅供参考。