subscription-integration について
Dodo Paymentsで継続課金を実装する開発スキルです。サブスクリプションの作成と管理、プラン変更、決済回復、都度課金、顧客ポータルを扱います。
主な機能
- 01Checkout Sessionsでの作成
- 02ステータスと解約の管理
- 03プラン変更と日割り計算
- 04都度課金の実行
- 05失敗した決済の回復
- 06顧客セルフサービスポータルの設定
活用シーン
- 01トライアル付きサブスクリプションの構築
- 02プランのアップグレードやダウングレード
- 03従量課金顧客への都度請求
- 04顧客自身による決済方法の更新や解約
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dodopayments/ skills --skill subscription-integrationインストール後、Claude Code で/subscription-integrationを入力して読み込まれたか確認します。
同梱ファイルreferences/webhook-events.md
詳細
Dodo Payments サブスクリプション連携
トライアル、プラン変更、オンデマンド課金に対応した継続課金を実装します。サブスクリプションはCheckout Sessionsを通じて作成し、subscriptions APIで管理し、webhookで監視します。
このスキルを使用する場面
- 継続課金とトライアルに対応したサブスクリプション製品を構築する
- サイクル途中のプランのアップグレード、ダウングレード、移行を処理する
- マンデートを使ったオンデマンド/オフセッション課金を実装する
- 失敗した支払いと督促による回収を管理する
- サブスクリプション管理用の顧客セルフサービスポータルを構築する
主な概念
サブスクリプションのライフサイクル: サブスクリプションのステータスはpending、active、past_due、on_hold、paused、cancelled、failed、expiredの8種類です。トライアル中のサブスクリプションはactiveを返します。subscription.renewedはイベントであり、ステータスではありません。更新に失敗すると、猶予期間を設定している場合はpast_due(アクセスは維持されます)、設定していない場合はon_hold(アクセスは取り消されます)に移行します。どちらも回復可能です。pausedは、あなたまたは顧客が意図的に一時停止した状態です。failedは終端状態で、初回作成に失敗した場合にのみ発生します。解約するとcancelledになり、または期間終了時にexpiredになるよう設定されます。
Checkout Sessions: サブスクリプション作成に推奨される方法です。支払いと顧客情報を収集する一度限りのホスト型チェックアウトで、その後、サブスクリプションをサーバー側で作成します。
日割り計算: 顧客がサイクル途中でプランを変更すると、Dodoは残り期間に基づいてクレジットまたは請求額を計算します。日割り計算モードによって、顧客に即時請求するか、クレジットを付与するか、どちらも行わないかが決まります。
マンデート: 顧客の支払い方法に繰り返し課金する(サブスクリプションの場合)、またはオンデマンドで課金する(使用量ベースの課金の場合)ための承認です。Checkout中に作成され、支払いに失敗した場合は更新できます。
サブスクリプションの作成
Checkout Session経由(推奨)
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_monthly_plan', quantity: 1 }
],
subscription_data: {
trial_period_days: 14, // Optional
},
customer: {
email: '[email protected]',
name: 'Jane Doe',
},
return_url: 'https://yoursite.com/success',
});
// Redirect user to session.checkout_url
console.log('Redirect to:', session.checkout_url);
顧客はホスト型チェックアウトで支払いを完了します。成功すると、Dodoがサブスクリプションを作成し、subscription.active webhookを送信します。
アドオンを追加する場合
const session = await client.checkoutSessions.create({
product_cart: [
{
product_id: 'pdt_pro_monthly',
quantity: 1,
addons: [
{ addon_id: 'adn_extra_seats', quantity: 3 }
]
}
],
subscription_data: {
trial_period_days: 7,
},
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
サブスクリプションのライフサイクルとステータス
| ステータス | 意味 | 遷移先 |
|---|---|---|
pending | 作成処理中 | → active、failed |
active | 更新中 | → past_due、on_hold、paused、cancelled、expired |
past_due | 更新に失敗し、猶予期間中です。顧客はpast_due_ends_atまでアクセスを維持します | → active(未払い分を清算)、on_holdまたはcancelled(設定に応じて猶予期間終了時) |
on_hold | 更新またはプラン変更の支払いに失敗し、更新は停止、アクセスは取り消されています。回復可能です | → active(支払い方法を更新/再試行が成功)、cancelled |
paused | あなたまたは顧客が意図的に一時停止した状態です。課金は凍結され、アクセスは取り消されます | → active(再開)、cancelled |
cancelled | 更新されません | → expired(期間終了時) |
failed | 作成時の初回マンデート/支払いに失敗した状態です。終端状態であり、on_holdや再試行の上限到達から移行することはありません | (回復不可) |
expired | サブスクリプション期間が終了しました | (終端状態) |
past_dueは猶予期間を有効にしている場合にのみ発生します。猶予期間を設定していない場合、更新の失敗後は直接on_holdに移行します。
トライアル期間: trial_period_daysを設定すると、サブスクリプションはすぐにactiveになりますが、トライアル終了までは請求されません。初回請求はトライアル終了日に発生します。
サブスクリプションのメソッド
取得
const subscription = await client.subscriptions.retrieve('sub_xxxxx');
console.log(subscription.status, subscription.next_billing_date);
一時停止と再開
一時停止と再開は、更新エンドポイントのstatusを通じて実行します。単独で送信してください。paused/activeを他のフィールドと組み合わせると、422で拒否されます。
// Pause: renewals stop, access is revoked, next_billing_date shifts by the pause length
await client.subscriptions.update('sub_xxxxx', { status: 'paused' });
// Resume
await client.subscriptions.update('sub_xxxxx', { status: 'active' });
一時停止するとsubscription.paused、再開するとsubscription.unpausedが送信されます。ポータルで顧客がセルフサービスで一時停止できるようにするには、オプトインが必要です(Settings → Subscriptions → Allow Subscription Pause)。
支払い方法の更新
支払いに失敗してサブスクリプションが on_hold になった場合、顧客は支払い方法を更新できます。これにより、未払い分が自動的に請求されます。
await client.subscriptions.updatePaymentMethod('sub_xxxxx', {
payment_method: {
type: 'existing',
payment_method_id: 'pm_new_method',
},
});
成功すると、payment.succeeded に続いて subscription.active が発行されます。
使用履歴の一覧表示(従量課金制サブスクリプション)
const history = await client.subscriptions.retrieveUsageHistory('sub_xxxxx', {
page_size: 50,
page_number: 0,
});
クレジット使用量の取得
const creditUsage = await client.subscriptions.retrieveCreditUsage('sub_xxxxx');
console.log('Subscription:', creditUsage.subscription_id);
for (const item of creditUsage.items) {
console.log(item.credit_entitlement_name, item.balance); // balance is a string
}
プランの変更(アップグレードとダウングレード)
プランの変更
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_higher_tier',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
on_payment_failure: 'prevent_change',
});
日割り計算モード:
| モード | アップグレード | ダウングレード | 請求日 |
|---|---|---|---|
prorated_immediately | 旧プランの未使用期間分をクレジットとして差し引き、新しいサイクルの料金を全額請求します(差引額 = 新しいサイクルの全額 − 未使用分のクレジット) | 同じ計算式です。未使用分のクレジットが新しいプランの料金を上回る場合にのみ、クレジットが残ります | 変更日にリセットされます |
difference_immediately | 新しいプランの料金を全額請求します | 差額がクレジットになります | リセットされます |
full_immediately | 新しいプランの料金を全額請求します | 新しいプランの料金を全額請求します。クレジットは発生しません | リセットされます |
do_not_bill | 請求なし | クレジットなし | 維持されます |
支払い失敗時の処理:
prevent_change:請求に失敗した場合、旧プランを維持します。apply_change:支払いに失敗しても新しいプランを適用します(サブスクリプションがon_holdになる場合があります)。
スケジュール設定: effective_at のデフォルトは 'immediately' です。変更をスケジュールするには、effective_at: 'next_billing_date' を指定します(ダウングレードで一般的です)。この場合、現在は請求されず、顧客は期間終了まで現在のプランを利用できます。また、保留中の変更はサブスクリプションの scheduled_change に表示されます(cancelChangePlan でキャンセルできます)。
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_lower_tier',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
effective_at: 'next_billing_date',
});
サブスクリプションが past_due の間は、プランの変更は拒否されます。 先に更新時の未払い分を清算してください。
プラン変更のプレビュー
確定前に顧客に見積もりを提示します。
const preview = await client.subscriptions.previewChangePlan('sub_xxxxx', {
product_id: 'pdt_new_plan',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
});
console.log('Effective at:', preview.immediate_charge.effective_at);
console.log('Line items:', preview.immediate_charge.line_items);
console.log('Summary:', preview.immediate_charge.summary);
console.log('New plan:', preview.new_plan);
スケジュールされたプラン変更のキャンセル
await client.subscriptions.cancelChangePlan('sub_xxxxx');
キャンセル
即時キャンセル
await client.subscriptions.update('sub_xxxxx', {
status: 'cancelled',
});
アクセス権は直ちに取り消されます。
期間終了時にキャンセル
await client.subscriptions.update('sub_xxxxx', {
cancel_at_next_billing_date: true,
});
サブスクリプションは次回請求日まで active のまま維持され、その後 expired に移行します。subscription.cancelled Webhook には cancel_at_next_billing_date: true と next_billing_date が含まれるため、いつアクセス権を取り消すかがわかります。
オンデマンド(セッション外)請求
従量課金制または使用量に応じたサブスクリプションでは、予定された更新を設定せずに顧客へオンデマンドで請求できます。
マンデート付きサブスクリプションの作成
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_usage_based', quantity: 1 }
],
subscription_data: {
on_demand: {
mandate_only: true,
}
},
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
これにより、マンデート付きで自動更新のないサブスクリプションが作成されます。請求のタイミングは任意に決められます。
オンデマンド請求
const charge = await client.subscriptions.charge('sub_xxxxx', {
product_price: 2500, // $25.00 in cents
product_currency: 'USD',
product_description: 'API calls for January 2025',
});
console.log('Payment ID:', charge.payment_id);
金額の単位は通貨の最小単位です(USD はセント、INR はパイサなど)。
Dodo はオンデマンド請求を自動で再試行しません。 再試行のロジックと拒否のフィルタリングは、利用者側で実装してください。payment.failed Webhook を監視し、独自の再試行戦略を実装してください。
支払い失敗と復旧
保留中のサブスクリプション
更新時またはプラン変更時の請求に失敗すると、サブスクリプションは on_hold に移行します。これは復旧可能な状態です。
// Listen for subscription.on_hold webhook
case 'subscription.on_hold':
// Notify customer, offer payment method update
await sendPaymentFailedEmail(data.customer.customer_id);
break;
復旧方法
-
支払いの再試行: 「設定」→「復旧」から有効にできます。Dodo は設定可能な 1~30 日間(デフォルトは 13 日間)に、失敗した更新時の請求を最大 8 回再試行します。ソフトディクラインの場合にのみ利用できます。
-
顧客ポータル: 顧客が支払い方法を更新すると、未払い分が自動的に請求され、
subscription.activeに移行します。 -
手動請求: サーバー側で支払い方法を更新してから、
updatePaymentMethod()を呼び出します。
督促
督促では、on_hold 中の更新や顧客ポータルからのキャンセルに対して、設定可能なメールを最大 4 通送信します。督促をすべて実行してもサブスクリプションの状態は変わらないため、最終的な結果は利用者側で処理する必要があります。
カスタマーポータル
顧客が自分で手続きできるようにします。サブスクリプションの確認、支払い方法の更新、解約、アップグレード/ダウングレードが可能です。
const portalSession = await client.customers.customerPortal.create(
'cus_xxxxx',
{ return_url: 'https://yoursite.com/account' }
);
// Redirect to portalSession.link
ポータルリンクの有効期限は24時間です。顧客は次の操作を行えます。
- サブスクリプションの詳細と更新日を確認する
- 直ちに、または期間終了時に解約する
- 有効化されている Product Collections 内でアップグレード/ダウングレードする
- 支払い方法を更新し、
on_hold状態のサブスクリプションを再開する - 請求履歴を確認し、請求書をダウンロードする
顧客の作成、更新、一覧表示を含む顧客管理全般については、customer-management スキルを参照してください。
Webhook イベント
完全なガイド:references/webhook-events.md。
以下を扱います。
- ハンドラーの例
クレジット特典
サブスクリプション商品にクレジット特典を付与すると、請求サイクルごとにクレジットを付与できます。クレジット管理全般については、credit-based-billing スキルを参照してください。
簡単な例:
// Product has credit entitlement attached (e.g., 10,000 tokens/month)
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_pro_with_credits', quantity: 1 }
],
subscription_data: {
trial_period_days: 14,
},
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
更新のたびにクレジットが付与されます。credit.added と credit.deducted の Webhook を監視し、台帳を同期してください。
よくある間違い
1. Webhook ではなく return_url でアクセス権を付与する
誤り:
// On return_url redirect
await grantAccess(user.id);
return_url が呼び出される時点では、サブスクリプションはまだ完全に作成されていません。Dodo が決済の承認または支払いを処理中の場合があります。必ず subscription.active Webhook を待ってください。
正しい方法:
// In webhook handler
case 'subscription.active':
await grantAccess(data.customer.customer_id);
break;
2. 非推奨の subscriptions.create を使用する
誤り:
const sub = await client.subscriptions.create({
product_id: 'pdt_monthly',
customer_id: 'cus_xxxxx',
});
このエンドポイントは非推奨です。Checkout Sessions を使用してください。
正しい方法:
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_monthly', quantity: 1 }],
customer: { customer_id: 'cus_xxxxx' },
return_url: 'https://yoursite.com/success',
});
3. subscription.plan_changed を誤って解釈する
subscription.plan_changed は、アップグレード、ダウングレード、アドオンの変更に加え、cancel_at_next_billing_date が切り替えられた場合にも発生します。すべての plan_changed イベントが有料アップグレードの成功を意味するとは限りません。ペイロードを調べ、サブスクリプションの現在の product_id を確認してください。
4. 日割り計算モードを指定し忘れる
誤り:
// Missing proration_billing_mode
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_new',
quantity: 1,
// proration_billing_mode: 'prorated_immediately', // REQUIRED
});
これは失敗します。必ず日割り計算モードを指定してください。
5. on_hold と paused を混同する
on_hold は支払い失敗による意図しない状態であり、支払い方法の更新または再試行の成功によって解消されます。paused は意図的な状態です。お客様または顧客が subscriptions.update(id, { status: 'paused' }) でサブスクリプションを一時停止し、{ status: 'active' } で再開します。subscription.paused/subscription.unpaused は subscription.on_hold とは分けて処理してください。
6. cancel_at_next_billing_date を処理しない
// Wrong: revoke immediately
await revokeAccess(data.customer.customer_id);
// Right: check the flag
if (data.cancel_at_next_billing_date) {
await scheduleAccessRevocation(data.subscription_id, new Date(data.next_billing_date));
} else {
await revokeAccessImmediately(data.subscription_id);
}