@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.
@@ -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
- 4. Save the license key configuration
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
- ### Auto-Generation on Purchase
18
+ ## Core concepts
38
19
 
39
- License keys can be automatically generated when a product is purchased:
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
- 1. Configure your product with license key settings
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
- ## API Reference
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
- These endpoints can be called directly from client applications:
29
+ 2. **`client.licenseKeys.*`** — merchant-side key management (requires API key)
30
+ - `list()`, `retrieve()`, `create()`, `update()` — manage keys for your customers
52
31
 
53
- | Endpoint | Description |
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
- ### Authenticated Endpoints (API Key Required)
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
- | Endpoint | Description |
62
- |----------|-------------|
63
- | `GET /license_keys` | List all license keys |
64
- | `GET /license_keys/:id` | Get license key details |
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
- ## Implementation Examples
44
+ ## End-User Activation Flow
71
45
 
72
- ### Activate a License Key
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
- message: 'License activated successfully',
64
+ customerId: response.customer.customer_id,
65
+ productId: response.product.product_id,
91
66
  };
92
67
  } catch (error: any) {
93
- return {
94
- success: false,
95
- message: error.message || 'Activation failed',
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
- ### Validate a License Key
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
- ### Deactivate a License
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, message: 'License deactivated' };
114
+ return { success: true };
141
115
  } catch (error: any) {
142
- return { success: false, message: error.message };
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 App Example
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 (with optional grace period)
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 Component for License Input
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 Example
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
- const config = new Conf({ projectName: 'your-cli' });
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 === 400) {
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') as any;
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') as any;
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 Commands
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 License Key Creation
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
- return NextResponse.json({ received: true });
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
- ## Server-Side Validation
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/validate-license/route.ts
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
- // Get detailed license info (requires API key)
488
- const licenses = await client.licenseKeys.list({
489
- license_key: licenseKey,
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 (licenses.items.length === 0) {
493
- return NextResponse.json({ valid: false, error: 'License not found' });
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
- const license = licenses.items[0];
560
+ if (event.type === 'subscription.cancelled' || event.type === 'subscription.expired') {
561
+ const { subscription_id, cancel_at_next_billing_date } = event.data;
497
562
 
498
- // Check various conditions
499
- const valid =
500
- license.status === 'active' &&
501
- (!license.expires_at || new Date(license.expires_at) > new Date());
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
- return NextResponse.json({
504
- valid,
505
- status: license.status,
506
- activationsUsed: license.activations_count,
507
- activationsLimit: license.activations_limit,
508
- expiresAt: license.expires_at,
509
- });
510
- } catch (error: any) {
511
- return NextResponse.json({ valid: false, error: error.message }, { status: 500 });
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
- ## Best Practices
596
+ ## Common Mistakes
519
597
 
520
- ### 1. Keep Limits Clear
521
- Choose sensible defaults for expiry and activations based on your product type.
598
+ ### 1. Validating only at install time
522
599
 
523
- ### 2. Guide Users
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
- ### 3. Validate Server-Side
530
- For critical access control, always validate on your server before granting access.
602
+ ```typescript
603
+ // Bad: validate once
604
+ if (await validateLicense(key)) {
605
+ store.set('trusted', true);
606
+ }
531
607
 
532
- ### 4. Handle Offline Gracefully
533
- Allow a grace period for offline use in desktop/CLI apps.
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
- ### 5. Monitor Events
536
- Use webhooks to detect abuse patterns and automate revocations.
616
+ ### 2. Not handling activation-limit errors
537
617
 
538
- ### 6. Provide Easy Deactivation
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
- ## Common Patterns
631
+ ### 3. Trusting client-side validation alone
544
632
 
545
- ### Feature Gating
633
+ Always validate on your server before granting access to sensitive features.
546
634
 
547
635
  ```typescript
548
- async function canAccessFeature(feature: string, licenseKey: string) {
549
- const { valid } = await validateLicense(licenseKey);
550
-
551
- if (!valid) return false;
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
- const license = await getLicenseDetails(licenseKey);
561
- return featureTiers[feature]?.includes(license.tier);
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
- ### Subscription-Linked Licenses
652
+ ### 4. Hardcoding license keys
566
653
 
567
- When license is linked to a subscription:
654
+ Never embed keys in client code or version control.
568
655
 
569
656
  ```typescript
570
- // Handle subscription.cancelled webhook
571
- if (event.type === 'subscription.cancelled') {
572
- const { customer_id } = event.data;
573
-
574
- // Disable associated license keys
575
- const licenses = await client.licenseKeys.list({ customer_id });
576
-
577
- for (const license of licenses.items) {
578
- await client.licenseKeys.update(license.id, {
579
- status: 'disabled',
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 Documentation](https://docs.dodopayments.com/features/license-keys)
590
- - [API Reference](https://docs.dodopayments.com/api-reference/license-keys)
591
- - [Video Tutorial](https://www.youtube.com/watch?v=BNuLTXok8dQ)
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)