オンデマンドサブスクリプションを使用すると、顧客の支払い方法を一度認証し、その後必要に応じて変動額を請求できます。固定スケジュールではなく、必要なときに請求することができます。この機能はすべてのアカウントで利用可能で、承認は不要です。
このガイドを使用して:
- オンデマンドサブスクリプションを作成する(オプションの初期価格でのマンダテを認証)
- カスタム金額での後続の請求をトリガーする
- Webhookを使用して結果を追跡する
一般的なサブスクリプションの設定については、サブスクリプション統合ガイドを参照してください。
前提条件
- Dodo PaymentsのマーチャントアカウントとAPIキー
- Webhookシークレットが設定されており、イベントを受信するエンドポイントがあること
- カタログにサブスクリプション商品があること
ホスト型チェックアウトで顧客にマンダテ(mandate)を承認してもらうには、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
主要なリクエストフィールド(ボディ):
Charge request body parameters
請求金額(通貨の最小単位)。例:$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 などのメタデータで再試行を識別してください。
推奨再試行スケジュール(サブスクリプション)
- 1回目の試行:請求を作成したときに即時
- 2回目の試行:3日後
- 3回目の試行:さらに7日後(合計10日)
- 4回目の試行(最終):さらに7日後(合計17日)
最終ステップ:未払いの場合は、サブスクリプションを未払いとしてマークするか、ポリシーに基づいてキャンセルします。顧客に支払い方法を更新するためのウィンドウ内で通知します。
バースト再試行を避け、承認時間に合わせる
- 再試行を元の認可タイムスタンプにアンカーし、ポートフォリオ全体での「バースト」挙動を避けます。
- 例:1:10 pm にトライアルやマンダテを開始した場合、バックオフに従って翌日以降も 1:10 pm に再試行(例:+3日→1:10 pm、+7日→1:10 pm)をスケジューリングします。
- または、最後に成功した支払い時刻
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 に注力してください。
テストと次のステップ
Create in test mode
テストAPIキーを使用して payment_link: true でサブスクリプションを作成し、リンクを開いてマンダテを完了します。
Trigger a charge
小額の product_price(例:100)でチャージエンドポイントを呼び出し、payment.succeeded を受け取ることを確認します。
Go live
イベントと内部状態の更新を検証したら、ライブAPIキーに切り替えます。
トラブルシューティング
- 422 Invalid Request:作成時に
on_demand.mandate_only、チャージ時に product_price が提供されていることを確認してください。
- 通貨エラー:
product_currency をオーバーライドする場合、その通貨がアカウントおよび顧客に対してサポートされていることを確認してください。
- Webhook を受信しない:Webhook URL と署名シークレットの設定を確認してください。