discounts-and-promotions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDiscounts 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., ) that customers enter at checkout. Codes are case-sensitive.
SUMMER2025Eligibility: 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 array, up to a maximum of 20 codes.
discount_codesSubscription-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 setting and the parameter control whether existing discounts carry forward.
preserve_on_plan_changediscount_codes折扣类型: Dodo 同时支持百分比折扣和固定金额折扣。百分比金额以整数表示,其中 表示 15%( 表示 1%)。固定金额使用货币的最小单位(USD 为美分)。
1500100折扣码: 客户在结账时输入的易读字符串(例如 )。代码区分大小写。
SUMMER2025资格限制: 折扣可以限制为仅适用于特定产品、客户或日期范围。没有限制的折扣适用于任何产品和任何客户。
叠加: 单次结账或订阅可以应用多个折扣码。它们按照 数组中指定的顺序应用,最多可应用 20 个代码。
discount_codes订阅周期限制: 可以将折扣配置为仅在特定数量的计费周期内生效(例如仅前 3 个月)。
套餐变更时保留: 当客户升级或降级订阅时, 设置和 参数将决定现有折扣是否延续。
preserve_on_plan_changediscount_codesDiscount 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 and 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.
flatpercentage重要: 功能文档指出,目前仅支持百分比折扣。然而,当前 OpenAPI schema 同时定义了带 的 和 类型。实现时请遵循 API reference。如果 折扣出现意外行为,请向 Dodo support 核实你的账户中是否已完全启用 折扣。
currency_optionsflatpercentageflatflatPercentage discounts
百分比折扣
Expressed as an integer where 100 = 1%. A 15% discount is .
1500typescript
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500, // 15%
code: 'PERCENT15',
});以整数表示,其中 表示 1%。15% 折扣对应 。
1001500typescript
const discount = await client.discounts.create({
type: 'percentage',
amount: 1500, // 15%
code: 'PERCENT15',
});Flat discounts
固定金额折扣
For flat discounts, set the deduction per currency through . A $10 USD deduction is (cents).
currency_optionsmax_amount_possible: 1000typescript
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',
});对于固定金额折扣,通过 设置每种货币的抵扣金额。抵扣 10 美元对应 (美分)。
currency_optionsmax_amount_possible: 1000typescript
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 , then attaching customers through the discount-customer endpoint. is not a parameter.
customer_eligibility: 'specific'customer_idsdiscounts.createtypescript
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'],
}),
},
);创建折扣时设置 ,然后通过 discount-customer endpoint 附加客户。 不是 参数。
customer_eligibility: 'specific'customer_idsdiscounts.createtypescript
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 parameter and each discount's setting. Set through or ; it is not a parameter.
discount_codespreserve_on_plan_changepreserve_on_plan_changediscounts.creatediscounts.updatesubscriptions.changePlan当客户变更订阅套餐时,折扣可能会被保留、替换或清除,具体取决于 参数以及每个折扣的 设置。通过 或 设置 ;它不是 参数。
discount_codespreserve_on_plan_changediscounts.creatediscounts.updatepreserve_on_plan_changesubscriptions.changePlanBehavior matrix
行为矩阵
| Scenario | | Discount's | Result |
|---|---|---|---|
| Upgrade with no change | | | Existing discounts carry forward |
| Upgrade with no change | | | Existing discounts are removed |
| Upgrade with no change | | any | Existing discounts are removed |
| Upgrade with replacement | | any | Only |
| 场景 | | 折扣的 | 结果 |
|---|---|---|---|
| 升级且不更改 | | | 现有折扣会延续 |
| 升级且不更改 | | | 现有折扣会被移除 |
| 升级且不更改 | | 任意 | 现有折扣会被移除 |
| 升级并替换 | | 任意 | 仅应用 |
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 explicitly removes all discounts, even if is true. Omit the parameter entirely if you want to preserve existing discounts.
discount_codes: []preserve_on_plan_changetypescript
// 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_changetruetypescript
// 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_cyclesexpires_attypescript
// 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_cyclesexpires_attypescript
// 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',
});