discounts-and-promotions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Discounts and Promotions

折扣与促销

This skill covers discount codes, coupons, and promotional pricing in Dodo Payments. Use it when building discount management, applying codes at checkout, validating codes before showing prices, or handling discounts during subscription plan changes.
本 skill 介绍 Dodo Payments 中的折扣码、优惠券和促销定价。在构建折扣管理、在结账时应用代码、在向用户显示调整后的价格之前验证代码,或在订阅套餐变更期间处理折扣时,请使用本 skill。

When to use this skill

何时使用此 skill

  • Create, update, list, or delete discount codes
  • Apply discount codes at checkout or during plan changes
  • Validate a code before displaying adjusted pricing to the user
  • Configure eligibility rules, usage limits, or subscription-cycle restrictions
  • Handle the subtle semantics of discount preservation during plan upgrades/downgrades
  • Debug why a discount isn't applying or stacking as expected
  • 创建、更新、列出或删除折扣码
  • 在结账或套餐变更时应用折扣码
  • 在向用户显示调整后的价格之前验证代码
  • 配置资格规则、使用次数限制或订阅周期限制
  • 处理套餐升级/降级过程中折扣保留的细微语义规则
  • 排查折扣为何未按预期应用或叠加

Core concepts

核心概念

Discount types: Dodo supports both percentage and flat-amount discounts. Percentage amounts are expressed as integers where 1500 means 15% (100 = 1%). Flat amounts are in the smallest currency unit (cents for USD).
Discount code: A human-readable string (e.g.,
SUMMER2025
) that customers enter at checkout. Codes are case-sensitive.
Eligibility: Discounts can be restricted to specific products, customers, or date ranges. A discount without restrictions applies to any product and any customer.
Stacking: Multiple discount codes can be applied to a single checkout or subscription. They are applied in the order specified in the
discount_codes
array, up to a maximum of 20 codes.
Subscription-cycle limits: A discount can be configured to apply only for a specific number of billing cycles (e.g., first 3 months only).
Preservation on plan change: When a customer upgrades or downgrades their subscription, the
preserve_on_plan_change
setting and the
discount_codes
parameter control whether existing discounts carry forward.
折扣类型: Dodo 同时支持百分比折扣和固定金额折扣。百分比金额以整数表示,其中
1500
表示 15%(
100
表示 1%)。固定金额使用货币的最小单位(USD 为美分)。
折扣码: 客户在结账时输入的易读字符串(例如
SUMMER2025
)。代码区分大小写。
资格限制: 折扣可以限制为仅适用于特定产品、客户或日期范围。没有限制的折扣适用于任何产品和任何客户。
叠加: 单次结账或订阅可以应用多个折扣码。它们按照
discount_codes
数组中指定的顺序应用,最多可应用 20 个代码。
订阅周期限制: 可以将折扣配置为仅在特定数量的计费周期内生效(例如仅前 3 个月)。
套餐变更时保留: 当客户升级或降级订阅时,
preserve_on_plan_change
设置和
discount_codes
参数将决定现有折扣是否延续。

Discount CRUD

折扣 CRUD

Create a discount

创建折扣

typescript
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',
});
typescript
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',
});

List discounts

列出折扣

typescript
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);
}
typescript
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);
}

Retrieve a discount by ID

按 ID 获取折扣

typescript
const discount = await client.discounts.retrieve('discount_id');
console.log(discount.code, discount.type, discount.amount);
typescript
const discount = await client.discounts.retrieve('discount_id');
console.log(discount.code, discount.type, discount.amount);

Retrieve a discount by code

按代码获取折扣

typescript
try {
  const discount = await client.discounts.retrieveByCode('SUMMER2025');
  console.log('Discount found:', discount.amount);
} catch (error) {
  console.log('Code not found or expired');
}
typescript
try {
  const discount = await client.discounts.retrieveByCode('SUMMER2025');
  console.log('Discount found:', discount.amount);
} catch (error) {
  console.log('Code not found or expired');
}

Update a discount

更新折扣

typescript
await client.discounts.update('discount_id', {
  metadata: { updated_at: new Date().toISOString() }
});
typescript
await client.discounts.update('discount_id', {
  metadata: { updated_at: new Date().toISOString() }
});

Delete a discount

删除折扣

typescript
await client.discounts.delete('discount_id');
typescript
await client.discounts.delete('discount_id');

Discount types and configuration

折扣类型与配置

Important: The feature documentation states that only percentage discounts are currently supported. However, the current OpenAPI schema defines both
flat
and
percentage
types with currency options. Follow the API reference when implementing. If you encounter unexpected behavior with flat discounts, verify with Dodo support whether flat discounts are fully enabled in your account.
重要: 功能文档指出,目前仅支持百分比折扣。然而,当前 OpenAPI schema 同时定义了带
currency_options
的
flat
和
percentage
类型。实现时请遵循 API reference。如果
flat
折扣出现意外行为,请向 Dodo support 核实你的账户中是否已完全启用
flat
折扣。

Percentage discounts

百分比折扣

Expressed as an integer where 100 = 1%. A 15% discount is
1500
.
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500, // 15%
  code: 'PERCENT15',
});
以整数表示,其中
100
表示 1%。15% 折扣对应
1500
。
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500, // 15%
  code: 'PERCENT15',
});

Flat discounts

固定金额折扣

For flat discounts, set the deduction per currency through
currency_options
. A $10 USD deduction is
max_amount_possible: 1000
(cents).
typescript
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',
});
对于固定金额折扣,通过
currency_options
设置每种货币的抵扣金额。抵扣 10 美元对应
max_amount_possible: 1000
(美分)。
typescript
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',
});

Eligibility and restrictions

资格与限制

Product restrictions

产品限制

Limit a discount to specific products:
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'PREMIUM_ONLY',
  restricted_to: ['pdt_premium', 'pdt_enterprise'],
});
将折扣限制为仅适用于特定产品:
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'PREMIUM_ONLY',
  restricted_to: ['pdt_premium', 'pdt_enterprise'],
});

Customer eligibility

客户资格

Restrict a discount to specific customers by creating it with
customer_eligibility: 'specific'
, then attaching customers through the discount-customer endpoint.
customer_ids
is not a
discounts.create
parameter.
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'VIP_ONLY',
  customer_eligibility: 'specific',
});

await fetch(
  `https://test.dodopayments.com/discounts/${discount.discount_id}/customers`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.DODO_PAYMENTS_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      customer_ids: ['cus_vip_001', 'cus_vip_002'],
    }),
  },
);
创建折扣时设置
customer_eligibility: 'specific'
,然后通过 discount-customer endpoint 附加客户。
customer_ids
不是
discounts.create
参数。
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'VIP_ONLY',
  customer_eligibility: 'specific',
});

await fetch(
  `https://test.dodopayments.com/discounts/${discount.discount_id}/customers`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.DODO_PAYMENTS_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      customer_ids: ['cus_vip_001', 'cus_vip_002'],
    }),
  },
);

Date range

日期范围

Set activation and expiration dates:
typescript
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',
});
设置启用日期和过期日期:
typescript
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',
});

Usage limits

使用次数限制

Limit the total number of times a code can be used:
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'LIMITED_100',
  usage_limit: 100,
});
限制代码可被使用的总次数:
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'LIMITED_100',
  usage_limit: 100,
});

Subscription-cycle limits

订阅周期限制

Apply a discount only for the first N billing cycles:
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'FIRST_3_MONTHS',
  subscription_cycles: 3, // applies to first 3 billing cycles only
});
仅在最初 N 个计费周期应用折扣:
typescript
const discount = await client.discounts.create({
  type: 'percentage',
  amount: 1500,
  code: 'FIRST_3_MONTHS',
  subscription_cycles: 3, // applies to first 3 billing cycles only
});

Applying codes at checkout

在结账时应用代码

Single code

单个代码

typescript
const session = await client.checkoutSessions.create({
  product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
  discount_codes: ['SUMMER2025'],
  customer: { email: 'user@example.com' },
  return_url: 'https://yoursite.com/return'
});

window.location.href = session.checkout_url;
typescript
const session = await client.checkoutSessions.create({
  product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
  discount_codes: ['SUMMER2025'],
  customer: { email: 'user@example.com' },
  return_url: 'https://yoursite.com/return'
});

window.location.href = session.checkout_url;

Multiple codes (stacking)

多个代码(叠加)

Codes are applied in the order specified. Maximum 20 codes per checkout:
typescript
const session = await client.checkoutSessions.create({
  product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
  discount_codes: ['WELCOME10', 'BLACKFRIDAY20', 'LOYALTY5'],
  customer: { email: 'user@example.com' },
  return_url: 'https://yoursite.com/return'
});
代码将按照指定顺序应用。每次结账最多可应用 20 个代码:
typescript
const session = await client.checkoutSessions.create({
  product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
  discount_codes: ['WELCOME10', 'BLACKFRIDAY20', 'LOYALTY5'],
  customer: { email: 'user@example.com' },
  return_url: 'https://yoursite.com/return'
});

Validating codes before checkout

结账前验证代码

Validate a code and check its eligibility before showing adjusted pricing:
typescript
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);
}
在显示调整后的价格前验证代码并检查其资格:
typescript
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);
}

Discounts on plan changes

套餐变更时的折扣

When a customer changes their subscription plan, discounts can be preserved, replaced, or cleared depending on the
discount_codes
parameter and each discount's
preserve_on_plan_change
setting. Set
preserve_on_plan_change
through
discounts.create
or
discounts.update
; it is not a
subscriptions.changePlan
parameter.
当客户变更订阅套餐时,折扣可能会被保留、替换或清除,具体取决于
discount_codes
参数以及每个折扣的
preserve_on_plan_change
设置。通过
discounts.create
或
discounts.update
设置
preserve_on_plan_change
;它不是
subscriptions.changePlan
参数。

Behavior matrix

行为矩阵

Scenario
discount_codes
Discount's
preserve_on_plan_change
Result
Upgrade with no change
undefined
true
Existing discounts carry forward
Upgrade with no change
undefined
false
Existing discounts are removed
Upgrade with no change
[]
(empty array)
anyExisting discounts are removed
Upgrade with replacement
['NEW_CODE']
anyOnly
NEW_CODE
applies; old discounts removed
场景
discount_codes
折扣的
preserve_on_plan_change
结果
升级且不更改
undefined
true
现有折扣会延续
升级且不更改
undefined
false
现有折扣会被移除
升级且不更改
[]
(空数组)
任意现有折扣会被移除
升级并替换
['NEW_CODE']
任意仅应用
NEW_CODE
;旧折扣会被移除

Example: preserve existing discounts

示例:保留现有折扣

typescript
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.
});
typescript
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.
});

Example: replace discounts on upgrade

示例:升级时替换折扣

typescript
await client.subscriptions.changePlan('sub_123', {
  product_id: 'pdt_pro',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately',
  discount_codes: ['UPGRADE20'], // replaces old discounts
});
typescript
await client.subscriptions.changePlan('sub_123', {
  product_id: 'pdt_pro',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately',
  discount_codes: ['UPGRADE20'], // replaces old discounts
});

Example: remove all discounts on downgrade

示例:降级时移除所有折扣

typescript
await client.subscriptions.changePlan('sub_123', {
  product_id: 'pdt_basic',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately',
  discount_codes: [], // empty array removes all discounts
});
typescript
await client.subscriptions.changePlan('sub_123', {
  product_id: 'pdt_basic',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately',
  discount_codes: [], // empty array removes all discounts
});

Common mistakes

常见错误

Validating discounts client-side only

仅在客户端验证折扣

Never trust client-side validation. Always validate on the server before applying a discount at checkout. A user can modify the discount code in the browser.
typescript
// 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.
}
绝不要信任客户端验证。始终在服务器端验证,然后才能在结账时应用折扣。用户可以在浏览器中修改折扣码。
typescript
// 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.
}

Assuming codes stack unconditionally

假设代码始终可以叠加

Discounts stack in order, but eligibility rules still apply. If a code is restricted to a product not in the cart, it won't apply even if other codes do.
typescript
// 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'
});
折扣会按顺序叠加,但资格规则仍然适用。如果某个代码仅限用于购物车中不存在的产品,那么即使其他代码可用,它也不会生效。
typescript
// 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'
});

Losing a discount on upgrade by passing an empty array

传入空数组导致升级时丢失折扣

Passing
discount_codes: []
explicitly removes all discounts, even if
preserve_on_plan_change
is true. Omit the parameter entirely if you want to preserve existing discounts.
typescript
// 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
});
显式传入
discount_codes: []
会移除所有折扣,即使
preserve_on_plan_change
为
true
。如果希望保留现有折扣,请完全省略该参数。
typescript
// 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
});

Not handling expired or exhausted codes

未处理已过期或已用尽的代码

A code can expire by date or by reaching its redemption limit. Always check both before showing a discount to the user.
typescript
// 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');
}
代码可能因到达有效期或兑换次数上限而失效。在向用户显示折扣前,务必同时检查这两种情况。
typescript
// 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');
}

Confusing subscription-cycle limits with expiration dates

混淆订阅周期限制和过期日期

subscription_cycles
applies only to subscriptions and controls how many billing cycles the discount applies.
expires_at
is a hard cutoff for all uses of the code.
typescript
// 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',
});
subscription_cycles
仅适用于订阅,用于控制折扣应用的计费周期数。
expires_at
是该代码所有使用场景的硬性截止时间。
typescript
// 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',
});

Resources

资源