オンデマンドサブスクリプションを使用すると、顧客の支払い方法を一度認証し、その後必要に応じて変動額を請求できます。固定スケジュールではなく、必要なときに請求することができます。この機能はすべてのアカウントで利用可能で、承認は不要です。
このガイドを使用して:
- オンデマンドサブスクリプションを作成する(オプションの初期価格でのマンダテを認証)
- カスタム金額での後続の請求をトリガーする
- 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)を介して再試行を区別します。
推奨再試行スケジュール(サブスクリプション)
- 1回目の試行:請求を作成したときに即時
- 2回目の試行:3日後
- 3回目の試行:さらに7日後(合計10日)
- 4回目の試行(最終):さらに7日後(合計17日)
最終ステップ:未払いの場合は、サブスクリプションを未払いとしてマークするか、ポリシーに基づいてキャンセルします。顧客に支払い方法を更新するためのウィンドウ内で通知します。
バースト再試行を避け、承認時間に合わせる
- 再試行を元の承認タイムスタンプに固定して、ポートフォリオ全体で「バースト」動作を避けます。
- 例:顧客が今日の午後1時10分にトライアルまたはマンダテを開始した場合、バックオフに従って、次の日の午後1時10分にフォローアップの再試行をスケジュールします(例:+3日→午後1時10分、+7日→午後1時10分)。
- あるいは、最後の成功した支払い時間を保存している場合
T、次の試行をT + X daysでスケジュールして、時間帯の整合性を保ちます。
タイムゾーンとDST:スケジュールのために一貫した時間基準を使用し、表示のためにのみ変換して間隔を維持します。
再試行しないべき拒否コード
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とシグネチャシークレットの設定を確認してください。