如何配置 Authorize.net ARB 的 Silent Post URL?
结论
Silent Post URL 应指向一个可从公网访问的 HTTPS 接口,用于接收每笔 ARB 扣款的异步交易结果。收到通知后,接口先检查幂等性,再通过 Authorize.net 的交易查询 API 核实交易,确认无误后更新本地账单和用户权限。
Silent Post 主要报告扣款成功、拒绝、处理错误等交易结果,无法完整覆盖订阅取消、暂停、到期等生命周期变化。
Authorize.net 通常也不会提供可直接用于提醒的可靠“下一次扣款时间”。系统需要在创建订阅时保存扣款计划,自行计算预计扣款日。银行卡到期年月同样应在首次收集支付信息时单独保存,不能依赖回调获取。
新系统还可以评估 Authorize.net Webhooks。Silent Post 是较早的通知机制,可用事件和后台入口可能随账户配置而异。
配置 Silent Post URL
登录 Authorize.net Merchant Interface,在账户设置中找到 Silent Post URL。不同版本的后台菜单名称可能不同,一般位于 Account 或 Settings 下的交易通知设置中。
填写完整的 HTTPS 地址,例如:
https://billing.example.com/webhooks/authorize-net/silent-post
配置时应满足以下要求:
- 分别为 Sandbox 和 Production 环境配置 URL。
- URL 必须能被 Authorize.net 从公网访问。
- 接口需要接受
POST请求和application/x-www-form-urlencoded数据。 - 地址不能要求登录、Cookie 或浏览器交互。
- 回调不能重定向到其他地址。
- 成功接收后返回 HTTP
200。 - 回调处理时间应尽量缩短,耗时业务可交给队列执行。
普通订单的返回地址和用户付款后的跳转地址不能与 Silent Post URL 混用。Silent Post 是服务器之间的通知,不会将用户浏览器带回网站。
Silent Post 会发送什么
ARB 完成扣款后,Silent Post 通常会发送一组以 x_ 开头的表单字段,常用字段包括:
x_response_code
x_response_reason_code
x_response_reason_text
x_trans_id
x_subscription_id
x_subscription_paynum
x_amount
x_response_code 的常见取值如下:
| 值 | 含义 | 建议处理 |
|---|---|---|
1 | Approved | 核实交易后确认付款 |
2 | Declined | 记录拒付,进入催款或宽限流程 |
3 | Error | 记录错误,等待重试或人工处理 |
4 | Held for Review | 标记为待审核,不立即开通长期权限 |
实际出现的字段及其内容取决于交易类型和账户配置,代码不能假定所有字段始终存在。
PHP 接收示例
以下示例读取参数,并完成基本校验和幂等处理。findLocalSubscription()、transactionExists() 等函数代表项目自己的数据访问逻辑。
<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
$payload = $_POST;
$transactionId = trim((string) ($payload['x_trans_id'] ?? ''));
$subscriptionId = trim((string) ($payload['x_subscription_id'] ?? ''));
$responseCode = (string) ($payload['x_response_code'] ?? '');
$reasonCode = (string) ($payload['x_response_reason_code'] ?? '');
$paymentNumber = (string) ($payload['x_subscription_paynum'] ?? '');
$amount = (string) ($payload['x_amount'] ?? '');
if ($transactionId === '' || $subscriptionId === '') {
http_response_code(400);
exit;
}
$subscription = findLocalSubscription($subscriptionId);
if ($subscription === null) {
recordUnmatchedCallback($payload);
http_response_code(200);
exit;
}
/*
* x_trans_id 应设置唯一约束。
* Authorize.net 重复发送通知时,不应重复延长会员期限或重复记账。
*/
if (transactionExists($transactionId)) {
http_response_code(200);
exit;
}
/*
* 不要仅凭 POST 参数修改用户权限。
* 应调用 Authorize.net Get Transaction Details API,
* 核对 transactionId、subscriptionId、amount 和交易状态。
*/
queueTransactionVerification([
'transaction_id' => $transactionId,
'subscription_id' => $subscriptionId,
'response_code' => $responseCode,
'reason_code' => $reasonCode,
'payment_number' => $paymentNumber,
'amount' => $amount,
'raw_payload' => $payload,
]);
http_response_code(200);
使用 Authorize.net PHP SDK 时,可以通过 GetTransactionDetailsRequest 查询交易。下面是基本调用方式,生产环境还需检查 API 返回码和空值:
<?php
use net\authorize\api\contract\v1 as AnetAPI;
use net\authorize\api\controller as AnetController;
use net\authorize\api\constants\ANetEnvironment;
function getAuthorizeNetTransaction(string $transactionId): ?AnetAPI\TransactionDetailsType
{
$authentication = new AnetAPI\MerchantAuthenticationType();
$authentication->setName(getenv('AUTHORIZE_NET_API_LOGIN_ID'));
$authentication->setTransactionKey(
getenv('AUTHORIZE_NET_TRANSACTION_KEY')
);
$request = new AnetAPI\GetTransactionDetailsRequest();
$request->setMerchantAuthentication($authentication);
$request->setTransId($transactionId);
$controller = new AnetController\GetTransactionDetailsController($request);
$environment = getenv('APP_ENV') === 'production'
? ANetEnvironment::PRODUCTION
: ANetEnvironment::SANDBOX;
$response = $controller->executeWithApiResponse($environment);
if ($response === null) {
return null;
}
$messages = $response->getMessages();
if (
$messages === null ||
$messages->getResultCode() !== 'Ok'
) {
return null;
}
return $response->getTransaction();
}
查询完成后,至少核对以下内容:
transId是否与回调中的x_trans_id一致。- 交易是否属于预期的商户账户和环境。
- 订阅 ID 是否与本地记录匹配。
- 金额和币种是否符合本期账单。
- 交易最终状态是否允许确认付款。
如何升级或降级用户
支付状态、订阅状态和业务权限应分别保存,不要将 Authorize.net 的状态直接写入用户等级字段。
可以按以下方式处理:
switch ($verifiedResponseCode) {
case '1':
markInvoicePaid($invoiceId, $transactionId);
extendUserAccess($userId, $paidThroughDate);
break;
case '2':
markInvoicePastDue($invoiceId, $reasonCode);
startDunningProcess($userId);
break;
case '3':
markInvoicePaymentError($invoiceId, $reasonCode);
scheduleReconciliation($transactionId);
break;
case '4':
markInvoiceUnderReview($invoiceId);
break;
default:
recordUnknownPaymentState($transactionId, $verifiedResponseCode);
break;
}
扣款成功后,可以升级用户或延长有效期。扣款失败后是否立即降级,应由业务规则决定。常见做法是先设置宽限期,让用户有时间更新银行卡,避免第一次拒付就关闭服务。
订阅处于 suspended、canceled、expired 或 terminated 状态,与单笔交易失败是两类情况。例如,订阅取消后,用户可能仍处于已经支付的服务期内。因此,降级时间通常应以本地保存的 paid_through 日期为准。
Silent Post 无法覆盖所有订阅状态变化。系统还需要定期调用 ARB 订阅查询接口进行对账,也可以使用账户支持的 Webhook 订阅相关事件。
如何获得银行卡到期时间
Silent Post 不适合获取银行卡到期时间。回调中的支付信息通常已经过掩码处理,也不应尝试从回调中恢复完整卡片数据。
如果系统通过支付页面直接收集到期年月,可以在创建订阅时保存:
card_expiry_month = 08
card_expiry_year = 2028
到期年月通常不属于完整银行卡号或 CVV,但仍应按照支付安全要求限制访问和日志输出。不要保存 CVV。除非系统具备相应的 PCI DSS 合规能力,也不要自行保存完整银行卡号。
使用 Accept Hosted、Accept.js 或其他令牌化方案时,应用可能只能取得掩码后的支付信息。能否取得到期年月,取决于具体集成方式和 API 返回内容。如果查询结果只有掩码值,需要在绑卡时单独记录年月,或者让用户进入安全的支付资料更新页面。
银行卡到期提醒应按“到期月份”处理。例如,可以在到期月份前 30 天和 7 天发送更新提醒,不要推断一个并不存在的精确到期日。
如何计算下一次 billing date
创建 ARB 订阅时,应用已经提交了支付计划,通常包括:
startDate
interval.length
interval.unit
totalOccurrences
trialOccurrences
这些信息应保存在本地。每次确认扣款成功后,再按照支付周期推进下一次预计扣款日期。
<?php
function calculateNextBillingDate(
DateTimeImmutable $currentBillingDate,
int $intervalLength,
string $intervalUnit
): DateTimeImmutable {
return match ($intervalUnit) {
'days' => $currentBillingDate->modify(
sprintf('+%d days', $intervalLength)
),
'months' => $currentBillingDate->modify(
sprintf('+%d months', $intervalLength)
),
default => throw new InvalidArgumentException(
'Unsupported interval unit'
),
};
}
这段代码只表达基本计算思路。按月计费时,月末日期需要单独处理。例如,从 1 月 31 日增加一个月,不同语言的日期库可能得到不同结果,也可能与支付网关的排期规则不一致。更安全的方案是在创建订阅时生成并保存计划日期,明确短月份的处理方式,再根据实际交易通知校正。
系统还要区分两个日期:
next_scheduled_billing_date:按订阅计划预计发起扣款的日期。next_retry_date:扣款失败后预计再次尝试的日期。
如果 Authorize.net 的订阅查询响应没有明确返回下一次处理日期,就不能根据订阅状态猜测。可以结合本地计划、已成功期数和已处理交易计算,再通过定期对账修正。
给用户的通知可以写成:
预计将于 2026-11-15 从尾号 1234 的银行卡扣款 ¥99.00。
使用“预计”更准确,因为银行拒付、账户暂停、时区和处理批次都可能影响最终结果。日期应按业务时区展示,数据库中则保留明确的时区或 UTC 时间。
实现时容易忽略的问题
- 为
x_trans_id建立唯一索引,避免重复通知造成重复记账或重复开通权益。 - 回调中的金额、订阅 ID 和交易状态都需要通过服务端 API 复核。
- 不要在日志中记录完整的 POST 数据,其中可能包含姓名、地址或掩码支付信息。
- Sandbox 和 Production 使用不同的凭据、API 环境和回调配置。
- Silent Post、Webhook 和定时对账可能重复报告同一笔交易,各个处理入口应共用一套幂等逻辑。
- 保存原始事件时,需要限制访问并设置合理的保留期限。
- 对账任务应查找“网关已成功、本地未入账”和“本地等待中、网关已有最终结果”的记录。
- 权益变更应以核实后的交易和
paid_through日期为依据,不能只看某一次回调是否到达。
备注:内容仅供参考。
