discounts-and-promotions について
Discounts and Promotionsは、Dodo Paymentsを使う開発者向けに、割引コードの管理、チェックアウトへの適用、適用条件の確認、サブスクリプションのプラン変更を扱うスキルです。割引方式、制限、併用順序、プラン変更時の割引の引き継ぎ方を説明します。
主な機能
- 01割引コードの作成、取得、更新、削除
- 02割引方式、適用条件、利用上限の設定
- 03チェックアウトでのコード適用と併用
- 04日付やサブスクリプション周期による制限
- 05プラン変更時の割引引き継ぎ管理
活用シーン
- 01割引コード管理フローの構築
- 02チェックアウト前の適用条件確認と価格表示
- 03商品や顧客に応じたプロモーション制限
- 04プラン変更時の割引の維持、置換、削除
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dodopayments/ skills --skill discounts-and-promotionsインストール後、Claude Code で/discounts-and-promotionsを入力して読み込まれたか確認します。
詳細
割引とプロモーション
このスキルでは、Dodo Payments の割引コード、クーポン、プロモーション価格を扱います。割引管理の構築、チェックアウト時のコードの適用、価格を表示する前のコードの検証、サブスクリプションプラン変更時の割引処理を行う場合に使用してください。
このスキルを使用するタイミング
- 割引コードを作成、更新、一覧表示、削除する
- チェックアウト時またはプラン変更時に割引コードを適用する
- 調整後の価格をユーザーに表示する前にコードを検証する
- 適用条件、使用回数の上限、サブスクリプションサイクルの制限を設定する
- サブスクリプションのアップグレード/ダウングレード時に割引を引き継ぐ際の微妙な仕様を扱う
- 割引が想定どおりに適用または併用されない理由をデバッグする
基本概念
割引の種類: Dodo は、パーセンテージ割引と定額割引の両方に対応しています。パーセンテージは整数で表し、1500 は 15% を意味します(100 = 1%)。定額割引は通貨の最小単位(USD の場合はセント)で表します。
割引コード: 顧客がチェックアウト時に入力する、人が読める文字列(例:SUMMER2025)です。API は作成時にコードを大文字に変換します(summer2025 は SUMMER2025 として保存されます)。コードは 3 文字以上である必要があり、code を省略すると、ランダムな 16 文字の大文字コードが生成されます。
適用条件: 割引の適用対象を特定の商品、顧客、日付範囲に限定できます。制限のない割引は、すべての商品と顧客に適用されます。
併用: 1 回のチェックアウトまたはサブスクリプションに複数の割引コードを適用できます。discount_codes 配列で指定された順に適用され、最大 20 個のコードを指定できます。
サブスクリプションサイクルの制限: 特定の請求サイクル数の間だけ適用されるように割引を設定できます(例:最初の 3 か月のみ)。
プラン変更時の引き継ぎ: 顧客がサブスクリプションをアップグレードまたはダウングレードする際、既存の割引を引き継ぐかどうかは、preserve_on_plan_change 設定と discount_codes パラメーターによって決まります。
割引の CRUD
割引を作成する
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
// Percentage discount: 15% off
const percentageDiscount = await client.discounts.create({
type: 'percentage',
amount: 1500, // 15%
code: 'SUMMER2025',
metadata: {
campaign: 'summer_promo',
source: 'email_blast'
}
});
// Flat discount: $10 off (1000 cents in currency_options)
const flatDiscount = await client.discounts.create({
type: 'flat',
amount: 1000,
currency_options: [{
currency: 'USD',
max_amount_possible: 1000,
is_default: true,
}],
code: 'WELCOME10',
});
割引を一覧表示する
const discounts = await client.discounts.list({
page_size: 50,
page_number: 0,
});
// Paginated responses expose `items`.
for (const discount of discounts.items) {
console.log(`${discount.code}: ${discount.type} ${discount.amount}`);
}
// Or let the SDK walk every page for you:
for await (const discount of client.discounts.list()) {
console.log(discount.code);
}
ID で割引を取得する
const discount = await client.discounts.retrieve('discount_id');
console.log(discount.code, discount.type, discount.amount);
コードで割引を取得する
try {
const discount = await client.discounts.retrieveByCode('SUMMER2025');
console.log('Discount found:', discount.amount);
} catch (error) {
console.log('Code not found or expired');
}
割引を更新する
await client.discounts.update('discount_id', {
metadata: { updated_at: new Date().toISOString() }
});
割引を削除する
await client.discounts.delete('discount_id');
割引の種類と設定
対応している種類は percentage と flat です(ダッシュボードでは 金額 と表示されます)。flat 割引では、解決可能なデフォルト通貨を少なくとも 1 つ含む currency_options が必要です。percentage 割引では、currency_options は省略可能で、通貨ごとの割引上限を設定できます。定額の割引額は、各明細項目ではなくカート全体から差し引かれます。
パーセンテージ割引
100 = 1% の整数で表します。15% の割引は 1500 です。
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500, // 15%
code: 'PERCENT15',
});
定額割引
定額割引では、currency_options を使って通貨ごとの割引額を設定します。USD で 10 ドルを割り引く場合は、max_amount_possible: 1000(セント)とします。
const discount = await client.discounts.create({
type: 'flat',
amount: 1000, // $10.00
currency_options: [{
currency: 'USD',
max_amount_possible: 1000,
is_default: true,
}],
code: 'FLAT10',
});
適用条件と制限
商品の制限
割引の適用対象を特定の商品に限定します。
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500,
code: 'PREMIUM_ONLY',
restricted_to: ['pdt_premium', 'pdt_enterprise'],
});
顧客の適用条件
customer_eligibility: 'specific' を指定して割引を作成し、割引と顧客を紐付けるエンドポイントを通じて顧客を追加することで、特定の顧客に割引を限定できます。customer_ids は discounts.create のパラメーターではありません。
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500,
code: 'VIP_ONLY',
customer_eligibility: 'specific',
});
// The SDK has no named method for this endpoint yet. `client.post` reuses the
// client's auth and base URL (test.dodopayments.com for `test_mode`,
// live.dodopayments.com for `live_mode`), so nothing is hardcoded.
await client.post(`/discounts/${discount.discount_id}/customers`, {
body: { customer_ids: ['cus_vip_001', 'cus_vip_002'] },
});
日付範囲
開始日と有効期限を設定します。
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500,
code: 'SUMMER2025',
starts_at: '2025-06-01T00:00:00Z',
expires_at: '2025-08-31T23:59:59Z',
});
使用回数の上限
コードの合計使用回数を制限します。
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500,
code: 'LIMITED_100',
usage_limit: 100,
});
サブスクリプションサイクルの制限
最初の N 回の請求サイクルに限って割引を適用します。
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500,
code: 'FIRST_3_MONTHS',
subscription_cycles: 3, // applies to first 3 billing cycles only
});
チェックアウト時にコードを適用する
単一のコード
ブラウザーから SDK を呼び出さないでください。SDK はシークレット API キーを使用します。サーバールートでセッションを作成し、URL のみを返します。
// Server route
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
discount_codes: ['SUMMER2025'],
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/return'
});
return Response.json({ checkout_url: session.checkout_url });
// Browser
const res = await fetch('/api/checkout', { method: 'POST' });
const { checkout_url } = await res.json();
window.location.href = checkout_url;
複数のコード(併用)
コードは指定された順に適用されます。チェックアウトごとに最大 20 個のコードを指定できます。
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
discount_codes: ['WELCOME10', 'BLACKFRIDAY20', 'LOYALTY5'],
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/return'
});
チェックアウト前にコードを検証する
調整後の価格を表示する前に、コードを検証して適用条件を確認します。
async function validateDiscount(code, productId) {
try {
const discount = await client.discounts.retrieveByCode(code);
// Check expiration
if (discount.expires_at && new Date(discount.expires_at) < new Date()) {
return { valid: false, reason: 'Code expired' };
}
// Check usage limit
if (discount.usage_limit && discount.times_used >= discount.usage_limit) {
return { valid: false, reason: 'Code exhausted' };
}
// Check product eligibility
if (discount.restricted_to.length > 0) {
if (!discount.restricted_to.includes(productId)) {
return { valid: false, reason: 'Code not valid for this product' };
}
}
return { valid: true, discount };
} catch (error) {
return { valid: false, reason: 'Code not found or unavailable' };
}
}
// Usage
const result = await validateDiscount('SUMMER2025', 'pdt_abc');
if (result.valid) {
console.log('Discount applies:', result.discount.amount);
} else {
console.log('Cannot apply:', result.reason);
}
プラン変更時の割引
顧客がサブスクリプションプランを変更すると、discount_codes パラメーターと各割引の preserve_on_plan_change 設定に応じて、割引を維持、置き換え、または解除できます。preserve_on_plan_change は discounts.create または discounts.update で設定します。subscriptions.changePlan のパラメーターではありません。
動作の一覧
| シナリオ | discount_codes | 割引の preserve_on_plan_change | 結果 |
|---|---|---|---|
| 変更なしでアップグレード | undefined | true | 既存の割引が引き継がれます |
| 変更なしでアップグレード | undefined | false | 既存の割引が解除されます |
| 変更なしでアップグレード | [](空の配列) | 任意 | 既存の割引が解除されます |
| 割引を置き換えてアップグレード | ['NEW_CODE'] | 任意 | NEW_CODE のみが適用され、古い割引は解除されます |
例:既存の割引を維持する
await client.discounts.update('dsc_existing', {
preserve_on_plan_change: true,
});
await client.subscriptions.changePlan('sub_123', {
product_id: 'pdt_pro',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
// Omit discount_codes. Existing discounts configured with
// preserve_on_plan_change: true carry forward.
});
例:アップグレード時に割引を置き換える
await client.subscriptions.changePlan('sub_123', {
product_id: 'pdt_pro',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
discount_codes: ['UPGRADE20'], // replaces old discounts
});
例:ダウングレード時にすべての割引を解除する
await client.subscriptions.changePlan('sub_123', {
product_id: 'pdt_basic',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
discount_codes: [], // empty array removes all discounts
});
よくある間違い
クライアント側だけで割引を検証する
クライアント側の検証を決して信用しないでください。チェックアウト時に割引を適用する前に、必ずサーバー側で検証してください。ユーザーはブラウザー上で割引コードを変更できます。
// WRONG: client-side only
if (code.length > 0) {
applyDiscount(code);
}
// CORRECT: server-side validation
try {
const discount = await client.discounts.retrieveByCode(code);
if (isEligible(discount)) {
// proceed with checkout
}
} catch (error) {
// Unknown or unavailable code; reject the checkout request.
}
コードが無条件に併用できると思い込む
割引は順番に適用されますが、適用条件は引き続き有効です。コードの対象商品がカートに含まれていない場合、ほかのコードが適用されても、そのコードは適用されません。
// Both codes may not apply if they have conflicting restrictions
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_basic', quantity: 1 }],
discount_codes: ['PREMIUM_ONLY', 'BASIC_ONLY'], // one or both may fail
return_url: 'https://yoursite.com/return'
});
空の配列を渡してアップグレード時に割引を失う
discount_codes: [] を明示的に渡すと、preserve_on_plan_change が true でも、すべての割引が解除されます。既存の割引を維持するには、このパラメーターを省略してください。
// WRONG: removes all discounts
await client.subscriptions.changePlan('sub_123', {
product_id: 'pdt_pro',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
discount_codes: [], // this clears discounts
});
// CORRECT: preserves discounts whose preserve_on_plan_change property is true
await client.subscriptions.changePlan('sub_123', {
product_id: 'pdt_pro',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
// omit discount_codes
});
期限切れまたは上限に達したコードを考慮しない
コードは、有効期限を過ぎるか、利用上限に達すると期限切れになります。ユーザーに割引を表示する前に、必ず両方を確認してください。
// WRONG: assumes code is always valid
const discount = await client.discounts.retrieveByCode(code);
applyDiscount(discount);
// CORRECT: catch unknown codes, then check expiration and usage
try {
const discount = await client.discounts.retrieveByCode(code);
if (discount.expires_at && new Date(discount.expires_at) < new Date()) {
showError('Code expired');
} else if (discount.usage_limit && discount.times_used >= discount.usage_limit) {
showError('Code exhausted');
} else {
applyDiscount(discount);
}
} catch (error) {
showError('Code not found');
}
サブスクリプションのサイクル上限と有効期限を混同する
subscription_cycles はサブスクリプションにのみ適用され、割引が適用される請求サイクル数を制御します。expires_at は、そのコードをあらゆる用途で使用できる期限を定めます。
// Applies to first 3 billing cycles of any subscription
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500,
code: 'FIRST_3_MONTHS',
subscription_cycles: 3,
});
// Expires on a specific date, regardless of billing cycles
const discount2 = await client.discounts.create({
type: 'percentage',
amount: 1500,
code: 'SUMMER_ONLY',
expires_at: '2025-08-31T23:59:59Z',
});