如何同时自动创建 Direct Debit mandate 和 subscription?
结论
不能在同一个 Billing Request 中直接创建 Direct Debit mandate 和 subscription。Billing Request Flow 用于收集付款人信息并建立 mandate。mandate 只是扣款授权,不会自行生成付款计划。
可以按以下流程实现自动化:
- 客户完成 Billing Request Flow。
- 确认 Billing Request 的状态已变为
fulfilled。 - 从 Billing Request 中读取新建的 mandate ID。
- 调用 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 创建失败和客户中途退出等情况。
备注:内容仅供参考。
