PAYATHON 2026

如何获取 authorize.net recurring subscription 每次扣款响应

支付小周

明确结论

创建 Authorize.Net ARB(Automated Recurring Billing)订阅后,后续扣款结果不会返回给最初的订阅创建请求。你需要配置 Authorize.Net Webhooks。每当交易产生或状态变化时,Authorize.Net 会向你的 HTTPS 接口发送通知。

收到通知后,应按以下流程处理:

  1. 验证 X-ANET-Signature,确认请求来自 Authorize.Net。
  2. 从通知中获取交易 ID(transId)。
  3. 调用 getTransactionDetails 查询完整的扣款结果。
  4. 根据交易 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.transId
  • transaction.transactionStatus
  • transaction.responseCode
  • transaction.responseReasonCode
  • transaction.responseReasonDescription
  • transaction.authAmount
  • transaction.subscription.id
  • transaction.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 的实际事件定义。可以组合使用以下方式处理:

  1. 监听所有可用的支付和订阅状态事件;
  2. 保存每个 Webhook 的原始内容;
  3. 使用 transId 查询交易详情;
  4. 配置 Merchant Interface 中的 ARB 或交易邮件通知,作为人工兜底;
  5. 定时调用订阅或交易查询接口进行对账,补偿遗漏的通知。

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
    → 幂等更新订单、订阅和用户权益

备注:内容仅供参考。