PAYATHON 2026

如何同时自动创建 Direct Debit mandate 和 subscription?

支付阿杰

结论

不能在同一个 Billing Request 中直接创建 Direct Debit mandate 和 subscription。Billing Request Flow 用于收集付款人信息并建立 mandate。mandate 只是扣款授权,不会自行生成付款计划。

可以按以下流程实现自动化:

  1. 客户完成 Billing Request Flow。
  2. 确认 Billing Request 的状态已变为 fulfilled。
  3. 从 Billing Request 中读取新建的 mandate ID。
  4. 调用 Subscriptions API,使用该 mandate 创建 subscription。

生产环境中,建议通过 webhook 触发 subscription 的创建,不要依赖客户返回 redirect_uri。

为什么创建 mandate 后没有付款

mandate 和 subscription 是两种不同的资源:

  • mandate:授权商户从客户的银行账户发起 Direct Debit。
  • subscription:定义扣款金额、频率和开始日期,并按计划自动创建后续 payment。

当前代码只完成了授权。即使 mandate 创建成功,GoCardless 也无法自行确定扣款金额、扣款周期和首次扣款日期。

获取新创建的 mandate

客户完成 hosted page 后,重新查询原来的 Billing Request。确认其状态为 fulfilled,再从 links.mandate 中读取 mandate ID。

$billingRequest = $client->billingRequests()->get($br->id);

if ($billingRequest->status !== 'fulfilled') {
    throw new RuntimeException(
        'Billing Request 尚未完成,当前状态:' . $billingRequest->status
    );
}

$mandateId = $billingRequest->links->mandate;

if (empty($mandateId)) {
    throw new RuntimeException('Billing Request 中没有可用的 mandate ID');
}

返回对象的具体属性形式可能因 GoCardless PHP SDK 版本而异。如果 SDK 返回的是数组,需要改用对应的数组访问方式。

通过 API 创建 subscription

取得 mandate ID 后,调用 Subscriptions API:

$subscription = $client->subscriptions()->create([
    "params" => [
        "amount" => 2500,
        "currency" => "GBP",
        "name" => "Monthly membership",
        "interval_unit" => "monthly",
        "interval" => 1,
        "day_of_month" => 15,
        "links" => [
            "mandate" => $mandateId,
        ],
        "metadata" => [
            "plan_id" => "monthly-membership",
            "billing_request_id" => $br->id,
        ],
    ],
]);

amount 使用最小货币单位。对于 GBP,2500 表示 £25.00。

interval_unit、day_of_month 和 start_date 等字段有组合限制。例如,设置 day_of_month 后能否同时设置 start_date,取决于当前 GoCardless API 对 subscription 参数的校验规则。不要同时提交相互冲突的日期设置。

如果需要按指定日期开始,也可以使用类似配置:

$subscription = $client->subscriptions()->create([
    "params" => [
        "amount" => 2500,
        "currency" => "GBP",
        "name" => "Monthly membership",
        "interval_unit" => "monthly",
        "interval" => 1,
        "start_date" => "2026-10-15",
        "links" => [
            "mandate" => $mandateId,
        ],
    ],
]);

开始日期需要满足 Direct Debit scheme 对提前通知和银行处理时间的要求。如果日期过早,API 可能会拒绝请求。

推荐使用 webhook 自动创建

不要完全依靠 redirect_uri 对应的页面创建 subscription。客户完成授权后可能直接关闭浏览器,也可能重复访问成功页。浏览器重定向不能充当可信的服务端完成通知。

建议采用以下流程:

创建 Billing Request
        ↓
创建 Billing Request Flow
        ↓
客户完成 hosted page
        ↓
GoCardless 发送 billing request fulfilled 事件
        ↓
服务端查询 Billing Request
        ↓
取得 links.mandate
        ↓
调用 Subscriptions API

Webhook 处理程序应先验证 webhook 签名,再根据事件中的 Billing Request ID 查询最新状态。以下代码省略了签名验证和事件解析,只保留核心逻辑:

$billingRequestId = $eventBillingRequestId;

$billingRequest = $client->billingRequests()->get($billingRequestId);

if ($billingRequest->status !== 'fulfilled') {
    return;
}

$mandateId = $billingRequest->links->mandate;

if (empty($mandateId)) {
    throw new RuntimeException('找不到 Billing Request 创建的 mandate');
}

// 先查询本地数据库,防止同一 webhook 被重复处理
if (subscriptionAlreadyCreated($billingRequestId)) {
    return;
}

$subscription = $client->subscriptions()->create([
    "params" => [
        "amount" => 2500,
        "currency" => "GBP",
        "name" => "Monthly membership",
        "interval_unit" => "monthly",
        "interval" => 1,
        "links" => [
            "mandate" => $mandateId,
        ],
        "metadata" => [
            "billing_request_id" => $billingRequestId,
        ],
    ],
]);

saveSubscriptionMapping(
    $billingRequestId,
    $mandateId,
    $subscription->id
);

Webhook 事件可能重试或重复投递,因此处理逻辑必须具备幂等性。至少要在本地保存 Billing Request、mandate 和 subscription 之间的对应关系,并在创建前检查该请求是否已经处理。

如果需要立即收取首笔款项

如果客户完成授权后还需要支付一次性首付款,可以考虑在 Billing Request 中同时配置 payment_request。这会创建一笔 payment,但不能代替长期 subscription。

配置方式大致如下:

$br = $client->billingRequests()->create([
    "params" => [
        "mandate_request" => [
            "scheme" => "bacs",
            "currency" => "GBP",
        ],
        "payment_request" => [
            "amount" => 2500,
            "currency" => "GBP",
            "description" => "Initial membership payment",
        ],
    ],
]);

这些字段能否同时使用,以及是否需要其他关联参数,取决于当前账户配置、API 版本和 Billing Requests API 的参数要求。即使首笔 payment 创建成功,后续的周期性扣款仍需单独创建 subscription。

另一种方式:自行创建周期付款

如果收费计划没有固定金额或固定周期,例如:

  • 每月金额不同;
  • 按实际使用量结算;
  • 扣款日期经常变化;
  • 每次扣款前都需要人工确认或业务规则判断;

可以不使用 subscription,而是在自己的系统中维护账期,并通过 Payments API,使用同一个 mandate 创建每一笔 payment。

这种方式更灵活,但商户需要自行处理调度、重试、幂等控制、状态同步和失败情况。对于固定周期、固定金额的收费计划,Subscriptions API 通常更合适。

注意事项

  • 不要在创建 Billing Request 后立即创建 subscription,因为此时 mandate 通常尚未建立。
  • 不要只依赖浏览器重定向结果,服务端应重新查询 Billing Request。
  • Webhook 必须验证签名,并安全处理重复事件。
  • 将套餐、价格或订单标识保存在自己的数据库中,或者写入允许使用的 metadata,以便 webhook 判断需要创建哪种 subscription。
  • 金额使用最小货币单位,GBP 的 2500 表示 £25.00。
  • subscription 创建成功不代表款项已经到账,后续 payment 仍会经历提交、确认或失败等状态。
  • 如果 subscription 创建失败,应记录失败原因并安排重试,避免 mandate 已经建立,但业务系统中没有对应的付款计划。
  • 开发阶段应在 sandbox 中测试完整流程,包括 webhook 重复投递、subscription 创建失败和客户中途退出等情况。

备注:内容仅供参考。