PAYATHON 2026

Authorize.net Automated Recurring Billing 支付回调

支付老李

结论

Authorize.Net 无法在下一次扣款时直接执行服务器上的 PHP 文件。正确的方式是为站点配置 Authorize.Net Webhooks。ARB 后续扣款成功或订阅状态发生变化时,Authorize.Net 会向指定的 HTTPS URL 发送 POST 请求。PHP 接口验证签名后,再根据交易 ID 查询交易详情并更新数据库。

旧系统可能使用 Silent Post URL 接收交易通知,新项目应优先选择 Webhooks。

处理流程

  1. 创建 ARB subscription,并保存 subscriptionId。
  2. 在 Authorize.Net 后台或通过 Webhooks API 注册回调地址,例如:
https://example.com/webhooks/authorize-net.php
  1. 至少订阅成功支付事件:
net.authorize.payment.authcapture.created

还可以根据业务需要订阅以下生命周期事件:

net.authorize.customer.subscription.suspended
net.authorize.customer.subscription.terminated
net.authorize.customer.subscription.cancelled
net.authorize.customer.subscription.expiring
  1. 收到 Authorize.Net 的 Webhook 后,从请求体中读取交易 ID。
  2. 调用 getTransactionDetails 查询完整的交易信息。
  3. 从交易详情中取得 subscription.id,找到对应的本地订阅记录。
  4. 使用交易 ID 作为幂等键,写入支付记录并更新订阅状态。

PHP Webhook 示例

下面是回调入口的核心结构。AUTHORIZE_NET_SIGNATURE_KEY 应保存在环境变量或密钥管理系统中,不要直接写进代码仓库。

<?php

declare(strict_types=1);

$rawBody = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_ANET_SIGNATURE'] ?? '';
$signatureKey = getenv('AUTHORIZE_NET_SIGNATURE_KEY');

if ($rawBody === false || !$signatureKey || $receivedSignature === '') {
    http_response_code(400);
    exit('Invalid request');
}

$key = hex2bin($signatureKey);

if ($key === false) {
    http_response_code(500);
    exit('Invalid configuration');
}

$expectedSignature = 'sha512=' . strtoupper(
    hash_hmac('sha512', $rawBody, $key)
);

if (!hash_equals($expectedSignature, strtoupper($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'] ?? '';
$transactionId = $event['payload']['id'] ?? null;

if (
    $eventType === 'net.authorize.payment.authcapture.created'
    && is_string($transactionId)
    && $transactionId !== ''
) {
    /*
     * 建议把事件放入队列,然后立即返回 HTTP 200。
     *
     * 后台任务需要:
     * 1. 调用 getTransactionDetails($transactionId)
     * 2. 读取 transaction.subscription.id
     * 3. 检查 transactionId 是否已经处理
     * 4. 在数据库事务中写入付款记录并更新订阅
     */
}

http_response_code(200);
echo 'OK';

比较签名时,必须使用 Webhook 请求的原始 body。不要先解析 JSON,再重新编码并计算签名。

查询交易详情

Webhook 通常只包含事件类型和资源标识,不能仅凭其中的内容确认订单金额。取得交易 ID 后,可以通过 Authorize.Net PHP SDK 调用 getTransactionDetails:

<?php

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

function getTransactionDetails(string $transactionId): AnetAPI\TransactionDetailsType
{
    $merchantAuthentication = new AnetAPI\MerchantAuthenticationType();
    $merchantAuthentication->setName(
        getenv('AUTHORIZE_NET_API_LOGIN_ID')
    );
    $merchantAuthentication->setTransactionKey(
        getenv('AUTHORIZE_NET_TRANSACTION_KEY')
    );

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

    $controller = new AnetController\GetTransactionDetailsController($request);

    $environment = getenv('AUTHORIZE_NET_ENV') === 'production'
        ? \net\authorize\api\constants\ANetEnvironment::PRODUCTION
        : \net\authorize\api\constants\ANetEnvironment::SANDBOX;

    $response = $controller->executeWithApiResponse($environment);

    if ($response === null || $response->getTransaction() === null) {
        throw new RuntimeException(
            'Unable to retrieve Authorize.Net transaction'
        );
    }

    return $response->getTransaction();
}

取得交易详情后,读取订阅 ID 并执行幂等更新:

$transaction = getTransactionDetails($transactionId);
$subscription = $transaction->getSubscription();
$subscriptionId = $subscription?->getId();

if ($subscriptionId === null) {
    throw new RuntimeException('Transaction is not linked to a subscription');
}

/*
 * 在一个数据库事务中执行:
 *
 * INSERT INTO payments (
 *     provider_transaction_id,
 *     provider_subscription_id,
 *     amount,
 *     status
 * ) VALUES (...)
 *
 * provider_transaction_id 应设置 UNIQUE 约束。
 */

不同版本的 Authorize.Net PHP SDK 可能使用略有不同的 getter 名称,请以项目实际安装版本的 SDK 类型定义为准。

数据库设计建议

不要只在 subscription 表中保存”最后付款时间”,最好为每笔交易单独保存记录:

CREATE TABLE subscription_payments (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    subscription_id VARCHAR(64) NOT NULL,
    transaction_id VARCHAR(64) NOT NULL,
    amount DECIMAL(12, 2) NOT NULL,
    status VARCHAR(32) NOT NULL,
    paid_at DATETIME NULL,
    created_at DATETIME NOT NULL,
    UNIQUE KEY uk_transaction_id (transaction_id)
);

Webhook 可能因超时或网络问题而重复发送,因此 transaction_id 必须设置唯一约束。遇到已经处理过的事件时,直接返回成功,不要重复记账。

注意事项

  • 回调地址必须可以从公网访问,并使用有效的 HTTPS 证书。
  • 必须验证 X-ANET-Signature,不能只依赖来源 IP 或请求中的交易 ID。
  • 收到 Webhook 后应尽快返回 2xx。耗时的 API 查询和数据库操作最好交给队列处理。
  • 更新数据库前,应核对交易状态、金额、币种、商户订单信息和 subscriptionId。
  • 成功支付事件无法覆盖所有失败情况。普通扣款失败时不一定会产生成功支付事件,因此还应处理订阅暂停、终止等状态事件,并定期通过 Authorize.Net API 对账。
  • Webhook 只负责发送通知,不能作为唯一的账务依据。系统还应保留定时补偿任务,用于查询遗漏的交易并修复本地状态。
  • Sandbox 和 Production 是相互独立的环境,Webhook、API 凭据与 Signature Key 都需要分别配置。
  • 不要通过用户浏览器的返回页面更新付款状态,因为 ARB 执行后续扣款时,用户通常不在线。

备注:内容仅供参考。