如何获取 authorize.net recurring subscription 每次扣款响应
明确结论
创建 Authorize.Net ARB(Automated Recurring Billing)订阅后,后续扣款结果不会返回给最初的订阅创建请求。你需要配置 Authorize.Net Webhooks。每当交易产生或状态变化时,Authorize.Net 会向你的 HTTPS 接口发送通知。
收到通知后,应按以下流程处理:
- 验证
X-ANET-Signature,确认请求来自 Authorize.Net。 - 从通知中获取交易 ID(
transId)。 - 调用
getTransactionDetails查询完整的扣款结果。 - 根据交易 ID 做幂等处理,避免重复入账。
旧项目中可能还在使用 Silent Post URL。这是早期方案,新系统应优先采用 Webhooks,不建议再基于 Silent Post 开发。
为什么创建订阅时拿不到后续响应
创建订阅接口只负责建立扣款计划,成功响应通常包含 subscriptionId。实际的周期扣款由 Authorize.Net 在未来异步执行,所以:
- 后续扣款不会发生在用户当前的 HTTP 请求中;
- 即使用户关闭浏览器,扣款也不会受到影响;
returnUrl或前端跳转地址无法可靠接收支付结果;- 扣款结果必须通过服务器端通知或主动查询获取。
还要区分下面两个标识:
subscriptionId:整个订阅计划的 ID;transId:某一次具体扣款交易的 ID。
同一个订阅会产生多个不同的 transId。
配置 Webhook
在 Authorize.Net Merchant Interface 中找到 Webhooks 配置,添加一个指向服务器的公开 HTTPS 地址,例如:
https://example.com/webhooks/authorize-net
Merchant Interface 的版本和账户权限不同,具体菜单位置可能有所变化。如果界面中没有 Webhooks 入口,也可以通过 Authorize.Net Webhooks API 创建。
至少要订阅支付交易相关的事件,例如:
net.authorize.payment.authcapture.created
net.authorize.payment.void.created
net.authorize.payment.refund.created
如果需要同步订阅生命周期,还可以订阅相应的 subscription 事件,如暂停、终止或取消。可用事件应以当前账户列出的 event types 为准。
测试环境和生产环境相互独立:
- Sandbox:
https://apitest.authorize.net - Production:
https://api.authorize.net
两套环境的 Webhook、API 凭据和 Signature Key 都需要分别配置。
Webhook 通知结构
支付成功时,通知内容通常如下:
{
"notificationId": "notification-id",
"eventType": "net.authorize.payment.authcapture.created",
"eventDate": "2026-08-29T08:00:00.000Z",
"webhookId": "webhook-id",
"payload": {
"responseCode": 1,
"authCode": "ABC123",
"avsResponse": "Y",
"authAmount": 19.99,
"entityName": "transaction",
"id": "12345678901"
}
}
对于交易事件,payload.id 通常就是 transId。Webhook 内容可用于定位事件,但在业务处理中,最好再调用 getTransactionDetails,查询订阅关联、交易状态、金额和错误信息等完整数据。
PHP 接收示例
下面的示例会读取原始请求体、验证签名并获取交易 ID:
<?php
$rawBody = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_ANET_SIGNATURE'] ?? '';
$signatureKey = getenv('AUTHORIZE_NET_SIGNATURE_KEY');
if (!$signatureKey) {
http_response_code(500);
exit('Signature key is not configured');
}
/*
* Authorize.Net Signature Key 是十六进制字符串。
* 计算签名时需要先将其转换为二进制。
*/
$key = hex2bin($signatureKey);
if ($key === false) {
http_response_code(500);
exit('Invalid signature key');
}
$expectedHash = hash_hmac('sha512', $rawBody, $key);
$expectedSignature = 'sha512=' . $expectedHash;
if (!hash_equals(strtolower($expectedSignature), strtolower($receivedSignature))) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
if (!is_array($event)) {
http_response_code(400);
exit('Invalid JSON');
}
$eventType = $event['eventType'] ?? '';
$notificationId = $event['notificationId'] ?? '';
$transactionId = $event['payload']['id'] ?? null;
if ($eventType === 'net.authorize.payment.authcapture.created' && $transactionId) {
/*
* 推荐先持久化 notificationId/transId,再交给队列异步处理。
* 数据库应对 notificationId 或 transId 建立唯一约束。
*/
enqueueAuthorizeNetTransaction($notificationId, $transactionId);
}
/*
* 尽快返回 2xx,避免 Authorize.Net 因超时而重试。
*/
http_response_code(200);
echo 'OK';
function enqueueAuthorizeNetTransaction(
string $notificationId,
string $transactionId
): void {
// 写入数据库或消息队列。
}
验证签名时,必须使用未经解析和重新编码的原始请求体。不能先执行 json_decode(),再使用重新生成的 JSON 计算签名,否则空格或字段编码的差异可能导致验证失败。
查询完整交易结果
取得 transId 后,通过 Authorize.Net Transaction Details API 的 getTransactionDetails 请求查询完整信息:
{
"getTransactionDetailsRequest": {
"merchantAuthentication": {
"name": "YOUR_API_LOGIN_ID",
"transactionKey": "YOUR_TRANSACTION_KEY"
},
"transId": "12345678901"
}
}
Sandbox 请求地址:
https://apitest.authorize.net/xml/v1/request.api
Production 请求地址:
https://api.authorize.net/xml/v1/request.api
响应中需要重点检查:
transaction.transIdtransaction.transactionStatustransaction.responseCodetransaction.responseReasonCodetransaction.responseReasonDescriptiontransaction.authAmounttransaction.subscription.idtransaction.subscription.payNum
处理逻辑示例:
switch ($transactionStatus) {
case 'settledSuccessfully':
markInvoiceAsPaid($transactionId);
break;
case 'declined':
markInvoiceAsFailed($transactionId);
notifyCustomerOfFailedPayment();
break;
case 'voided':
case 'refundSettledSuccessfully':
updateInvoiceAfterReversal($transactionId);
break;
default:
/*
* authorizedPendingCapture、capturedPendingSettlement 等状态
* 尚不能视为最终结算成功。
*/
keepInvoicePending($transactionId, $transactionStatus);
}
authcapture.created 只表示授权并捕获交易已经创建,不代表资金一定完成了结算。如果业务要求结算成功后才开通权益,还需要关注结算状态,并通过后续通知或交易查询确认状态为 settledSuccessfully。
失败扣款如何处理
不能只监听成功交易。ARB 扣款可能因余额不足、卡片过期或发卡行拒绝等原因失败。
能够收到哪些失败事件,以及 payload 包含哪些字段,取决于当前 Webhooks event types 和 Authorize.Net 的实际事件定义。可以组合使用以下方式处理:
- 监听所有可用的支付和订阅状态事件;
- 保存每个 Webhook 的原始内容;
- 使用
transId查询交易详情; - 配置 Merchant Interface 中的 ARB 或交易邮件通知,作为人工兜底;
- 定时调用订阅或交易查询接口进行对账,补偿遗漏的通知。
Webhook 是异步通知机制。不能假设通知只发送一次、严格按照顺序到达,或一定早于其他相关事件。
重要注意事项
- Webhook 地址必须可以从公网访问,并使用有效的 HTTPS 证书。
- 不能只根据请求来源 IP 判断可信性,必须验证
X-ANET-Signature。 - 不要在 Webhook 请求中执行耗时操作。先保存事件并返回
2xx,再异步处理。 - 使用
notificationId和transId做幂等控制,避免重复更新订单或重复发放权益。 - 金额、用户 ID 和订单状态不能只以前端数据为准。
- 不要把 Webhook 的到达顺序视为交易发生顺序。
- Sandbox 中创建的 Webhook 不会自动同步到生产环境。
- Signature Key 和 Transaction Key 用途不同,不能混用。
- 即使已经配置 Webhook,也要保留定时对账任务,主动检查订阅和交易状态。
完整的处理链路如下:
创建 ARB 订阅
→ 保存 subscriptionId
→ Authorize.Net 周期性扣款
→ Webhook 通知你的服务器
→ 验证签名
→ 提取 transId
→ 调用 getTransactionDetails
→ 幂等更新订单、订阅和用户权益
备注:内容仅供参考。
