按需订阅允许您一次授权客户的支付方式,然后在需要时收取可变金额,而不是按照固定的时间表进行收费。此功能适用于所有账户,无需批准。
使用本指南:
- 创建按需订阅(授权一个可选的初始价格的授权)
- 使用自定义金额触发后续收费
- 使用 Webhook 跟踪结果
有关一般订阅设置,请参见 订阅集成指南。
先决条件
- Dodo Payments 商户账户和 API 密钥
- 配置的 Webhook 密钥和接收事件的端点
- 您目录中的订阅产品
如果您希望客户通过托管结账批准授权,请设置 payment_link: true 并提供一个 return_url。
按需工作原理
- 您使用
on_demand 对象创建一个订阅,以授权支付方式并可选地收取初始费用。
- 之后,您使用专用收费端点以自定义金额对该订阅进行收费。
- 您监听 Webhook(例如,
payment.succeeded,payment.failed)以更新您的系统。
创建按需订阅
端点:POST /checkouts
请求的关键字段(主体):
请在 创建结账会话 中找到它们。
创建按需订阅
Node.js SDK
Python SDK
Go SDK
cURL
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode', // defaults to 'live_mode'
});
async function main() {
const subscription = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_123', quantity: 1 }],
billing_address: { city: 'SF', country: 'US', state: 'CA', street: '1 Market St', zipcode: '94105' },
customer: { customer_id: 'cus_123' },
return_url: 'https://example.com/billing/success',
subscription_data: {
on_demand: {
mandate_only: true // set false to collect an initial charge
// product_price: 1000, // optional: charge $10.00 now if mandate_only is false
// product_currency: 'USD',
// product_description: 'Custom initial charge',
// adaptive_currency_fees_inclusive: false,
}
}
});
console.log(subscription.checkout_url);
}
main().catch(console.error);
import os
from dodopayments import DodoPayments
client = DodoPayments(
bearer_token=os.environ.get('DODO_PAYMENTS_API_KEY'),
environment="test_mode", # defaults to "live_mode"
)
checkout = client.checkout_sessions.create(
product_cart=[
{"product_id": "pdt_123", "quantity": 1}
],
billing_address={
"city": "SF",
"country": "US",
"state": "CA",
"street": "1 Market St",
"zipcode": "94105",
},
customer={
"customer_id": "cus_123",
},
return_url="https://example.com/billing/success",
subscription_data={
"on_demand":{
"mandate_only": True, # set False to collect an initial charge
# "product_price": 1000, # optional: charge $10.00 now if mandate_only is false
# "product_currency": "USD",
# "product_description": "Custom initial charge",
# "adaptive_currency_fees_inclusive": False,
}
},
)
print(checkout.checkout_url)
package main
import (
"fmt"
"os"
dodo "github.com/dodopayments/dodopayments-go"
)
func main() {
client := dodo.NewClient(dodo.Config{
BearerToken: os.Getenv("DODO_PAYMENTS_API_KEY"),
Environment: dodo.TestMode, // defaults to LiveMode
})
checkout, err := client.CheckoutSessions.Create(dodo.CheckoutSessionCreateParams{
ProductCart: []dodo.ProductCartItem{
{
ProductID: "pdt_123",
Quantity: 1,
},
},
BillingAddress: &dodo.BillingAddress{
City: "SF",
Country: "US",
State: "CA",
Street: "1 Market St",
Zipcode: "94105",
},
Customer: &dodo.CustomerRef{
CustomerID: "cus_123",
},
ReturnURL: "https://example.com/billing/success",
SubscriptionData: &dodo.SubscriptionData{
OnDemand: &dodo.OnDemandSubscription{
MandateOnly: true, // set false to collect an initial charge
// ProductPrice: 1000, // optional: charge $10.00 now if mandate_only is false
// ProductCurrency: "USD",
// ProductDescription: "Custom initial charge",
// AdaptiveCurrencyFeesInclusive: false,
},
},
})
if err != nil {
panic(err)
}
fmt.Println(checkout.CheckoutURL)
}
curl -X POST "$DODO_API/checkouts" \
-H "Authorization: Bearer $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_cart": [
{
"product_id": "pdt_123",
"quantity": 1
}
],
"customer": {
"customer_id": "cus_123"
},
"billing_address": {
"street": "1 Market St",
"city": "SF",
"state": "CA",
"country": "US",
"zipcode": "94105"
},
"subscription_data": {
"on_demand": {
"mandate_only": true
}
},
"return_url": "https://example.com/billing/success"
}'
{
"session_id": "cks_123",
"checkout_url": "https://test.checkout.dodopayments.com/session/cks123"
}
收取按需订阅费用
在授权后,根据需要创建收费。
端点:POST /subscriptions/{subscription_id}/charge
请求的关键字段(主体):
收费金额(以最小货币单位计)。例如:要收取 $25.00,请传递 2500。
adaptive_currency_fees_inclusive
如果为 true,则在 product_price 中包含自适应货币费用。如果为 false,则费用会加在上面。
Node.js SDK
Python SDK
Go SDK
cURL
import DodoPayments from 'dodopayments';
const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY });
async function chargeNow(subscriptionId) {
const res = await client.subscriptions.charge(subscriptionId, { product_price: 2500 });
console.log(res.payment_id);
}
chargeNow('sub_123').catch(console.error);
import os
from dodopayments import DodoPayments
client = DodoPayments(bearer_token=os.environ.get('DODO_PAYMENTS_API_KEY'))
response = client.subscriptions.charge(
subscription_id="sub_123",
product_price=2500,
)
print(response.payment_id)
package main
import (
"context"
"fmt"
"github.com/dodopayments/dodopayments-go"
"github.com/dodopayments/dodopayments-go/option"
)
func main() {
client := dodopayments.NewClient(option.WithBearerToken("YOUR_API_KEY"))
res, err := client.Subscriptions.Charge(context.TODO(), "sub_123", dodopayments.SubscriptionChargeParams{
ProductPrice: dodopayments.F(int64(2500)),
})
if err != nil { panic(err) }
fmt.Println(res.PaymentID)
}
curl -X POST "$DODO_API/subscriptions/sub_123/charge" \
-H "Authorization: Bearer $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_price": 2500,
"product_description": "Extra usage for March"
}'
{ "payment_id": "pay_abc123" }
对非按需订阅进行收费可能会失败。确保在收费之前,订阅的详细信息中包含 on_demand: true。
付款重试
我们的欺诈检测系统可能会阻止激进的重试模式(并可能将其标记为潜在的卡片测试)。遵循安全的重试策略。
突发重试模式可能会被我们的风险系统和处理器标记为欺诈或可疑的卡片测试。避免集中的重试;遵循下面的退避计划和时间对齐指导。
安全重试策略的原则
- 退避机制:在重试之间使用指数退避。
- 重试限制:限制总重试次数(最多 3-4 次尝试)。
- 智能过滤:仅在可重试的失败(例如,网络/发卡行错误、资金不足)时重试;绝不要重试硬拒绝。
- 防止卡片测试:不要重试诸如
DO_NOT_HONOR,STOLEN_CARD,LOST_CARD,PICKUP_CARD,FRAUDULENT,AUTHENTICATION_FAILURE。
- 可选的元数据变化:如果您维护自己的重试系统,通过元数据区分重试(例如,
retry_attempt)。
建议的重试计划(订阅)
- 第一次尝试:创建收费时立即进行
- 第二次尝试:3 天后
- 第三次尝试:再过 7 天(总共 10 天)
- 第四次尝试(最终):再过 7 天(总共 17 天)
最后一步:如果仍未付款,请根据您的政策将订阅标记为未付款或取消它。在更新其支付方式的窗口期间通知客户。
避免突发重试;与授权时间对齐
- 将重试锚定到原始授权时间戳,以避免在您的组合中出现“突发”行为。
- 示例:如果客户在今天下午 1:10 开始试用或授权,请根据您的退避计划在后续天数的下午 1:10 安排后续重试(例如,+3 天 → 下午 1:10,+7 天 → 下午 1:10)。
- 或者,如果您存储最后一次成功付款时间
T,请在 T + X days 安排下一次尝试,以保持时间对齐。
时区和夏令时:使用一致的时间标准进行调度,仅在显示时进行转换以保持间隔。
不应重试的拒绝代码
STOLEN_CARD
DO_NOT_HONOR
FRAUDULENT
PICKUP_CARD
AUTHENTICATION_FAILURE
LOST_CARD
有关拒绝原因的完整列表及其是否可由用户更正的信息,请参见
交易失败 文档。
仅在软/临时问题(例如,insufficient_funds,issuer_unavailable,processing_error,网络超时)时重试。如果同一拒绝重复,请暂停进一步重试。
实施指南(无代码)
- 使用持久化精确时间戳的调度程序/队列;在确切的时间偏移(例如,
T + 3 days 在同一 HH:MM)计算下一次尝试。
- 维护并引用最后一次成功付款时间戳
T 以计算下一次尝试;不要在同一时刻聚集多个订阅。
- 始终评估最后一次拒绝原因;对于上述跳过列表中的硬拒绝停止重试。
- 限制每个客户和每个账户的并发重试,以防止意外激增。
- 主动沟通:在下次计划尝试之前,通过电子邮件/SMS 通知客户更新其支付方式。
- 仅将元数据用于可观察性(例如,
retry_attempt);绝不要试图通过轮换无关字段来“规避”欺诈/风险系统。
使用 Webhook 跟踪结果
实现 Webhook 处理以跟踪客户旅程。请参见 实现 Webhook。
- subscription.active:授权的授权和激活的订阅
- subscription.failed:创建失败(例如,授权失败)
- subscription.on_hold:订阅被搁置(例如,未付款状态)
- payment.succeeded:收费成功
- payment.failed:收费失败
对于按需流程,关注 payment.succeeded 和 payment.failed 以对账基于使用的收费。
测试和后续步骤
在测试模式下创建
使用您的测试 API 密钥创建带有 payment_link: true 的订阅,然后打开链接并完成授权。
触发收费
使用小的 product_price(例如,100)调用收费端点,并验证您收到 payment.succeeded。
上线
在验证事件和内部状态更新后,切换到您的实时 API 密钥。
故障排除
- 422 无效请求:确保在创建时提供了
on_demand.mandate_only,并在收费时提供了 product_price。
- 货币错误:如果您覆盖了
product_currency,请确认它对您的账户和客户是支持的。
- 未收到 Webhook:验证您的 Webhook URL 和签名密钥配置。