@dodopayments/opencode-plugin 0.6.0 → 0.6.2

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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/opencode-plugin/README.md +1 -1
  3. package/package.json +1 -1
  4. package/skills/better-auth-integration/SKILL.md +12 -3
  5. package/skills/billing-sdk/SKILL.md +15 -167
  6. package/skills/billing-sdk/references/pricing-page.md +197 -0
  7. package/skills/checkout-integration/SKILL.md +31 -460
  8. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  9. package/skills/checkout-integration/references/server-examples.md +317 -0
  10. package/skills/credit-based-billing/SKILL.md +11 -158
  11. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  12. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  13. package/skills/customer-management/SKILL.md +25 -17
  14. package/skills/discounts-and-promotions/SKILL.md +20 -17
  15. package/skills/dodo-best-practices/SKILL.md +18 -5
  16. package/skills/dodo-mcp-usage/SKILL.md +152 -0
  17. package/skills/framework-adapters/SKILL.md +64 -421
  18. package/skills/framework-adapters/references/astro.md +52 -0
  19. package/skills/framework-adapters/references/bun.md +45 -0
  20. package/skills/framework-adapters/references/convex.md +91 -0
  21. package/skills/framework-adapters/references/express.md +72 -0
  22. package/skills/framework-adapters/references/fastify.md +54 -0
  23. package/skills/framework-adapters/references/hono.md +63 -0
  24. package/skills/framework-adapters/references/nuxt.md +53 -0
  25. package/skills/framework-adapters/references/remix.md +65 -0
  26. package/skills/framework-adapters/references/sveltekit.md +49 -0
  27. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  28. package/skills/license-keys/SKILL.md +117 -367
  29. package/skills/license-keys/references/cli-tool.md +143 -0
  30. package/skills/license-keys/references/desktop-app.md +170 -0
  31. package/skills/localized-pricing/SKILL.md +51 -29
  32. package/skills/mobile-checkout/SKILL.md +72 -310
  33. package/skills/mobile-checkout/references/android.md +69 -0
  34. package/skills/mobile-checkout/references/flutter.md +97 -0
  35. package/skills/mobile-checkout/references/ios.md +66 -0
  36. package/skills/mobile-checkout/references/react-native.md +100 -0
  37. package/skills/product-catalog-management/SKILL.md +39 -16
  38. package/skills/refunds-and-disputes/SKILL.md +91 -71
  39. package/skills/subscription-integration/SKILL.md +43 -64
  40. package/skills/subscription-integration/references/webhook-events.md +82 -0
  41. package/skills/testing-and-go-live/SKILL.md +22 -15
  42. package/skills/usage-based-billing/SKILL.md +16 -122
  43. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  44. package/skills/webhook-integration/SKILL.md +31 -263
  45. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  46. package/skills/webhook-integration/references/event-catalog.md +103 -0
  47. package/skills/webhook-integration/references/local-testing.md +42 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: webhook-integration
3
- description: Complete guide for setting up and handling Dodo Payments webhooks for real-time payment event notifications.
3
+ description: Dodo Payments webhook handling, covering endpoint setup, Standard Webhooks signature verification on the raw body, payment, subscription, and refund event types, idempotency with webhook-id, retries, and local testing. Use when receiving Dodo events, verifying webhook-signature headers, or syncing your database from payment.succeeded or subscription events.
4
4
  ---
5
5
 
6
6
  # Dodo Payments Webhook Integration
@@ -272,91 +272,19 @@ Signature verification requires the exact bytes Dodo sent. If you parse the JSON
272
272
 
273
273
  ## Webhook Event Catalog
274
274
 
275
- Dodo sends 40+ event types across nine domains. Subscribe to only the events you need.
276
-
277
- ### Payment events
278
-
279
- | Event | When it fires | What to do |
280
- |-------|---|---|
281
- | `payment.succeeded` | Payment completed successfully | Grant access, send confirmation, update order status |
282
- | `payment.failed` | Payment attempt failed | Notify customer, suggest retry or alternative payment method |
283
- | `payment.processing` | Payment is still being processed | Acknowledge receipt, wait for `payment.succeeded` or `payment.failed` |
284
- | `payment.cancelled` | Payment was cancelled before completion | Update order status, notify customer if applicable |
285
-
286
- ### Subscription events
287
-
288
- | Event | When it fires | What to do |
289
- |-------|---|---|
290
- | `subscription.active` | Subscription becomes active; recurring charges are scheduled | Grant subscription access, send welcome email |
291
- | `subscription.updated` | Any field on the subscription changes | Sync changes to your database |
292
- | `subscription.on_hold` | Failed renewal temporarily pauses the subscription | Notify customer, prompt payment method update |
293
- | `subscription.renewed` | Subscription amount successfully deducted for a billing period | Log renewal, update next billing date |
294
- | `subscription.plan_changed` | Plan upgraded, downgraded, or modified | Update customer's access level or feature set |
295
- | `subscription.update_payment_method` | Payment method is updated | Sync the new payment method to your records |
296
- | `subscription.cancelled` | Merchant or customer cancels the subscription | Revoke access, send cancellation confirmation |
297
- | `subscription.failed` | Subscription creation fails (mandate creation failed) | Notify customer, suggest alternative payment method |
298
- | `subscription.expired` | Subscription reaches the end of its term | Revoke access, offer renewal or upgrade |
299
-
300
- ### Refund events
301
-
302
- | Event | When it fires | What to do |
303
- |-------|---|---|
304
- | `refund.succeeded` | Refund successfully processed | Update order status, revoke access if applicable |
305
- | `refund.failed` | Refund processing fails | Alert team, investigate reason |
306
-
307
- ### Dispute events
308
-
309
- | Event | When it fires | What to do |
310
- |-------|---|---|
311
- | `dispute.opened` | Customer initiates a dispute | Alert team, prepare evidence |
312
- | `dispute.expired` | Dispute expires without resolution | Log outcome |
313
- | `dispute.accepted` | Merchant accepts the dispute | Process refund if not already done |
314
- | `dispute.cancelled` | Customer or system cancels the dispute | Log outcome |
315
- | `dispute.challenged` | Merchant challenges the dispute | Prepare additional evidence |
316
- | `dispute.won` | Merchant wins the dispute | Log outcome, retain funds |
317
- | `dispute.lost` | Merchant loses the dispute | Process refund, log outcome |
318
-
319
- ### License key events
320
-
321
- | Event | When it fires | What to do |
322
- |-------|---|---|
323
- | `license_key.created` | License key is generated | Send key to customer (legacy; prefer `entitlement_grant.delivered`) |
324
-
325
- ### Entitlement grant events
326
-
327
- | Event | When it fires | What to do |
328
- |-------|---|---|
329
- | `entitlement_grant.created` | Grant row is created | Prepare for fulfillment |
330
- | `entitlement_grant.delivered` | Fulfillment completes; customer receives access | Grant platform, file, or license-key access |
331
- | `entitlement_grant.failed` | Delivery fails and is no longer retried | Alert team, inspect `error_code` and `error_message` |
332
- | `entitlement_grant.revoked` | Access is withdrawn | Revoke customer access, inspect `revocation_reason` |
333
-
334
- ### Credit events
335
-
336
- These concern virtual credit entitlements, not monetary wallet balances.
337
-
338
- | Event | When it fires | What to do |
339
- |-------|---|---|
340
- | `credit.added` | Credits granted via subscription, purchase, add-on, or API | Update internal credit balance, log grant |
341
- | `credit.deducted` | Usage or manual debit consumes credits | Update internal credit balance |
342
- | `credit.expired` | Unused credits reach expiry | Log expiration, notify customer if applicable |
343
- | `credit.rolled_over` | Unused credits carried into a new grant | Update internal balance |
344
- | `credit.rollover_forfeited` | Credits forfeited at max rollover count | Log forfeiture |
345
- | `credit.overage_charged` | Overage charged after usage exceeds balance | Update internal balance, notify customer |
346
- | `credit.overage_reset` | Accumulated overage reset (e.g., new billing cycle) | Update internal balance |
347
- | `credit.manual_adjustment` | Manual credit or debit adjustment made | Update internal balance, log adjustment |
348
- | `credit.balance_low` | Balance falls below configured threshold | Notify customer, suggest purchase |
349
-
350
- ### Recovery and dunning events
351
-
352
- | Event | When it fires | What to do |
353
- |-------|---|---|
354
- | `abandoned_checkout.detected` | Failed or incomplete checkout classified as abandoned (after 60 min) | Monitor recovery link usage |
355
- | `abandoned_checkout.recovered` | Customer pays through recovery link | Log recovery, update order status |
356
- | `dunning.started` | Dunning attempt begins after subscription enters `on_hold` | Monitor dunning progress |
357
- | `dunning.recovered` | Customer updates payment method and charge succeeds | Reactivate subscription, send confirmation |
275
+ Full guide: [references/event-catalog.md](references/event-catalog.md).
358
276
 
359
- ---
277
+ Covers:
278
+
279
+ - Payment events
280
+ - Subscription events
281
+ - Refund events
282
+ - Dispute events
283
+ - License key events
284
+ - Entitlement grant events
285
+ - Credit events
286
+ - Recovery and dunning events
287
+ - Payout events
360
288
 
361
289
  ## Production-Grade Handler Pattern
362
290
 
@@ -464,7 +392,7 @@ Understand how Dodo delivers webhooks:
464
392
 
465
393
  | Property | Behavior |
466
394
  |----------|----------|
467
- | **Timeout** | 15 seconds for connection and read |
395
+ | **Timeout** | 30 seconds for connection and read |
468
396
  | **Success** | Any `2xx` response acknowledges delivery. Return `200` immediately after durably recording the event. |
469
397
  | **Failure** | Any non-`2xx` response triggers a retry. |
470
398
  | **Retries** | Eight attempts: immediately, 5s, 5m, 30m, 2h, 5h, 10h, 10h |
@@ -483,7 +411,7 @@ If you use a supported framework, use the official adaptor package for built-in
483
411
  |-----------|---------|---|
484
412
  | Next.js | `@dodopayments/nextjs` | `Webhooks({ webhookKey, onPayload })` |
485
413
  | Nuxt | `@dodopayments/nuxt` | `Webhooks({ webhookKey, onPayload })` |
486
- | Express | `@dodopayments/express` | Middleware with raw-body parser |
414
+ | Express | `@dodopayments/express` | `Webhooks({ webhookKey, onPayload })` - register `express.json()` first, **not** `express.raw()` (it verifies against the parsed `req.body`) |
487
415
  | Fastify | `@dodopayments/fastify` | `Webhooks({ webhookKey, onPayload })` |
488
416
  | Hono | `@dodopayments/hono` | `Webhooks({ webhookKey, onPayload })` |
489
417
  | Astro | `@dodopayments/astro` | `Webhooks({ webhookKey, onPayload })` |
@@ -534,189 +462,29 @@ Handle `subscription.active` only in a handler that exposes `webhook-id`, then c
534
462
 
535
463
  ## Local Testing
536
464
 
537
- ### Dashboard test tool
538
-
539
- 1. Go to **Developer → Webhooks → [your endpoint] → Testing**
540
- 2. Select an event type
541
- 3. Click **Send Example**
542
- 4. Verify your endpoint returns `200` and the signature verifies
543
-
544
- ### CLI: Live forwarding
545
-
546
- Forward real test-mode events to localhost:
547
-
548
- ```bash
549
- dodo wh listen http://localhost:3000/webhook
550
- ```
551
-
552
- This creates a test webhook, opens a WebSocket relay, and forwards events with valid signatures to your local URL. Requires a test-mode API key. The URL argument is required in direct mode — bare `dodo wh listen` only works as `/wh listen` inside the TUI.
465
+ Full guide: [references/local-testing.md](references/local-testing.md).
553
466
 
554
- ### CLI: Unsigned mock events
467
+ Covers:
555
468
 
556
- Generate realistic unsigned payloads for testing without signature verification:
557
-
558
- ```bash
559
- dodo wh trigger payment.success http://localhost:3000/webhook
560
- ```
561
-
562
- Use `unsafeUnwrap()` only for these unsigned payloads. Both arguments are required in direct mode.
563
-
564
- ### Tunnel
565
-
566
- Expose localhost with ngrok and register the HTTPS URL in the dashboard:
567
-
568
- ```bash
569
- ngrok http 3000
570
- # Register https://xxxx.ngrok.io/webhook in the dashboard
571
- ```
572
-
573
- ---
469
+ - Dashboard test tool
470
+ - CLI: Live forwarding
471
+ - CLI: Unsigned mock events
472
+ - Tunnel
574
473
 
575
474
  ## Common Mistakes
576
475
 
577
- ### 1. Signing only the payload or `timestamp.payload`
578
-
579
- **Wrong:**
580
- ```typescript
581
- const signed = crypto.createHmac('sha256', secret)
582
- .update(payload)
583
- .digest('base64');
584
-
585
- // or
586
- const signed = crypto.createHmac('sha256', secret)
587
- .update(`${timestamp}.${payload}`)
588
- .digest('base64');
589
- ```
590
-
591
- **Correct:** The signed message is `webhook-id.webhook-timestamp.raw_body`, all three parts joined by periods. Use the SDK helper to avoid this entirely.
592
-
593
- ### 2. Re-serializing the parsed body
594
-
595
- **Wrong:**
596
- ```typescript
597
- const body = await req.json();
598
- const signed = JSON.stringify(body); // Reordered, reformatted, breaks signature
599
- ```
600
-
601
- **Correct:** Always use the raw bytes:
602
- ```typescript
603
- const body = await req.text(); // or req.body.toString() in Express
604
- // Pass body directly to unwrap() or webhook.verify()
605
- ```
606
-
607
- ### 3. Naive comma-splitting of the signature header
608
-
609
- **Wrong:**
610
- ```typescript
611
- const sig = signature.split(',')[1]; // Assumes exactly one comma
612
- ```
476
+ Full guide: [references/common-mistakes.md](references/common-mistakes.md).
613
477
 
614
- The header is versioned and can contain multiple values. Use the SDK helper.
478
+ Covers:
615
479
 
616
- ### 4. `timingSafeEqual` throwing on length mismatch
617
-
618
- **Wrong:**
619
- ```typescript
620
- crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
621
- // Throws if lengths differ; doesn't return false
622
- ```
623
-
624
- **Correct:** Use the SDK helper, which handles this safely.
625
-
626
- ### 5. Granting access from `return_url` instead of verified webhooks
627
-
628
- **Wrong:**
629
- ```typescript
630
- // User visits return_url after checkout; you grant access
631
- app.get('/checkout/return', (req, res) => {
632
- grantAccess(req.query.customer_id); // Unverified!
633
- });
634
- ```
635
-
636
- **Correct:** Grant access only after receiving and verifying a webhook:
637
- ```typescript
638
- const event = client.webhooks.unwrap(rawBody, { headers });
639
-
640
- if (event.type === 'payment.succeeded') {
641
- grantAccess(event.data.customer.customer_id);
642
- }
643
- ```
644
-
645
- ### 6. Doing slow work before responding 2xx
646
-
647
- **Wrong:**
648
- ```typescript
649
- app.post('/webhook', async (req, res) => {
650
- const unwrapped = client.webhooks.unwrap(...);
651
-
652
- // Slow database writes, email sends, etc.
653
- await db.payment.create(...);
654
- await sendEmail(...);
655
-
656
- res.json({ received: true }); // Dodo times out after 15s
657
- });
658
- ```
659
-
660
- **Correct:** Durably enqueue first, then respond immediately. Return non-`2xx` if persistence fails so Dodo retries:
661
- ```typescript
662
- app.post('/webhook', async (req, res) => {
663
- const unwrapped = client.webhooks.unwrap(...);
664
-
665
- try {
666
- await queue.add('process-event', unwrapped); // Durable write
667
- return res.json({ received: true });
668
- } catch (error) {
669
- console.error('Failed to persist webhook', error);
670
- return res.status(503).json({ error: 'Webhook persistence failed' });
671
- }
672
- });
673
- ```
674
-
675
- ### 7. Acknowledging before durable persistence
676
-
677
- **Wrong:**
678
- ```typescript
679
- res.json({ received: true });
680
- await queue.add('process-event', unwrapped); // A crash can lose an acknowledged event
681
- ```
682
-
683
- **Correct:** Verify the signature, durably insert or enqueue the event, and only then return `2xx`. If persistence fails, return non-`2xx` so Dodo retries.
684
-
685
- ### 8. Committing an idempotency claim before fulfillment
686
-
687
- **Wrong:**
688
- ```typescript
689
- const claim = await db.webhookLog.createMany({
690
- data: [{ webhookId }],
691
- skipDuplicates: true,
692
- });
693
- if (claim.count === 0) return;
694
-
695
- await grantSubscriptionEntitlements(event.data); // A failure leaves the claim behind
696
- ```
697
-
698
- The retry sees the existing claim and skips fulfillment, permanently dropping the event.
699
-
700
- **Correct:** Commit the claim and all durable fulfillment changes in one transaction. A failure rolls both back, so the retry can claim the event again:
701
- ```typescript
702
- const event = client.webhooks.unwrap(rawBody, { headers });
703
-
704
- await db.$transaction(async (tx) => {
705
- const claim = await tx.webhookLog.createMany({
706
- data: [{ webhookId, eventType: event.type }],
707
- skipDuplicates: true,
708
- });
709
- if (claim.count === 0) return;
710
-
711
- if (event.type === 'subscription.active') {
712
- await grantSubscriptionEntitlements(event.data, tx);
713
- }
714
- });
715
- ```
716
-
717
- Use a transactional outbox for email or other external effects that must follow the database commit.
718
-
719
- ---
480
+ - Signing anything other than `webhook-id.webhook-timestamp.raw_body`
481
+ - Re-serializing the parsed body
482
+ - Naive comma-splitting of the signature header
483
+ - `timingSafeEqual` throwing on length mismatch
484
+ - Granting access from `return_url` instead of verified webhooks
485
+ - Doing slow work before responding 2xx
486
+ - Acknowledging before durable persistence
487
+ - Committing an idempotency claim before fulfillment
720
488
 
721
489
  ## Resources
722
490
 
@@ -0,0 +1,146 @@
1
+ # Dodo Payments Webhook Integration: Common Mistakes
2
+
3
+
4
+ ### 1. Signing only the payload or `timestamp.payload`
5
+
6
+ **Wrong:**
7
+ ```typescript
8
+ const signed = crypto.createHmac('sha256', secret)
9
+ .update(payload)
10
+ .digest('base64');
11
+
12
+ // or
13
+ const signed = crypto.createHmac('sha256', secret)
14
+ .update(`${timestamp}.${payload}`)
15
+ .digest('base64');
16
+ ```
17
+
18
+ **Correct:** The signed message is `webhook-id.webhook-timestamp.raw_body`, all three parts joined by periods. Use the SDK helper to avoid this entirely.
19
+
20
+ ### 2. Re-serializing the parsed body
21
+
22
+ **Wrong:**
23
+ ```typescript
24
+ const body = await req.json();
25
+ const signed = JSON.stringify(body); // Reordered, reformatted, breaks signature
26
+ ```
27
+
28
+ **Correct:** Always use the raw bytes:
29
+ ```typescript
30
+ const body = await req.text(); // or req.body.toString() in Express
31
+ // Pass body directly to unwrap() or webhook.verify()
32
+ ```
33
+
34
+ ### 3. Naive comma-splitting of the signature header
35
+
36
+ **Wrong:**
37
+ ```typescript
38
+ const sig = signature.split(',')[1]; // Assumes exactly one comma
39
+ ```
40
+
41
+ The header is versioned and can contain multiple values. Use the SDK helper.
42
+
43
+ ### 4. `timingSafeEqual` throwing on length mismatch
44
+
45
+ **Wrong:**
46
+ ```typescript
47
+ crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
48
+ // Throws if lengths differ; doesn't return false
49
+ ```
50
+
51
+ **Correct:** Use the SDK helper, which handles this safely.
52
+
53
+ ### 5. Granting access from `return_url` instead of verified webhooks
54
+
55
+ **Wrong:**
56
+ ```typescript
57
+ // User visits return_url after checkout; you grant access
58
+ app.get('/checkout/return', (req, res) => {
59
+ grantAccess(req.query.customer_id); // Unverified!
60
+ });
61
+ ```
62
+
63
+ **Correct:** Grant access only after receiving and verifying a webhook:
64
+ ```typescript
65
+ const event = client.webhooks.unwrap(rawBody, { headers });
66
+
67
+ if (event.type === 'payment.succeeded') {
68
+ grantAccess(event.data.customer.customer_id);
69
+ }
70
+ ```
71
+
72
+ ### 6. Doing slow work before responding 2xx
73
+
74
+ **Wrong:**
75
+ ```typescript
76
+ app.post('/webhook', async (req, res) => {
77
+ const unwrapped = client.webhooks.unwrap(...);
78
+
79
+ // Slow database writes, email sends, etc.
80
+ await db.payment.create(...);
81
+ await sendEmail(...);
82
+
83
+ res.json({ received: true }); // Dodo times out after 30s
84
+ });
85
+ ```
86
+
87
+ **Correct:** Durably enqueue first, then respond immediately. Return non-`2xx` if persistence fails so Dodo retries:
88
+ ```typescript
89
+ app.post('/webhook', async (req, res) => {
90
+ const unwrapped = client.webhooks.unwrap(...);
91
+
92
+ try {
93
+ await queue.add('process-event', unwrapped); // Durable write
94
+ return res.json({ received: true });
95
+ } catch (error) {
96
+ console.error('Failed to persist webhook', error);
97
+ return res.status(503).json({ error: 'Webhook persistence failed' });
98
+ }
99
+ });
100
+ ```
101
+
102
+ ### 7. Acknowledging before durable persistence
103
+
104
+ **Wrong:**
105
+ ```typescript
106
+ res.json({ received: true });
107
+ await queue.add('process-event', unwrapped); // A crash can lose an acknowledged event
108
+ ```
109
+
110
+ **Correct:** Verify the signature, durably insert or enqueue the event, and only then return `2xx`. If persistence fails, return non-`2xx` so Dodo retries.
111
+
112
+ ### 8. Committing an idempotency claim before fulfillment
113
+
114
+ **Wrong:**
115
+ ```typescript
116
+ const claim = await db.webhookLog.createMany({
117
+ data: [{ webhookId }],
118
+ skipDuplicates: true,
119
+ });
120
+ if (claim.count === 0) return;
121
+
122
+ await grantSubscriptionEntitlements(event.data); // A failure leaves the claim behind
123
+ ```
124
+
125
+ The retry sees the existing claim and skips fulfillment, permanently dropping the event.
126
+
127
+ **Correct:** Commit the claim and all durable fulfillment changes in one transaction. A failure rolls both back, so the retry can claim the event again:
128
+ ```typescript
129
+ const event = client.webhooks.unwrap(rawBody, { headers });
130
+
131
+ await db.$transaction(async (tx) => {
132
+ const claim = await tx.webhookLog.createMany({
133
+ data: [{ webhookId, eventType: event.type }],
134
+ skipDuplicates: true,
135
+ });
136
+ if (claim.count === 0) return;
137
+
138
+ if (event.type === 'subscription.active') {
139
+ await grantSubscriptionEntitlements(event.data, tx);
140
+ }
141
+ });
142
+ ```
143
+
144
+ Use a transactional outbox for email or other external effects that must follow the database commit.
145
+
146
+ ---
@@ -0,0 +1,103 @@
1
+ # Dodo Payments Webhook Integration: Webhook Event Catalog
2
+
3
+
4
+ Dodo delivers 48 event types across ten families (payment, subscription, refund, dispute, license key, entitlement grant, credit, abandoned checkout, dunning, payout). Subscribe to only the events you need.
5
+
6
+ ### Payment events
7
+
8
+ | Event | When it fires | What to do |
9
+ |-------|---|---|
10
+ | `payment.succeeded` | Payment completed successfully | Grant access, send confirmation, update order status |
11
+ | `payment.failed` | Payment attempt failed | Notify customer, suggest retry or alternative payment method |
12
+ | `payment.processing` | Payment is still being processed | Acknowledge receipt, wait for `payment.succeeded` or `payment.failed` |
13
+ | `payment.cancelled` | Payment was cancelled before completion | Update order status, notify customer if applicable |
14
+
15
+ ### Subscription events
16
+
17
+ | Event | When it fires | What to do |
18
+ |-------|---|---|
19
+ | `subscription.active` | Subscription becomes active; recurring charges are scheduled | Grant subscription access, send welcome email |
20
+ | `subscription.updated` | Any field on the subscription changes | Sync changes to your database |
21
+ | `subscription.past_due` | Renewal failed and the grace period opened; customer keeps access (payload has `past_due_ends_at`) | Prompt for payment before the deadline |
22
+ | `subscription.on_hold` | Failed renewal stops renewals and revokes access | Notify customer, prompt payment method update |
23
+ | `subscription.paused` | Subscription is deliberately paused | Suspend access while paused |
24
+ | `subscription.unpaused` | Paused subscription is resumed | Restore access |
25
+ | `subscription.renewed` | Subscription amount successfully deducted for a billing period | Log renewal, update next billing date |
26
+ | `subscription.plan_changed` | Plan upgraded, downgraded, or modified | Update customer's access level or feature set |
27
+ | `subscription.update_payment_method` | Payment method is updated | Sync the new payment method to your records |
28
+ | `subscription.cancelled` | Merchant or customer cancels the subscription | Revoke access, send cancellation confirmation |
29
+ | `subscription.failed` | Subscription creation fails (mandate creation failed) | Notify customer, suggest alternative payment method |
30
+ | `subscription.expired` | Subscription reaches the end of its term | Revoke access, offer renewal or upgrade |
31
+
32
+ ### Refund events
33
+
34
+ | Event | When it fires | What to do |
35
+ |-------|---|---|
36
+ | `refund.succeeded` | Refund successfully processed | Update order status, revoke access if applicable |
37
+ | `refund.failed` | Refund processing fails | Alert team, investigate reason |
38
+
39
+ ### Dispute events
40
+
41
+ | Event | When it fires | What to do |
42
+ |-------|---|---|
43
+ | `dispute.opened` | Customer initiates a dispute | Alert team, prepare evidence |
44
+ | `dispute.expired` | Dispute expires without resolution | Log outcome |
45
+ | `dispute.accepted` | Merchant accepts the dispute | Process refund if not already done |
46
+ | `dispute.cancelled` | Customer or system cancels the dispute | Log outcome |
47
+ | `dispute.challenged` | Merchant challenges the dispute | Prepare additional evidence |
48
+ | `dispute.won` | Merchant wins the dispute | Log outcome, retain funds |
49
+ | `dispute.lost` | Merchant loses the dispute | Process refund, log outcome |
50
+
51
+ ### License key events
52
+
53
+ | Event | When it fires | What to do |
54
+ |-------|---|---|
55
+ | `license_key.created` | License key is generated | Legacy; prefer `entitlement_grant.created` with `status: "Delivered"` |
56
+
57
+ ### Entitlement grant events
58
+
59
+ | Event | When it fires | What to do |
60
+ |-------|---|---|
61
+ | `entitlement_grant.created` | Grant row is created. Auto-fulfilled license keys arrive here with `status: "Delivered"` and a `license_key`; no `.delivered` follows | Grant access when `status` is `Delivered`; otherwise prepare for fulfillment |
62
+ | `entitlement_grant.delivered` | An existing grant moves to `Delivered` (manual fulfillment, or a revoked grant restored) | Grant platform, file, or license-key access |
63
+ | `entitlement_grant.failed` | Delivery fails and is no longer retried | Alert team, inspect `error_code` and `error_message` |
64
+ | `entitlement_grant.revoked` | Access is withdrawn | Revoke customer access, inspect `revocation_reason` |
65
+
66
+ ### Credit events
67
+
68
+ These concern virtual credit entitlements, not monetary wallet balances.
69
+
70
+ | Event | When it fires | What to do |
71
+ |-------|---|---|
72
+ | `credit.added` | Credits granted via subscription, purchase, add-on, or API | Update internal credit balance, log grant |
73
+ | `credit.deducted` | Usage or manual debit consumes credits | Update internal credit balance |
74
+ | `credit.expired` | Unused credits reach expiry | Log expiration, notify customer if applicable |
75
+ | `credit.rolled_over` | Unused credits carried into a new grant | Update internal balance |
76
+ | `credit.rollover_forfeited` | Credits forfeited at max rollover count | Log forfeiture |
77
+ | `credit.overage_charged` | Overage charged after usage exceeds balance | Update internal balance, notify customer |
78
+ | `credit.overage_reset` | Accumulated overage reset (e.g., new billing cycle) | Update internal balance |
79
+ | `credit.manual_adjustment` | Manual credit or debit adjustment made | Update internal balance, log adjustment |
80
+ | `credit.balance_low` | Balance falls below configured threshold | Notify customer, suggest purchase |
81
+
82
+ ### Recovery and dunning events
83
+
84
+ | Event | When it fires | What to do |
85
+ |-------|---|---|
86
+ | `abandoned_checkout.detected` | Failed or incomplete checkout classified as abandoned (after 60 min) | Monitor recovery link usage |
87
+ | `abandoned_checkout.recovered` | Customer pays through recovery link | Log recovery, update order status |
88
+ | `dunning.started` | Dunning attempt begins after subscription enters `on_hold` | Monitor dunning progress |
89
+ | `dunning.recovered` | Customer updates payment method and charge succeeds | Reactivate subscription, send confirmation |
90
+
91
+ ### Payout events
92
+
93
+ These concern your own funds moving to your bank account - use them for bookkeeping, not customer access.
94
+
95
+ | Event | When it fires | What to do |
96
+ |-------|---|---|
97
+ | `payout.created` | Payout is created (formerly emitted as `payout.not_initiated`) | Record the pending payout |
98
+ | `payout.in_progress` | Payout processing starts | Mark as in transit |
99
+ | `payout.on_hold` | Payout is paused or under review | Check whether more information is required |
100
+ | `payout.success` | Payout settles to your bank account | Reconcile in accounting |
101
+ | `payout.failed` | Payout fails; amount and fees return to your wallet | Alert finance, check bank details |
102
+
103
+ ---
@@ -0,0 +1,42 @@
1
+ # Dodo Payments Webhook Integration: Local Testing
2
+
3
+
4
+ ### Dashboard test tool
5
+
6
+ 1. Go to **Developer → Webhooks → [your endpoint] → Testing**
7
+ 2. Select an event type
8
+ 3. Click **Send Example**
9
+ 4. Verify your endpoint returns `200` and the signature verifies
10
+
11
+ ### CLI: Live forwarding
12
+
13
+ Forward real test-mode events to localhost:
14
+
15
+ ```bash
16
+ dodo wh listen http://localhost:3000/webhook
17
+ ```
18
+
19
+ This creates a test webhook, opens a WebSocket relay, and forwards events with the original `webhook-id`/`webhook-signature`/`webhook-timestamp` headers to your local URL. Requires a test-mode API key. The relay and CLI parse and **re-serialize** the JSON body, so if it differs byte-for-byte from the original (for example number formatting), signature verification fails even though the headers are intact - that is a relay artifact, not a bug in your verifier. The URL argument is required in direct mode — bare `dodo wh listen` only works as `/wh listen` inside the TUI.
20
+
21
+ ### CLI: Unsigned mock events
22
+
23
+ Generate realistic unsigned payloads for testing without signature verification:
24
+
25
+ ```bash
26
+ dodo wh trigger payment.success http://localhost:3000/webhook
27
+ ```
28
+
29
+ Use `unsafeUnwrap()` only for these unsigned payloads. Both arguments are required in direct mode.
30
+
31
+ Trigger names are not always the payload `type`: `payment.success` sends `type: "payment.succeeded"`, `refund.success` sends `refund.succeeded`, and `licence.created` sends `license_key.created`. Your handler must switch on the payload types. `subscription.past_due` and `subscription.unpaused` cannot be triggered.
32
+
33
+ ### Tunnel
34
+
35
+ Expose localhost with ngrok and register the HTTPS URL in the dashboard:
36
+
37
+ ```bash
38
+ ngrok http 3000
39
+ # Register https://xxxx.ngrok.io/webhook in the dashboard
40
+ ```
41
+
42
+ ---