@dodopayments/opencode-plugin 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +1 -1
- package/README.md +152 -42
- package/opencode-plugin/README.md +20 -6
- package/opencode-plugin/index.js +27 -1
- package/package.json +5 -5
- package/skills/better-auth-integration/SKILL.md +214 -0
- package/skills/billing-sdk/SKILL.md +357 -370
- package/skills/checkout-integration/SKILL.md +580 -329
- package/skills/credit-based-billing/SKILL.md +448 -775
- package/skills/customer-management/SKILL.md +281 -0
- package/skills/discounts-and-promotions/SKILL.md +464 -0
- package/skills/dodo-best-practices/SKILL.md +471 -0
- package/skills/framework-adapters/SKILL.md +644 -0
- package/skills/license-keys/SKILL.md +324 -199
- package/skills/localized-pricing/SKILL.md +230 -0
- package/skills/mobile-checkout/SKILL.md +521 -0
- package/skills/product-catalog-management/SKILL.md +311 -0
- package/skills/refunds-and-disputes/SKILL.md +333 -0
- package/skills/subscription-integration/SKILL.md +323 -404
- package/skills/testing-and-go-live/SKILL.md +335 -0
- package/skills/usage-based-billing/SKILL.md +339 -479
- package/skills/webhook-integration/SKILL.md +600 -402
- package/skills/best-practices/SKILL.md +0 -315
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: discounts-and-promotions
|
|
3
|
+
description: Guide for implementing discount codes and promotional pricing with Dodo Payments, including CRUD operations, eligibility rules, stacking, subscription-cycle limits, and plan-change preservation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Discounts and Promotions
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
## When to use this skill
|
|
11
|
+
|
|
12
|
+
- Create, update, list, or delete discount codes
|
|
13
|
+
- Apply discount codes at checkout or during plan changes
|
|
14
|
+
- Validate a code before displaying adjusted pricing to the user
|
|
15
|
+
- Configure eligibility rules, usage limits, or subscription-cycle restrictions
|
|
16
|
+
- Handle the subtle semantics of discount preservation during plan upgrades/downgrades
|
|
17
|
+
- Debug why a discount isn't applying or stacking as expected
|
|
18
|
+
|
|
19
|
+
## Core concepts
|
|
20
|
+
|
|
21
|
+
**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).
|
|
22
|
+
|
|
23
|
+
**Discount code:** A human-readable string (e.g., `SUMMER2025`) that customers enter at checkout. Codes are case-sensitive.
|
|
24
|
+
|
|
25
|
+
**Eligibility:** Discounts can be restricted to specific products, customers, or date ranges. A discount without restrictions applies to any product and any customer.
|
|
26
|
+
|
|
27
|
+
**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.
|
|
28
|
+
|
|
29
|
+
**Subscription-cycle limits:** A discount can be configured to apply only for a specific number of billing cycles (e.g., first 3 months only).
|
|
30
|
+
|
|
31
|
+
**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.
|
|
32
|
+
|
|
33
|
+
## Discount CRUD
|
|
34
|
+
|
|
35
|
+
### Create a discount
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import DodoPayments from 'dodopayments';
|
|
39
|
+
|
|
40
|
+
const client = new DodoPayments({
|
|
41
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
42
|
+
environment: 'test_mode',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Percentage discount: 15% off
|
|
46
|
+
const percentageDiscount = await client.discounts.create({
|
|
47
|
+
type: 'percentage',
|
|
48
|
+
amount: 1500, // 15%
|
|
49
|
+
code: 'SUMMER2025',
|
|
50
|
+
metadata: {
|
|
51
|
+
campaign: 'summer_promo',
|
|
52
|
+
source: 'email_blast'
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Flat discount: $10 off (1000 cents in currency_options)
|
|
57
|
+
const flatDiscount = await client.discounts.create({
|
|
58
|
+
type: 'flat',
|
|
59
|
+
amount: 1000,
|
|
60
|
+
currency_options: [{
|
|
61
|
+
currency: 'USD',
|
|
62
|
+
max_amount_possible: 1000,
|
|
63
|
+
is_default: true,
|
|
64
|
+
}],
|
|
65
|
+
code: 'WELCOME10',
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### List discounts
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const discounts = await client.discounts.list({
|
|
73
|
+
page_size: 50,
|
|
74
|
+
page_number: 0,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Paginated responses expose `items`.
|
|
78
|
+
for (const discount of discounts.items) {
|
|
79
|
+
console.log(`${discount.code}: ${discount.type} ${discount.amount}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Or let the SDK walk every page for you:
|
|
83
|
+
for await (const discount of client.discounts.list()) {
|
|
84
|
+
console.log(discount.code);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Retrieve a discount by ID
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const discount = await client.discounts.retrieve('discount_id');
|
|
92
|
+
console.log(discount.code, discount.type, discount.amount);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Retrieve a discount by code
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
try {
|
|
99
|
+
const discount = await client.discounts.retrieveByCode('SUMMER2025');
|
|
100
|
+
console.log('Discount found:', discount.amount);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.log('Code not found or expired');
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Update a discount
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
await client.discounts.update('discount_id', {
|
|
110
|
+
metadata: { updated_at: new Date().toISOString() }
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Delete a discount
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
await client.discounts.delete('discount_id');
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Discount types and configuration
|
|
121
|
+
|
|
122
|
+
**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.
|
|
123
|
+
|
|
124
|
+
### Percentage discounts
|
|
125
|
+
|
|
126
|
+
Expressed as an integer where 100 = 1%. A 15% discount is `1500`.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const discount = await client.discounts.create({
|
|
130
|
+
type: 'percentage',
|
|
131
|
+
amount: 1500, // 15%
|
|
132
|
+
code: 'PERCENT15',
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Flat discounts
|
|
137
|
+
|
|
138
|
+
For flat discounts, set the deduction per currency through `currency_options`. A $10 USD deduction is `max_amount_possible: 1000` (cents).
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const discount = await client.discounts.create({
|
|
142
|
+
type: 'flat',
|
|
143
|
+
amount: 1000, // $10.00
|
|
144
|
+
currency_options: [{
|
|
145
|
+
currency: 'USD',
|
|
146
|
+
max_amount_possible: 1000,
|
|
147
|
+
is_default: true,
|
|
148
|
+
}],
|
|
149
|
+
code: 'FLAT10',
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Eligibility and restrictions
|
|
154
|
+
|
|
155
|
+
### Product restrictions
|
|
156
|
+
|
|
157
|
+
Limit a discount to specific products:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const discount = await client.discounts.create({
|
|
161
|
+
type: 'percentage',
|
|
162
|
+
amount: 1500,
|
|
163
|
+
code: 'PREMIUM_ONLY',
|
|
164
|
+
restricted_to: ['pdt_premium', 'pdt_enterprise'],
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Customer eligibility
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const discount = await client.discounts.create({
|
|
174
|
+
type: 'percentage',
|
|
175
|
+
amount: 1500,
|
|
176
|
+
code: 'VIP_ONLY',
|
|
177
|
+
customer_eligibility: 'specific',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await fetch(
|
|
181
|
+
`https://test.dodopayments.com/discounts/${discount.discount_id}/customers`,
|
|
182
|
+
{
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: {
|
|
185
|
+
Authorization: `Bearer ${process.env.DODO_PAYMENTS_API_KEY}`,
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
customer_ids: ['cus_vip_001', 'cus_vip_002'],
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Date range
|
|
196
|
+
|
|
197
|
+
Set activation and expiration dates:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const discount = await client.discounts.create({
|
|
201
|
+
type: 'percentage',
|
|
202
|
+
amount: 1500,
|
|
203
|
+
code: 'SUMMER2025',
|
|
204
|
+
starts_at: '2025-06-01T00:00:00Z',
|
|
205
|
+
expires_at: '2025-08-31T23:59:59Z',
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Usage limits
|
|
210
|
+
|
|
211
|
+
Limit the total number of times a code can be used:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const discount = await client.discounts.create({
|
|
215
|
+
type: 'percentage',
|
|
216
|
+
amount: 1500,
|
|
217
|
+
code: 'LIMITED_100',
|
|
218
|
+
usage_limit: 100,
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Subscription-cycle limits
|
|
223
|
+
|
|
224
|
+
Apply a discount only for the first N billing cycles:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const discount = await client.discounts.create({
|
|
228
|
+
type: 'percentage',
|
|
229
|
+
amount: 1500,
|
|
230
|
+
code: 'FIRST_3_MONTHS',
|
|
231
|
+
subscription_cycles: 3, // applies to first 3 billing cycles only
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Applying codes at checkout
|
|
236
|
+
|
|
237
|
+
### Single code
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const session = await client.checkoutSessions.create({
|
|
241
|
+
product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
|
|
242
|
+
discount_codes: ['SUMMER2025'],
|
|
243
|
+
customer: { email: 'user@example.com' },
|
|
244
|
+
return_url: 'https://yoursite.com/return'
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
window.location.href = session.checkout_url;
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Multiple codes (stacking)
|
|
251
|
+
|
|
252
|
+
Codes are applied in the order specified. Maximum 20 codes per checkout:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const session = await client.checkoutSessions.create({
|
|
256
|
+
product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
|
|
257
|
+
discount_codes: ['WELCOME10', 'BLACKFRIDAY20', 'LOYALTY5'],
|
|
258
|
+
customer: { email: 'user@example.com' },
|
|
259
|
+
return_url: 'https://yoursite.com/return'
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Validating codes before checkout
|
|
264
|
+
|
|
265
|
+
Validate a code and check its eligibility before showing adjusted pricing:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
async function validateDiscount(code, productId) {
|
|
269
|
+
try {
|
|
270
|
+
const discount = await client.discounts.retrieveByCode(code);
|
|
271
|
+
|
|
272
|
+
// Check expiration
|
|
273
|
+
if (discount.expires_at && new Date(discount.expires_at) < new Date()) {
|
|
274
|
+
return { valid: false, reason: 'Code expired' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check usage limit
|
|
278
|
+
if (discount.usage_limit && discount.times_used >= discount.usage_limit) {
|
|
279
|
+
return { valid: false, reason: 'Code exhausted' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check product eligibility
|
|
283
|
+
if (discount.restricted_to.length > 0) {
|
|
284
|
+
if (!discount.restricted_to.includes(productId)) {
|
|
285
|
+
return { valid: false, reason: 'Code not valid for this product' };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { valid: true, discount };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return { valid: false, reason: 'Code not found or unavailable' };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Usage
|
|
296
|
+
const result = await validateDiscount('SUMMER2025', 'pdt_abc');
|
|
297
|
+
if (result.valid) {
|
|
298
|
+
console.log('Discount applies:', result.discount.amount);
|
|
299
|
+
} else {
|
|
300
|
+
console.log('Cannot apply:', result.reason);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Discounts on plan changes
|
|
305
|
+
|
|
306
|
+
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.
|
|
307
|
+
|
|
308
|
+
### Behavior matrix
|
|
309
|
+
|
|
310
|
+
| Scenario | `discount_codes` | Discount's `preserve_on_plan_change` | Result |
|
|
311
|
+
|---|---|---|---|
|
|
312
|
+
| Upgrade with no change | `undefined` | `true` | Existing discounts carry forward |
|
|
313
|
+
| Upgrade with no change | `undefined` | `false` | Existing discounts are removed |
|
|
314
|
+
| Upgrade with no change | `[]` (empty array) | any | Existing discounts are removed |
|
|
315
|
+
| Upgrade with replacement | `['NEW_CODE']` | any | Only `NEW_CODE` applies; old discounts removed |
|
|
316
|
+
|
|
317
|
+
### Example: preserve existing discounts
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
await client.discounts.update('dsc_existing', {
|
|
321
|
+
preserve_on_plan_change: true,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await client.subscriptions.changePlan('sub_123', {
|
|
325
|
+
product_id: 'pdt_pro',
|
|
326
|
+
quantity: 1,
|
|
327
|
+
proration_billing_mode: 'prorated_immediately',
|
|
328
|
+
// Omit discount_codes. Existing discounts configured with
|
|
329
|
+
// preserve_on_plan_change: true carry forward.
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Example: replace discounts on upgrade
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
await client.subscriptions.changePlan('sub_123', {
|
|
337
|
+
product_id: 'pdt_pro',
|
|
338
|
+
quantity: 1,
|
|
339
|
+
proration_billing_mode: 'prorated_immediately',
|
|
340
|
+
discount_codes: ['UPGRADE20'], // replaces old discounts
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Example: remove all discounts on downgrade
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
await client.subscriptions.changePlan('sub_123', {
|
|
348
|
+
product_id: 'pdt_basic',
|
|
349
|
+
quantity: 1,
|
|
350
|
+
proration_billing_mode: 'prorated_immediately',
|
|
351
|
+
discount_codes: [], // empty array removes all discounts
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Common mistakes
|
|
356
|
+
|
|
357
|
+
### Validating discounts client-side only
|
|
358
|
+
|
|
359
|
+
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.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// WRONG: client-side only
|
|
363
|
+
if (code.length > 0) {
|
|
364
|
+
applyDiscount(code);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// CORRECT: server-side validation
|
|
368
|
+
try {
|
|
369
|
+
const discount = await client.discounts.retrieveByCode(code);
|
|
370
|
+
if (isEligible(discount)) {
|
|
371
|
+
// proceed with checkout
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
// Unknown or unavailable code; reject the checkout request.
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Assuming codes stack unconditionally
|
|
379
|
+
|
|
380
|
+
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.
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// Both codes may not apply if they have conflicting restrictions
|
|
384
|
+
const session = await client.checkoutSessions.create({
|
|
385
|
+
product_cart: [{ product_id: 'pdt_basic', quantity: 1 }],
|
|
386
|
+
discount_codes: ['PREMIUM_ONLY', 'BASIC_ONLY'], // one or both may fail
|
|
387
|
+
return_url: 'https://yoursite.com/return'
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Losing a discount on upgrade by passing an empty array
|
|
392
|
+
|
|
393
|
+
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.
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// WRONG: removes all discounts
|
|
397
|
+
await client.subscriptions.changePlan('sub_123', {
|
|
398
|
+
product_id: 'pdt_pro',
|
|
399
|
+
quantity: 1,
|
|
400
|
+
proration_billing_mode: 'prorated_immediately',
|
|
401
|
+
discount_codes: [], // this clears discounts
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// CORRECT: preserves discounts whose preserve_on_plan_change property is true
|
|
405
|
+
await client.subscriptions.changePlan('sub_123', {
|
|
406
|
+
product_id: 'pdt_pro',
|
|
407
|
+
quantity: 1,
|
|
408
|
+
proration_billing_mode: 'prorated_immediately',
|
|
409
|
+
// omit discount_codes
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Not handling expired or exhausted codes
|
|
414
|
+
|
|
415
|
+
A code can expire by date or by reaching its redemption limit. Always check both before showing a discount to the user.
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// WRONG: assumes code is always valid
|
|
419
|
+
const discount = await client.discounts.retrieveByCode(code);
|
|
420
|
+
applyDiscount(discount);
|
|
421
|
+
|
|
422
|
+
// CORRECT: catch unknown codes, then check expiration and usage
|
|
423
|
+
try {
|
|
424
|
+
const discount = await client.discounts.retrieveByCode(code);
|
|
425
|
+
if (discount.expires_at && new Date(discount.expires_at) < new Date()) {
|
|
426
|
+
showError('Code expired');
|
|
427
|
+
} else if (discount.usage_limit && discount.times_used >= discount.usage_limit) {
|
|
428
|
+
showError('Code exhausted');
|
|
429
|
+
} else {
|
|
430
|
+
applyDiscount(discount);
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
showError('Code not found');
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Confusing subscription-cycle limits with expiration dates
|
|
438
|
+
|
|
439
|
+
`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.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// Applies to first 3 billing cycles of any subscription
|
|
443
|
+
const discount = await client.discounts.create({
|
|
444
|
+
type: 'percentage',
|
|
445
|
+
amount: 1500,
|
|
446
|
+
code: 'FIRST_3_MONTHS',
|
|
447
|
+
subscription_cycles: 3,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Expires on a specific date, regardless of billing cycles
|
|
451
|
+
const discount2 = await client.discounts.create({
|
|
452
|
+
type: 'percentage',
|
|
453
|
+
amount: 1500,
|
|
454
|
+
code: 'SUMMER_ONLY',
|
|
455
|
+
expires_at: '2025-08-31T23:59:59Z',
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Resources
|
|
460
|
+
|
|
461
|
+
- [Discounts feature guide](https://docs.dodopayments.com/features/discount-codes)
|
|
462
|
+
- [Create discount API](https://docs.dodopayments.com/api-reference/discounts/create-discount)
|
|
463
|
+
- [Checkout Sessions API](https://docs.dodopayments.com/developer-resources/checkout-session)
|
|
464
|
+
- [Subscription plan changes](https://docs.dodopayments.com/api-reference/subscriptions/change-plan)
|