Authorize.net Automated Recurring Billing 支付回调
支付老李
结论
Authorize.Net 无法在下一次扣款时直接执行服务器上的 PHP 文件。正确的方式是为站点配置 Authorize.Net Webhooks。ARB 后续扣款成功或订阅状态发生变化时,Authorize.Net 会向指定的 HTTPS URL 发送 POST 请求。PHP 接口验证签名后,再根据交易 ID 查询交易详情并更新数据库。
旧系统可能使用 Silent Post URL 接收交易通知,新项目应优先选择 Webhooks。
处理流程
- 创建 ARB subscription,并保存
subscriptionId。 - 在 Authorize.Net 后台或通过 Webhooks API 注册回调地址,例如:
https://example.com/webhooks/authorize-net.php
- 至少订阅成功支付事件:
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
- 收到 Authorize.Net 的 Webhook 后,从请求体中读取交易 ID。
- 调用
getTransactionDetails查询完整的交易信息。 - 从交易详情中取得
subscription.id,找到对应的本地订阅记录。 - 使用交易 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 执行后续扣款时,用户通常不在线。
备注:内容仅供参考。
