如何设置 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_id | ARB 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:Approved2:Declined3:Error4: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 主要用于兼容交易结果通知,预扣款提醒仍需依靠本地订阅日历和定时任务。
备注:内容仅供参考。
