@dodopayments/opencode-plugin 0.3.3 → 0.4.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/README.md +48 -12
- package/opencode-plugin/README.md +3 -3
- package/package.json +1 -1
- package/skills/best-practices/SKILL.md +335 -179
- 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/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
|
@@ -5,71 +5,45 @@ description: Guide for implementing license key management with Dodo Payments -
|
|
|
5
5
|
|
|
6
6
|
# Dodo Payments License Keys
|
|
7
7
|
|
|
8
|
-
**Reference: [docs.dodopayments.com/features/license-keys](https://docs.dodopayments.com/features/license-keys)**
|
|
9
|
-
|
|
10
8
|
License keys authorize access to your digital products. Use them for software licensing, per-seat controls, and gating premium features.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## Overview
|
|
15
|
-
|
|
16
|
-
License keys are unique tokens that:
|
|
17
|
-
- Authorize access to software, plugins, CLIs
|
|
18
|
-
- Limit activations per user or device
|
|
19
|
-
- Gate downloads, updates, or premium features
|
|
20
|
-
- Can be linked to subscriptions or one-time purchases
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## Creating License Keys
|
|
25
|
-
|
|
26
|
-
### In Dashboard
|
|
27
|
-
|
|
28
|
-
1. Go to Dashboard → License Keys
|
|
29
|
-
2. Click "Create License Key"
|
|
30
|
-
3. Configure settings:
|
|
31
|
-
- **Expiry Date**: Duration or "no expiry" for perpetual
|
|
32
|
-
- **Activation Limit**: Max concurrent activations (1, 5, unlimited)
|
|
33
|
-
- **Activation Instructions**: Steps for customers
|
|
10
|
+
## When to use this skill
|
|
34
11
|
|
|
35
|
-
|
|
12
|
+
- Implementing end-user license activation and validation flows
|
|
13
|
+
- Building merchant dashboards to manage customer license keys
|
|
14
|
+
- Handling license expiry, activation limits, and subscription-linked revocation
|
|
15
|
+
- Integrating license checks into desktop apps, CLIs, or web services
|
|
16
|
+
- Reacting to license lifecycle webhooks
|
|
36
17
|
|
|
37
|
-
|
|
18
|
+
## Core concepts
|
|
38
19
|
|
|
39
|
-
License
|
|
20
|
+
**License Key Entitlements** deliver keys when a product is purchased. The entitlement config supplies activation limits, optional duration, and fulfillment mode (auto or manual).
|
|
40
21
|
|
|
41
|
-
|
|
42
|
-
2. When purchased, a key is generated and emailed to customer
|
|
43
|
-
3. `license_key.created` webhook is fired
|
|
22
|
+
**Three distinct resources:**
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
### Public Endpoints (No API Key Required)
|
|
24
|
+
1. **`client.licenses.*`** — end-user activation flow (public, no API key required)
|
|
25
|
+
- `activate()` — activate a key on a device
|
|
26
|
+
- `validate()` — check if a key is valid
|
|
27
|
+
- `deactivate()` — free an activation slot
|
|
50
28
|
|
|
51
|
-
|
|
29
|
+
2. **`client.licenseKeys.*`** — merchant-side key management (requires API key)
|
|
30
|
+
- `list()`, `retrieve()`, `create()`, `update()` — manage keys for your customers
|
|
52
31
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
| `POST /licenses/activate` | Activate a license key |
|
|
56
|
-
| `POST /licenses/deactivate` | Deactivate an instance |
|
|
57
|
-
| `POST /licenses/validate` | Check if key is valid |
|
|
32
|
+
3. **`client.licenseKeyInstances.*`** — per-device activation instances (requires API key)
|
|
33
|
+
- `list()`, `retrieve()`, `update()` — track active devices per key
|
|
58
34
|
|
|
59
|
-
|
|
35
|
+
**Activation limits:** blank/null means unlimited; otherwise it's the maximum concurrent activations. Attempting to activate beyond the limit returns `422`.
|
|
60
36
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
| `PATCH /license_keys/:id` | Update license key |
|
|
66
|
-
| `GET /license_key_instances` | List activation instances |
|
|
37
|
+
**Expiry semantics:**
|
|
38
|
+
- One-time-payment keys honor the entitlement duration.
|
|
39
|
+
- Subscription-issued keys have no independent expiry; validity follows subscription state. On hold disables them temporarily. An immediate cancellation disables them permanently, but when `cancel_at_next_billing_date` is set, keep them active until the subscription reaches the end of its term.
|
|
40
|
+
- Imported keys use nullable `expires_at`; null means perpetual.
|
|
67
41
|
|
|
68
42
|
---
|
|
69
43
|
|
|
70
|
-
##
|
|
44
|
+
## End-User Activation Flow
|
|
71
45
|
|
|
72
|
-
### Activate a
|
|
46
|
+
### Activate a license key
|
|
73
47
|
|
|
74
48
|
```typescript
|
|
75
49
|
import DodoPayments from 'dodopayments';
|
|
@@ -87,18 +61,21 @@ async function activateLicense(licenseKey: string, deviceName: string) {
|
|
|
87
61
|
return {
|
|
88
62
|
success: true,
|
|
89
63
|
instanceId: response.id,
|
|
90
|
-
|
|
64
|
+
customerId: response.customer.customer_id,
|
|
65
|
+
productId: response.product.product_id,
|
|
91
66
|
};
|
|
92
67
|
} catch (error: any) {
|
|
93
|
-
|
|
94
|
-
success: false,
|
|
95
|
-
|
|
96
|
-
};
|
|
68
|
+
if (error.status === 422) {
|
|
69
|
+
return { success: false, error: 'Activation limit reached' };
|
|
70
|
+
}
|
|
71
|
+
return { success: false, error: error.message || 'Activation failed' };
|
|
97
72
|
}
|
|
98
73
|
}
|
|
99
74
|
```
|
|
100
75
|
|
|
101
|
-
|
|
76
|
+
Response includes `id` (instance ID), `business_id`, `name`, `license_key_id`, `created_at`, customer details, and product details.
|
|
77
|
+
|
|
78
|
+
### Validate a license key
|
|
102
79
|
|
|
103
80
|
```typescript
|
|
104
81
|
import DodoPayments from 'dodopayments';
|
|
@@ -111,19 +88,16 @@ async function validateLicense(licenseKey: string) {
|
|
|
111
88
|
license_key: licenseKey,
|
|
112
89
|
});
|
|
113
90
|
|
|
114
|
-
return {
|
|
115
|
-
valid: response.valid,
|
|
116
|
-
activations: response.activations_count,
|
|
117
|
-
maxActivations: response.activations_limit,
|
|
118
|
-
expiresAt: response.expires_at,
|
|
119
|
-
};
|
|
91
|
+
return { valid: response.valid };
|
|
120
92
|
} catch (error) {
|
|
121
93
|
return { valid: false };
|
|
122
94
|
}
|
|
123
95
|
}
|
|
124
96
|
```
|
|
125
97
|
|
|
126
|
-
|
|
98
|
+
Optional: pass `license_key_instance_id` to validate a specific instance.
|
|
99
|
+
|
|
100
|
+
### Deactivate a license
|
|
127
101
|
|
|
128
102
|
```typescript
|
|
129
103
|
import DodoPayments from 'dodopayments';
|
|
@@ -137,23 +111,120 @@ async function deactivateLicense(licenseKey: string, instanceId: string) {
|
|
|
137
111
|
license_key_instance_id: instanceId,
|
|
138
112
|
});
|
|
139
113
|
|
|
140
|
-
return { success: true
|
|
114
|
+
return { success: true };
|
|
141
115
|
} catch (error: any) {
|
|
142
|
-
return { success: false,
|
|
116
|
+
return { success: false, error: error.message };
|
|
143
117
|
}
|
|
144
118
|
}
|
|
145
119
|
```
|
|
146
120
|
|
|
147
121
|
---
|
|
148
122
|
|
|
123
|
+
## Merchant-Side Key Management
|
|
124
|
+
|
|
125
|
+
### List customer license keys
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import DodoPayments from 'dodopayments';
|
|
129
|
+
|
|
130
|
+
const client = new DodoPayments({
|
|
131
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
132
|
+
environment: 'test_mode',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
async function listCustomerKeys(customerId: string) {
|
|
136
|
+
const keys = await client.licenseKeys.list({
|
|
137
|
+
customer_id: customerId,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return keys.items.map(key => ({
|
|
141
|
+
id: key.id,
|
|
142
|
+
key: key.key,
|
|
143
|
+
status: key.status,
|
|
144
|
+
expiresAt: key.expires_at,
|
|
145
|
+
activationsUsed: key.instances_count,
|
|
146
|
+
activationsLimit: key.activations_limit,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Retrieve a single license key
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const key = await client.licenseKeys.retrieve('lk_abc123');
|
|
155
|
+
|
|
156
|
+
console.log({
|
|
157
|
+
id: key.id,
|
|
158
|
+
key: key.key,
|
|
159
|
+
status: key.status,
|
|
160
|
+
expiresAt: key.expires_at,
|
|
161
|
+
activationsUsed: key.instances_count,
|
|
162
|
+
activationsLimit: key.activations_limit,
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Update a license key
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Disable a key or adjust its activation limit
|
|
170
|
+
await client.licenseKeys.update('lk_abc123', {
|
|
171
|
+
disabled: true,
|
|
172
|
+
// or adjust activations_limit
|
|
173
|
+
activations_limit: 10,
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Create a license key (manual issuance)
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const newKey = await client.licenseKeys.create({
|
|
181
|
+
customer_id: 'cus_abc123',
|
|
182
|
+
key: 'PREMIUM-AAAA-BBBB-CCCC',
|
|
183
|
+
product_id: 'pdt_abc123',
|
|
184
|
+
activations_limit: 5,
|
|
185
|
+
expires_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
console.log(newKey.key); // The actual license key string
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## List Activation Instances
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import DodoPayments from 'dodopayments';
|
|
197
|
+
|
|
198
|
+
const client = new DodoPayments({
|
|
199
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
200
|
+
environment: 'test_mode',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// List all instances for a specific license key
|
|
204
|
+
for await (const instance of client.licenseKeyInstances.list({
|
|
205
|
+
license_key_id: 'lk_abc123',
|
|
206
|
+
})) {
|
|
207
|
+
console.log({
|
|
208
|
+
id: instance.id,
|
|
209
|
+
name: instance.name,
|
|
210
|
+
createdAt: instance.created_at,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Optional query fields: `page_size` (default 10, max 100), `page_number` (default 0), `license_key_id`, and `grant_id`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
149
219
|
## Desktop App Integration
|
|
150
220
|
|
|
151
|
-
### Electron
|
|
221
|
+
### Electron app example
|
|
152
222
|
|
|
153
223
|
```typescript
|
|
154
224
|
// main/license.ts
|
|
155
225
|
import Store from 'electron-store';
|
|
156
226
|
import DodoPayments from 'dodopayments';
|
|
227
|
+
import os from 'os';
|
|
157
228
|
|
|
158
229
|
const store = new Store();
|
|
159
230
|
const client = new DodoPayments();
|
|
@@ -166,21 +237,19 @@ interface LicenseInfo {
|
|
|
166
237
|
|
|
167
238
|
export async function activateLicense(licenseKey: string): Promise<boolean> {
|
|
168
239
|
try {
|
|
169
|
-
// Get device identifier
|
|
170
240
|
const deviceName = `${os.hostname()} - ${os.platform()}`;
|
|
171
|
-
|
|
241
|
+
|
|
172
242
|
const response = await client.licenses.activate({
|
|
173
243
|
license_key: licenseKey,
|
|
174
244
|
name: deviceName,
|
|
175
245
|
});
|
|
176
246
|
|
|
177
|
-
// Store license info locally
|
|
178
247
|
const licenseInfo: LicenseInfo = {
|
|
179
248
|
key: licenseKey,
|
|
180
249
|
instanceId: response.id,
|
|
181
250
|
activatedAt: new Date().toISOString(),
|
|
182
251
|
};
|
|
183
|
-
|
|
252
|
+
|
|
184
253
|
store.set('license', licenseInfo);
|
|
185
254
|
return true;
|
|
186
255
|
} catch (error) {
|
|
@@ -191,7 +260,7 @@ export async function activateLicense(licenseKey: string): Promise<boolean> {
|
|
|
191
260
|
|
|
192
261
|
export async function checkLicense(): Promise<boolean> {
|
|
193
262
|
const license = store.get('license') as LicenseInfo | undefined;
|
|
194
|
-
|
|
263
|
+
|
|
195
264
|
if (!license) {
|
|
196
265
|
return false;
|
|
197
266
|
}
|
|
@@ -203,10 +272,10 @@ export async function checkLicense(): Promise<boolean> {
|
|
|
203
272
|
|
|
204
273
|
return response.valid;
|
|
205
274
|
} catch (error) {
|
|
206
|
-
// If offline, trust local license
|
|
275
|
+
// If offline, trust local license with a grace period
|
|
207
276
|
const activatedAt = new Date(license.activatedAt);
|
|
208
277
|
const daysSinceActivation = (Date.now() - activatedAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
209
|
-
|
|
278
|
+
|
|
210
279
|
// Allow 30-day offline grace period
|
|
211
280
|
return daysSinceActivation < 30;
|
|
212
281
|
}
|
|
@@ -214,7 +283,7 @@ export async function checkLicense(): Promise<boolean> {
|
|
|
214
283
|
|
|
215
284
|
export async function deactivateLicense(): Promise<boolean> {
|
|
216
285
|
const license = store.get('license') as LicenseInfo | undefined;
|
|
217
|
-
|
|
286
|
+
|
|
218
287
|
if (!license) {
|
|
219
288
|
return true;
|
|
220
289
|
}
|
|
@@ -234,7 +303,7 @@ export async function deactivateLicense(): Promise<boolean> {
|
|
|
234
303
|
}
|
|
235
304
|
```
|
|
236
305
|
|
|
237
|
-
### React
|
|
306
|
+
### React component for license input
|
|
238
307
|
|
|
239
308
|
```tsx
|
|
240
309
|
// components/LicenseActivation.tsx
|
|
@@ -254,9 +323,8 @@ export function LicenseActivation({ onActivated }: Props) {
|
|
|
254
323
|
setError(null);
|
|
255
324
|
|
|
256
325
|
try {
|
|
257
|
-
// Call main process (Electron IPC)
|
|
258
326
|
const success = await window.electronAPI.activateLicense(licenseKey);
|
|
259
|
-
|
|
327
|
+
|
|
260
328
|
if (success) {
|
|
261
329
|
onActivated();
|
|
262
330
|
} else {
|
|
@@ -273,7 +341,7 @@ export function LicenseActivation({ onActivated }: Props) {
|
|
|
273
341
|
<div className="license-form">
|
|
274
342
|
<h2>Activate Your License</h2>
|
|
275
343
|
<p>Enter your license key to unlock all features.</p>
|
|
276
|
-
|
|
344
|
+
|
|
277
345
|
<input
|
|
278
346
|
type="text"
|
|
279
347
|
value={licenseKey}
|
|
@@ -281,16 +349,12 @@ export function LicenseActivation({ onActivated }: Props) {
|
|
|
281
349
|
placeholder="XXXX-XXXX-XXXX-XXXX"
|
|
282
350
|
disabled={loading}
|
|
283
351
|
/>
|
|
284
|
-
|
|
352
|
+
|
|
285
353
|
{error && <p className="error">{error}</p>}
|
|
286
|
-
|
|
354
|
+
|
|
287
355
|
<button onClick={handleActivate} disabled={loading || !licenseKey}>
|
|
288
356
|
{loading ? 'Activating...' : 'Activate License'}
|
|
289
357
|
</button>
|
|
290
|
-
|
|
291
|
-
<a href="https://yoursite.com/purchase" target="_blank">
|
|
292
|
-
Don't have a license? Purchase here
|
|
293
|
-
</a>
|
|
294
358
|
</div>
|
|
295
359
|
);
|
|
296
360
|
}
|
|
@@ -300,7 +364,7 @@ export function LicenseActivation({ onActivated }: Props) {
|
|
|
300
364
|
|
|
301
365
|
## CLI Tool Integration
|
|
302
366
|
|
|
303
|
-
### Node.js CLI
|
|
367
|
+
### Node.js CLI example
|
|
304
368
|
|
|
305
369
|
```typescript
|
|
306
370
|
// src/license.ts
|
|
@@ -308,7 +372,18 @@ import Conf from 'conf';
|
|
|
308
372
|
import DodoPayments from 'dodopayments';
|
|
309
373
|
import { machineIdSync } from 'node-machine-id';
|
|
310
374
|
|
|
311
|
-
|
|
375
|
+
interface StoredLicense {
|
|
376
|
+
key: string;
|
|
377
|
+
instanceId: string;
|
|
378
|
+
machineId: string;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
interface CliStore {
|
|
382
|
+
license?: StoredLicense;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Typing the store keeps `config.get('license')` strongly typed at every call site.
|
|
386
|
+
const config = new Conf<CliStore>({ projectName: 'your-cli' });
|
|
312
387
|
const client = new DodoPayments();
|
|
313
388
|
|
|
314
389
|
export async function activate(licenseKey: string): Promise<void> {
|
|
@@ -329,9 +404,7 @@ export async function activate(licenseKey: string): Promise<void> {
|
|
|
329
404
|
|
|
330
405
|
console.log('License activated successfully!');
|
|
331
406
|
} catch (error: any) {
|
|
332
|
-
if (error.status ===
|
|
333
|
-
console.error('Invalid license key.');
|
|
334
|
-
} else if (error.status === 403) {
|
|
407
|
+
if (error.status === 422) {
|
|
335
408
|
console.error('Activation limit reached. Deactivate another device first.');
|
|
336
409
|
} else {
|
|
337
410
|
console.error('Activation failed:', error.message);
|
|
@@ -341,7 +414,7 @@ export async function activate(licenseKey: string): Promise<void> {
|
|
|
341
414
|
}
|
|
342
415
|
|
|
343
416
|
export async function checkLicense(): Promise<boolean> {
|
|
344
|
-
const license = config.get('license')
|
|
417
|
+
const license = config.get('license');
|
|
345
418
|
|
|
346
419
|
if (!license) {
|
|
347
420
|
return false;
|
|
@@ -359,7 +432,7 @@ export async function checkLicense(): Promise<boolean> {
|
|
|
359
432
|
}
|
|
360
433
|
|
|
361
434
|
export async function deactivate(): Promise<void> {
|
|
362
|
-
const license = config.get('license')
|
|
435
|
+
const license = config.get('license');
|
|
363
436
|
|
|
364
437
|
if (!license) {
|
|
365
438
|
console.log('No active license found.');
|
|
@@ -379,7 +452,6 @@ export async function deactivate(): Promise<void> {
|
|
|
379
452
|
}
|
|
380
453
|
}
|
|
381
454
|
|
|
382
|
-
// Middleware to check license before commands
|
|
383
455
|
export function requireLicense() {
|
|
384
456
|
return async () => {
|
|
385
457
|
const valid = await checkLicense();
|
|
@@ -392,7 +464,7 @@ export function requireLicense() {
|
|
|
392
464
|
}
|
|
393
465
|
```
|
|
394
466
|
|
|
395
|
-
### CLI
|
|
467
|
+
### CLI commands
|
|
396
468
|
|
|
397
469
|
```typescript
|
|
398
470
|
// src/cli.ts
|
|
@@ -419,7 +491,6 @@ program
|
|
|
419
491
|
console.log(valid ? 'License: Active' : 'License: Not activated');
|
|
420
492
|
});
|
|
421
493
|
|
|
422
|
-
// Protected command example
|
|
423
494
|
program
|
|
424
495
|
.command('generate')
|
|
425
496
|
.description('Generate something (requires license)')
|
|
@@ -435,150 +506,200 @@ program.parse();
|
|
|
435
506
|
|
|
436
507
|
## Webhook Integration
|
|
437
508
|
|
|
438
|
-
### Handle
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
// app/api/webhooks/dodo/route.ts
|
|
442
|
-
export async function POST(req: NextRequest) {
|
|
443
|
-
const event = await req.json();
|
|
444
|
-
|
|
445
|
-
if (event.type === 'license_key.created') {
|
|
446
|
-
const { id, key, product_id, customer_id, expires_at } = event.data;
|
|
447
|
-
|
|
448
|
-
// Store in your database
|
|
449
|
-
await prisma.license.create({
|
|
450
|
-
data: {
|
|
451
|
-
externalId: id,
|
|
452
|
-
key: key,
|
|
453
|
-
productId: product_id,
|
|
454
|
-
customerId: customer_id,
|
|
455
|
-
expiresAt: expires_at ? new Date(expires_at) : null,
|
|
456
|
-
status: 'active',
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// Optional: Send custom email with activation instructions
|
|
461
|
-
await sendLicenseEmail(customer_id, key, product_id);
|
|
462
|
-
}
|
|
509
|
+
### Handle license key delivery
|
|
463
510
|
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
---
|
|
511
|
+
When a product with licensing enabled is purchased, an `entitlement_grant.delivered` webhook fires with the license key details:
|
|
469
512
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
For sensitive operations, validate server-side with your API key:
|
|
513
|
+
Persist a local license-to-subscription association during fulfillment. Cancellation handling must query that association by `subscription_id`; filtering only by customer would also revoke keys for unrelated products or subscriptions.
|
|
473
514
|
|
|
474
515
|
```typescript
|
|
475
|
-
// app/api/
|
|
516
|
+
// app/api/webhooks/dodo/route.ts
|
|
476
517
|
import { NextRequest, NextResponse } from 'next/server';
|
|
477
518
|
import DodoPayments from 'dodopayments';
|
|
478
519
|
|
|
479
520
|
const client = new DodoPayments({
|
|
480
|
-
bearerToken: process.env.DODO_PAYMENTS_API_KEY
|
|
521
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
522
|
+
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
|
|
481
523
|
});
|
|
482
524
|
|
|
483
525
|
export async function POST(req: NextRequest) {
|
|
484
|
-
const { licenseKey } = await req.json();
|
|
485
|
-
|
|
486
526
|
try {
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
527
|
+
const body = await req.text();
|
|
528
|
+
const event = client.webhooks.unwrap(body, {
|
|
529
|
+
headers: {
|
|
530
|
+
'webhook-id': req.headers.get('webhook-id') || '',
|
|
531
|
+
'webhook-signature': req.headers.get('webhook-signature') || '',
|
|
532
|
+
'webhook-timestamp': req.headers.get('webhook-timestamp') || '',
|
|
533
|
+
},
|
|
490
534
|
});
|
|
491
535
|
|
|
492
|
-
if (
|
|
493
|
-
|
|
536
|
+
if (event.type === 'entitlement_grant.delivered') {
|
|
537
|
+
const { customer_id, id, license_key } = event.data;
|
|
538
|
+
|
|
539
|
+
// Delivered grants for other entitlement types do not include a license key.
|
|
540
|
+
if (!license_key) {
|
|
541
|
+
return NextResponse.json({ received: true });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Store in your database
|
|
545
|
+
await prisma.license.create({
|
|
546
|
+
data: {
|
|
547
|
+
externalId: id,
|
|
548
|
+
key: license_key.key,
|
|
549
|
+
customerId: customer_id,
|
|
550
|
+
expiresAt: license_key.expires_at ? new Date(license_key.expires_at) : null,
|
|
551
|
+
activationsLimit: license_key.activations_limit,
|
|
552
|
+
status: 'active',
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Send email with activation instructions
|
|
557
|
+
await sendLicenseEmail(customer_id, license_key.key);
|
|
494
558
|
}
|
|
495
559
|
|
|
496
|
-
|
|
560
|
+
if (event.type === 'subscription.cancelled' || event.type === 'subscription.expired') {
|
|
561
|
+
const { subscription_id, cancel_at_next_billing_date } = event.data;
|
|
497
562
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
563
|
+
// End-of-period cancellation keeps access active until the term expires.
|
|
564
|
+
if (event.type === 'subscription.cancelled' && cancel_at_next_billing_date) {
|
|
565
|
+
return NextResponse.json({ received: true });
|
|
566
|
+
}
|
|
502
567
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
568
|
+
// Query your persisted license-to-subscription mapping.
|
|
569
|
+
const licenseKeyIds = await getLicenseKeyIdsForSubscription(subscription_id);
|
|
570
|
+
|
|
571
|
+
for (const licenseKeyId of licenseKeyIds) {
|
|
572
|
+
await client.licenseKeys.update(licenseKeyId, {
|
|
573
|
+
disabled: true,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return NextResponse.json({ received: true });
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error('Webhook error:', error);
|
|
581
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
|
|
512
582
|
}
|
|
513
583
|
}
|
|
514
584
|
```
|
|
515
585
|
|
|
586
|
+
**Webhook events:**
|
|
587
|
+
|
|
588
|
+
- `entitlement_grant.delivered` — license key issued (current, recommended)
|
|
589
|
+
- `entitlement_grant.revoked` — license key revoked
|
|
590
|
+
- `license_key.created` — legacy event (still fires, but use `entitlement_grant.*` for new integrations)
|
|
591
|
+
- `subscription.cancelled` — disable only this subscription's keys for immediate cancellation
|
|
592
|
+
- `subscription.expired` — disable this subscription's keys when its term ends
|
|
593
|
+
|
|
516
594
|
---
|
|
517
595
|
|
|
518
|
-
##
|
|
596
|
+
## Common Mistakes
|
|
519
597
|
|
|
520
|
-
### 1.
|
|
521
|
-
Choose sensible defaults for expiry and activations based on your product type.
|
|
598
|
+
### 1. Validating only at install time
|
|
522
599
|
|
|
523
|
-
|
|
524
|
-
Provide precise activation instructions:
|
|
525
|
-
- "Paste the key in Settings → License"
|
|
526
|
-
- "Run: `mycli activate <key>`"
|
|
527
|
-
- Include self-serve documentation links
|
|
600
|
+
Don't validate once and trust forever. Validate periodically (e.g., weekly) to catch revoked or expired keys.
|
|
528
601
|
|
|
529
|
-
|
|
530
|
-
|
|
602
|
+
```typescript
|
|
603
|
+
// Bad: validate once
|
|
604
|
+
if (await validateLicense(key)) {
|
|
605
|
+
store.set('trusted', true);
|
|
606
|
+
}
|
|
531
607
|
|
|
532
|
-
|
|
533
|
-
|
|
608
|
+
// Good: validate on each startup
|
|
609
|
+
const isValid = await validateLicense(key);
|
|
610
|
+
if (!isValid) {
|
|
611
|
+
// Fail closed
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
```
|
|
534
615
|
|
|
535
|
-
###
|
|
536
|
-
Use webhooks to detect abuse patterns and automate revocations.
|
|
616
|
+
### 2. Not handling activation-limit errors
|
|
537
617
|
|
|
538
|
-
|
|
539
|
-
Let users deactivate devices themselves to manage their activation slots.
|
|
618
|
+
When a user hits the activation limit, they need a clear path to deactivate an old device.
|
|
540
619
|
|
|
541
|
-
|
|
620
|
+
```typescript
|
|
621
|
+
try {
|
|
622
|
+
await client.licenses.activate({ license_key: key, name: deviceName });
|
|
623
|
+
} catch (error: any) {
|
|
624
|
+
if (error.status === 422) {
|
|
625
|
+
// Show UI: "You've reached your activation limit. Deactivate a device first."
|
|
626
|
+
// Provide a list of active instances so they can choose which to remove.
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
```
|
|
542
630
|
|
|
543
|
-
|
|
631
|
+
### 3. Trusting client-side validation alone
|
|
544
632
|
|
|
545
|
-
|
|
633
|
+
Always validate on your server before granting access to sensitive features.
|
|
546
634
|
|
|
547
635
|
```typescript
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
// Map features to license tiers
|
|
554
|
-
const featureTiers = {
|
|
555
|
-
'basic-export': ['starter', 'pro', 'enterprise'],
|
|
556
|
-
'advanced-export': ['pro', 'enterprise'],
|
|
557
|
-
'api-access': ['enterprise'],
|
|
558
|
-
};
|
|
636
|
+
// Bad: trust the client
|
|
637
|
+
if (localStorage.getItem('license_valid')) {
|
|
638
|
+
showPremiumFeature();
|
|
639
|
+
}
|
|
559
640
|
|
|
560
|
-
|
|
561
|
-
|
|
641
|
+
// Good: validate server-side
|
|
642
|
+
const response = await fetch('/api/validate-license', {
|
|
643
|
+
method: 'POST',
|
|
644
|
+
body: JSON.stringify({ licenseKey }),
|
|
645
|
+
});
|
|
646
|
+
const { valid } = await response.json();
|
|
647
|
+
if (valid) {
|
|
648
|
+
showPremiumFeature();
|
|
562
649
|
}
|
|
563
650
|
```
|
|
564
651
|
|
|
565
|
-
###
|
|
652
|
+
### 4. Hardcoding license keys
|
|
566
653
|
|
|
567
|
-
|
|
654
|
+
Never embed keys in client code or version control.
|
|
568
655
|
|
|
569
656
|
```typescript
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
657
|
+
// Bad
|
|
658
|
+
const LICENSE_KEY = 'PRO-AAAA-BBBB-CCCC-DDDD';
|
|
659
|
+
|
|
660
|
+
// Good
|
|
661
|
+
const licenseKey = process.env.DODO_LICENSE_KEY;
|
|
662
|
+
// or read from user input / secure storage
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 5. Not storing the instance ID
|
|
666
|
+
|
|
667
|
+
The instance ID is required for deactivation. Store it alongside the key.
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// Bad: only store the key
|
|
671
|
+
store.set('license_key', key);
|
|
672
|
+
|
|
673
|
+
// Good: store both
|
|
674
|
+
store.set('license', {
|
|
675
|
+
key,
|
|
676
|
+
instanceId: response.id,
|
|
677
|
+
activatedAt: new Date().toISOString(),
|
|
678
|
+
});
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### 6. Failing open on validation errors
|
|
682
|
+
|
|
683
|
+
If validation fails (network error, server down), fail closed. Don't grant access.
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// Bad: assume valid if offline
|
|
687
|
+
try {
|
|
688
|
+
const valid = await validateLicense(key);
|
|
689
|
+
return valid;
|
|
690
|
+
} catch {
|
|
691
|
+
return true; // WRONG: grants access on error
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Good: fail closed, with optional grace period
|
|
695
|
+
try {
|
|
696
|
+
const valid = await validateLicense(key);
|
|
697
|
+
return valid;
|
|
698
|
+
} catch {
|
|
699
|
+
// Only trust local cache if within grace period
|
|
700
|
+
const lastValidated = store.get('last_validated_at');
|
|
701
|
+
const daysSince = (Date.now() - lastValidated) / (1000 * 60 * 60 * 24);
|
|
702
|
+
return daysSince < 30;
|
|
582
703
|
}
|
|
583
704
|
```
|
|
584
705
|
|
|
@@ -586,6 +707,10 @@ if (event.type === 'subscription.cancelled') {
|
|
|
586
707
|
|
|
587
708
|
## Resources
|
|
588
709
|
|
|
589
|
-
- [License Keys
|
|
590
|
-
- [API
|
|
591
|
-
- [
|
|
710
|
+
- [License Keys Guide](https://docs.dodopayments.com/features/license-keys)
|
|
711
|
+
- [Activate License API](https://docs.dodopayments.com/api-reference/licenses/activate-license)
|
|
712
|
+
- [Validate License API](https://docs.dodopayments.com/api-reference/licenses/validate-license)
|
|
713
|
+
- [Deactivate License API](https://docs.dodopayments.com/api-reference/licenses/deactivate-license)
|
|
714
|
+
- [License Key Instances API](https://docs.dodopayments.com/api-reference/licenses/get-license-key-instances)
|
|
715
|
+
- [Entitlement Grant Webhooks](https://docs.dodopayments.com/developer-resources/webhooks/intents/entitlement-grant)
|
|
716
|
+
- [Webhook Verification](https://docs.dodopayments.com/developer-resources/webhooks/verification)
|