PAYATHON 2026

如何获取 Authorize.NET ARB 每笔定期交易详情

支付老李

结论

可以获取并保存 ARB 每次扣款产生的交易详情,但不能只依赖 ARB 订阅查询接口。

ARB 负责维护订阅计划。每次实际扣款成功或失败后,都会生成一笔普通的 Authorize.NET 交易,并拥有独立的 transId。建议按以下方式处理:

  1. 使用 Authorize.NET Webhooks 接收交易事件。
  2. 从事件中获取交易 ID。
  3. 调用 getTransactionDetails 查询完整交易信息。
  4. 根据 subscription.id 和 subscription.payNum,将交易关联到本地订阅及对应期数。
  5. 以 transId 为唯一键,将结果写入本地数据库。
  6. 定期使用 Transaction Reporting API 补查,防止通知丢失造成数据缺口。

对于计划在 2015 年 1 月 1 日执行的年度扣款,可以在交易处理后取得详情。但计划日期不等于交易完成或结算时间。扣款可能失败、重试、延迟处理,也可能稍后才完成结算。

为什么不能只查询 ARB 订阅

ARB 订阅接口主要返回订阅本身的信息,例如:

  • 订阅 ID
  • 当前状态
  • 付款计划
  • 下一次计划扣款日期
  • 已完成和剩余的付款次数

它不是逐笔交易流水接口,因此查询订阅状态不能代替查询每笔支付交易。

实际扣款完成后,Authorize.NET 会创建对应的交易记录。可以使用 Transaction Reporting API 查询这些交易,例如:

  • getTransactionDetails
  • getSettledBatchList
  • getTransactionList
  • getUnsettledTransactionList

调用 getTransactionDetails 时必须提供 transId。如果还不知道交易 ID,可以先从 Webhook 获取,或通过批次和交易列表接口查找。

推荐实现流程

1. 创建订阅时保存对应关系

调用 ARBCreateSubscription 成功后,将返回的订阅 ID 保存到本地,例如:

LocalCustomerId
LocalSubscriptionId
AuthorizeNetSubscriptionId
StartDate
Interval
Amount
Status

后续可以通过交易详情中的 subscription.id 关联这条记录。

2. 配置 Webhook

为支付相关事件配置 Webhook。收到事件后,先保存以下内容:

  • Webhook 通知 ID
  • eventType
  • payload.id,通常用于标识交易
  • 通知接收时间
  • 原始通知内容
  • 当前处理状态

具体事件名称和载荷字段可能随 Authorize.NET Webhook 规范调整,应以账户环境和官方 API 文档中的实际定义为准。

Webhook 只表示有事件发生。不要仅凭通知正文认定交易已经结算,还需要查询交易详情并确认状态。

3. 使用交易 ID 查询完整详情

下面的示例使用 Authorize.NET .NET SDK 中常见的 getTransactionDetails 调用方式。不同 SDK 版本的命名空间或控制器构造方式可能略有不同。

using System;
using AuthorizeNet.Api.Contracts.V1;
using AuthorizeNet.Api.Controllers;
using AuthorizeNet.Api.Controllers.Bases;

public sealed class AuthorizeNetTransactionService
{
    private readonly merchantAuthenticationType _authentication;

    public AuthorizeNetTransactionService(
        string apiLoginId,
        string transactionKey,
        bool useProduction)
    {
        _authentication = new merchantAuthenticationType
        {
            name = apiLoginId,
            ItemElementName = ItemChoiceType.transactionKey,
            Item = transactionKey
        };

        ApiOperationBase<ANetApiRequest, ANetApiResponse>.RunEnvironment =
            useProduction
                ? AuthorizeNet.Environment.PRODUCTION
                : AuthorizeNet.Environment.SANDBOX;
    }

    public transactionDetailsType GetTransaction(string transactionId)
    {
        var request = new getTransactionDetailsRequest
        {
            merchantAuthentication = _authentication,
            transId = transactionId
        };

        var controller = new getTransactionDetailsController(request);
        controller.Execute();

        var response = controller.GetApiResponse();

        if (response == null)
        {
            throw new InvalidOperationException(
                "Authorize.NET did not return a response.");
        }

        if (response.messages.resultCode != messageTypeEnum.Ok)
        {
            string errorCode =
                response.messages.message?[0].code ?? "unknown";

            string errorText =
                response.messages.message?[0].text ?? "Unknown error";

            throw new InvalidOperationException(
                $"Authorize.NET error {errorCode}: {errorText}");
        }

        return response.transaction;
    }
}

调用示例:

var service = new AuthorizeNetTransactionService(
    apiLoginId: "YOUR_API_LOGIN_ID",
    transactionKey: "YOUR_TRANSACTION_KEY",
    useProduction: false);

transactionDetailsType transaction =
    service.GetTransaction("TRANSACTION_ID_FROM_WEBHOOK");

Console.WriteLine($"Transaction ID: {transaction.transId}");
Console.WriteLine($"Status: {transaction.transactionStatus}");
Console.WriteLine($"Amount: {transaction.settleAmount}");
Console.WriteLine($"Submit Time UTC: {transaction.submitTimeUTC}");

if (transaction.subscription != null)
{
    Console.WriteLine(
        $"Subscription ID: {transaction.subscription.id}");

    Console.WriteLine(
        $"Payment Number: {transaction.subscription.payNum}");
}

处理 ARB 交易时,应重点检查:

transaction.subscription.id
transaction.subscription.payNum

subscription.id 用于关联订阅,subscription.payNum 用于确认这是该订阅的第几期付款。字段是否存在仍要以实际交易类型和接口响应为准,代码也必须允许 subscription 为空。

4. 幂等写入数据库

Webhook 可能重复投递,补查任务也可能再次发现同一笔交易,因此不能在每次收到通知时直接新增记录。

建议为 Authorize.NET 的 transId 建立唯一约束:

CREATE UNIQUE INDEX UX_RecurringTransaction_TransactionId
ON RecurringTransaction (TransactionId);

可以保存以下字段:

TransactionId
AuthorizeNetSubscriptionId
PaymentNumber
TransactionStatus
TransactionType
AuthorizedAmount
SettledAmount
SubmitTimeUtc
SettlementTimeUtc
ResponseCode
ResponseReasonCode
ResponseReasonDescription
AccountType
MaskedAccountNumber
RawResponse
CreatedAtUtc
UpdatedAtUtc

收到重复通知时,根据 TransactionId 更新已有记录。不要保存完整卡号、银行账户号、CVV 或其他不应写入数据库的敏感支付信息。

不知道交易 ID 时如何补查

如果未收到 Webhook、系统曾停机,或旧系统没有保存通知,可以使用 Transaction Reporting API 补查:

  1. 调用 getSettledBatchList,获取指定时间范围内的结算批次。
  2. 对每个批次调用 getTransactionList。
  3. 检查交易记录中的订阅信息。
  4. 根据订阅 ID、付款期数和交易 ID,与本地记录匹配。
  5. 如有需要,再调用 getTransactionDetails 获取单笔交易的完整详情。

尚未结算的交易可以通过 getUnsettledTransactionList 检查。“已创建”、“已授权”、“已捕获”和”已结算”属于不同状态,数据库最好分别记录交易状态和结算状态。

补查任务可以定期运行,例如每小时查询近期批次,并适当扩大时间窗口。由于写入时以 transId 保证幂等,重复扫描不会产生重复记录。

关于 Silent Post

较早的 Authorize.NET ARB 集成经常使用 Silent Post URL。扣款发生后,服务器会收到类似普通支付响应的表单字段,其中可能包括:

  • x_trans_id
  • x_response_code
  • x_response_reason_code
  • x_subscription_id
  • x_subscription_paynum

维护旧系统时,可以利用这些字段定位交易,再调用 getTransactionDetails。

新开发通常更适合使用 Webhooks,同时保留 Transaction Reporting API 作为补偿机制。是否继续使用 Silent Post,应根据当前 Authorize.NET 账户功能和官方支持状态决定。

需要特别注意的事项

  • nextPaymentDate 是计划执行日期,不能证明扣款成功或已经结算。
  • 交易失败时也应保存结果,否则本地账务会缺少失败和重试记录。
  • Webhook 接口应尽快返回成功响应,后续查询和入库工作由后台任务处理。
  • Webhook 通知可能重复、延迟或乱序,处理逻辑必须保证幂等。
  • 沙箱中的定期任务调度行为不一定与生产环境中的自然年度周期完全相同,应使用适合测试的周期验证流程。
  • API 凭据必须存放在安全配置或密钥管理系统中,不能硬编码到源码。
  • 本地数据库应以 transId 识别交易,以订阅 ID 识别订阅,以 payNum 识别付款期数。
  • 如果业务以”已结算”作为最终入账标准,应在查询到结算状态后再确认收入,不能在收到交易创建通知时立即确认。

更稳妥的整体架构是使用 Webhook 实时发现交易,通过 getTransactionDetails 获取完整详情,再由 Transaction Reporting API 定期对账补漏。这样既能记录 2015 年 1 月 1 日这类计划扣款,也能处理失败、重试、重复通知和结算延迟。

备注:内容仅供参考。