@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
@@ -0,0 +1,82 @@
1
+ # Dodo Payments Subscription Integration: Webhook Events
2
+
3
+
4
+ | Event | When | Action |
5
+ |-------|------|--------|
6
+ | `subscription.active` | Subscription becomes active, including a trial start or recovery | Grant access |
7
+ | `subscription.updated` | Any subscription field changes | Sync your stored copy (status, dates, metadata) |
8
+ | `subscription.renewed` | Successful renewal | Log renewal, send receipt |
9
+ | `subscription.past_due` | Renewal failed and the grace period opened (payload has `past_due_ends_at`) | Keep access, prompt for payment before the deadline |
10
+ | `subscription.on_hold` | Renewal/plan-change payment failed | Notify customer, offer recovery |
11
+ | `subscription.paused` | Subscription paused | Revoke access while paused |
12
+ | `subscription.unpaused` | Paused subscription resumed | Restore access |
13
+ | `subscription.plan_changed` | Plan upgraded/downgraded or add-ons changed | Update entitlements |
14
+ | `subscription.update_payment_method` | Payment method updated | Update stored payment details |
15
+ | `subscription.cancelled` | Customer cancels | Schedule access revocation per `cancel_at_next_billing_date` |
16
+ | `subscription.failed` | Initial creation failed (mandate creation) | Notify customer, offer a new subscription |
17
+ | `subscription.expired` | Subscription term ended | Revoke access |
18
+
19
+ Webhook signature verification, raw-body handling, durable processing, and idempotency are covered in the `webhook-integration` skill.
20
+
21
+ ### Example Handler
22
+
23
+ ```typescript
24
+ import { NextRequest, NextResponse } from 'next/server';
25
+
26
+ export async function POST(req: NextRequest) {
27
+ const raw = await req.text();
28
+ const headers = Object.fromEntries(req.headers.entries());
29
+ const event = await client.webhooks.unwrap(raw, { headers });
30
+ const webhookId = req.headers.get('webhook-id');
31
+
32
+ if (!webhookId) {
33
+ return NextResponse.json({ error: 'Missing webhook-id' }, { status: 400 });
34
+ }
35
+
36
+ // Implement this as an atomic insert backed by a UNIQUE constraint.
37
+ // Keep the claim and entitlement changes in the same database transaction.
38
+ const claimed = await claimWebhookId(webhookId);
39
+ if (!claimed) {
40
+ return NextResponse.json({ received: true });
41
+ }
42
+
43
+ switch (event.type) {
44
+ case 'subscription.active':
45
+ await grantAccess(event.data.customer.customer_id, event.data.product_id);
46
+ break;
47
+ case 'subscription.on_hold':
48
+ await notifyPaymentFailed(event.data.customer.customer_id);
49
+ break;
50
+ case 'subscription.paused':
51
+ case 'subscription.unpaused': {
52
+ // Pause and resume can happen in quick succession, and handlers for
53
+ // different events can finish in any order. Don't trust the event type:
54
+ // re-read the subscription and apply its CURRENT status, scoped to this
55
+ // subscription (the customer may hold other active ones). Run this under
56
+ // a per-subscription lock if your handlers execute concurrently.
57
+ const current = await client.subscriptions.retrieve(event.data.subscription_id);
58
+ if (current.status === 'paused') {
59
+ await suspendSubscriptionAccess(current.subscription_id);
60
+ } else if (current.status === 'active' || current.status === 'past_due') {
61
+ // past_due keeps access during the payment grace period
62
+ await restoreSubscriptionAccess(current.subscription_id);
63
+ }
64
+ break;
65
+ }
66
+ case 'subscription.cancelled':
67
+ if (event.data.cancel_at_next_billing_date) {
68
+ await scheduleAccessRevocation(event.data.subscription_id, new Date(event.data.next_billing_date));
69
+ } else {
70
+ await revokeAccessImmediately(event.data.subscription_id);
71
+ }
72
+ break;
73
+ case 'subscription.expired':
74
+ await revokeAccess(event.data.customer.customer_id);
75
+ break;
76
+ }
77
+
78
+ return NextResponse.json({ received: true });
79
+ }
80
+ ```
81
+
82
+ ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: testing-and-go-live
3
- description: Guide for test-mode payment scenarios, renewal simulation, local webhook delivery tests, test and live catalog migration, and production launch checks.
3
+ description: Dodo Payments test mode and production launch, covering test cards and payment methods, success and decline scenarios, renewal simulation, local webhook testing with the CLI, copying products from test to live, and a go-live checklist. Use when verifying payment flows, switching from test_mode to live_mode, or preparing to launch.
4
4
  ---
5
5
 
6
6
  # Testing and Go-Live
@@ -44,6 +44,8 @@ const client = new DodoPayments({
44
44
  ```
45
45
 
46
46
  ```python
47
+ import os
48
+
47
49
  from dodopayments import DodoPayments
48
50
 
49
51
  client = DodoPayments(
@@ -67,31 +69,34 @@ Get your test API key from the Dodo dashboard. Test keys are prefixed `dodo_test
67
69
 
68
70
  ### Card success and decline scenarios
69
71
 
70
- Use these card numbers in test mode. The expiry can be any future date; the CVC can be any three digits.
72
+ Use these card numbers in test mode with expiry **06/32** (or **12/34**) and CVV **123**.
71
73
 
72
74
  | Scenario | Card Number | Result |
73
75
  |---|---|---|
74
76
  | Success | `4242 4242 4242 4242` | Payment succeeds |
75
77
  | Decline | `4000 0000 0000 0002` | Payment declined |
76
78
  | Decline (insufficient funds) | `4000 0000 0000 9995` | Insufficient funds error |
77
- | Decline (lost card) | `4000 0000 0000 9987` | Lost card error |
79
+
80
+ Only use cards listed on the [Testing Process](https://docs.dodopayments.com/miscellaneous/testing-process) page; other Stripe-style test numbers (for example "lost card" variants) are not documented for Dodo.
78
81
 
79
82
  ### Subscription renewal failure
80
83
 
81
84
  To test subscription renewal failures, use this card:
82
85
 
83
- | Card Number | Behavior |
84
- |---|---|
85
- | `4000 0000 0000 0069` | Renewal fails on next billing date |
86
+ | Card Number | Expiry | CVV | Behavior |
87
+ |---|---|---|---|
88
+ | `4000 0000 0000 0341` | 12/34 | 123 | Declined at the subscription's next charge (renewal, upgrade, or downgrade) |
89
+
90
+ Create the subscription with a success card first, then switch its payment method to this card in the Customer Portal.
86
91
 
87
92
  ### UPI (India)
88
93
 
89
- Test UPI success and failure with these VPAs:
94
+ Test UPI success and failure with these VPAs. The billing country must be `IN` and the currency `INR` (non-Indian merchants also need Adaptive Currency enabled):
90
95
 
91
96
  | VPA | Result |
92
97
  |---|---|
93
- | `success@okhdfcbank` | Payment succeeds |
94
- | `failure@okhdfcbank` | Payment fails |
98
+ | `success@upi` | Payment succeeds |
99
+ | `failure@upi` | Payment fails |
95
100
 
96
101
  ### BNPL, wallets, and regional methods
97
102
 
@@ -162,7 +167,9 @@ dodo wh trigger payment.success http://localhost:3000/webhook
162
167
 
163
168
  Sends a realistic mock payload for a chosen event. It runs offline and works even while logged out — which is the tell for the part that matters: **these payloads are unsigned.** `unwrap()` rejects them because there is no valid signature to verify.
164
169
 
165
- Use `unsafeUnwrap()` for triggered events only, and never on an endpoint that also receives real traffic. Event names are `<category>.<event>`, for example `payment.success`, `subscription.active`, or `dispute.opened`.
170
+ Use `unsafeUnwrap()` for triggered events only, and never on an endpoint that also receives real traffic. Trigger names are `<category>.<event>`, for example `payment.success`, `subscription.active`, or `dispute.opened`.
171
+
172
+ **Trigger names are not always the payload `type`.** `payment.success` delivers `type: "payment.succeeded"`, `refund.success` delivers `refund.succeeded`, and `licence.created` delivers `license_key.created`. Switch on the payload types in your handler; a `case 'payment.success'` never matches.
166
173
 
167
174
  As with `listen`, both arguments are required in direct mode; `/wh trigger` inside the TUI opens a wizard instead.
168
175
 
@@ -184,19 +191,19 @@ Products are environment-specific. You cannot reuse a test product ID in product
184
191
 
185
192
  **Programmatic approach:**
186
193
 
187
- Fetch the product from test mode, extract its configuration, and create it in live mode:
194
+ Fetch the product from test mode, extract its configuration, and create it in live mode. API keys are per mode — a test key does not authenticate against live mode and vice versa — so this needs two distinct keys:
188
195
 
189
196
  ```typescript
190
197
  // In test mode
191
198
  const testClient = new DodoPayments({
192
- bearerToken: process.env.DODO_PAYMENTS_API_KEY,
199
+ bearerToken: process.env.DODO_PAYMENTS_TEST_API_KEY,
193
200
  environment: 'test_mode',
194
201
  });
195
202
  const testProduct = await testClient.products.retrieve('pdt_test_123');
196
203
 
197
204
  // In live mode
198
205
  const liveClient = new DodoPayments({
199
- bearerToken: process.env.DODO_PAYMENTS_API_KEY,
206
+ bearerToken: process.env.DODO_PAYMENTS_LIVE_API_KEY,
200
207
  environment: 'live_mode',
201
208
  });
202
209
  const liveProduct = await liveClient.products.create({
@@ -331,5 +338,5 @@ Test keys (prefixed `dodo_test_`) only work with `https://test.dodopayments.com`
331
338
 
332
339
  - [Testing Process](https://docs.dodopayments.com/miscellaneous/testing-process)
333
340
  - [Test vs. Live Mode](https://docs.dodopayments.com/miscellaneous/test-mode-vs-live-mode)
334
- - [Webhook Integration](https://docs.dodopayments.com/developer-resources/webhooks/intents/introduction)
335
- - [Dodo CLI](https://docs.dodopayments.com/developer-resources/cli)
341
+ - [Webhook Integration](https://docs.dodopayments.com/developer-resources/webhooks)
342
+ - [Dodo CLI](https://docs.dodopayments.com/developer-resources/sdks/cli)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: usage-based-billing
3
- description: Guide for charging directly per measured API call, token, storage unit, or other consumption using meters, stable usage events, aggregation, free thresholds, and metered subscriptions.
3
+ description: Dodo Payments usage-based billing that charges per measured unit, covering meters, usage event ingestion by event_name, aggregation, free thresholds, price_per_unit, and metered subscriptions. Use when billing per API call, AI token, storage, or bandwidth, sending usage events, or adding metered pricing to a plan; use credit-based-billing for prepaid credit pools.
4
4
  ---
5
5
 
6
6
  # Dodo Payments Usage-Based Billing
@@ -181,7 +181,7 @@ async function trackBatchUsage(
181
181
  customerId: string;
182
182
  eventName: string;
183
183
  occurredAt: string;
184
- metadata: Record<string, string>;
184
+ metadata: Record<string, string | number | boolean>;
185
185
  }>
186
186
  ) {
187
187
  const formattedEvents = events.map((event) => ({
@@ -235,135 +235,29 @@ The `product-catalog-management` skill is the canonical source for the complete
235
235
 
236
236
  ## Instrumenting Your Application
237
237
 
238
- ### Track API Calls
238
+ Full guide: [references/instrumentation.md](references/instrumentation.md).
239
239
 
240
- Persist the event before reporting the operation as complete, then ingest it from a retrying worker. The outbox or queue implementation must durably store the payload before `persist` resolves.
240
+ Covers:
241
241
 
242
- ```typescript
243
- type PersistedUsageEvent = {
244
- event_id: string;
245
- customer_id: string;
246
- event_name: string;
247
- timestamp: string;
248
- metadata: Record<string, string | number | boolean>;
249
- };
250
-
251
- interface UsageOutbox {
252
- persist(event: PersistedUsageEvent): Promise<void>;
253
- nextBatch(limit: number): Promise<PersistedUsageEvent[]>;
254
- markIngested(eventIds: string[]): Promise<void>;
255
- }
256
-
257
- async function completeApiOperation(
258
- outbox: UsageOutbox,
259
- operationId: string,
260
- customerId: string,
261
- occurredAt: string,
262
- ): Promise<void> {
263
- await outbox.persist({
264
- event_id: `api-call:${operationId}`,
265
- customer_id: customerId,
266
- event_name: 'api.call',
267
- timestamp: occurredAt,
268
- metadata: { endpoint: '/v1/users', method: 'GET', status: 200 },
269
- });
270
- }
271
-
272
- async function ingestUsageOutbox(outbox: UsageOutbox): Promise<void> {
273
- const events = await outbox.nextBatch(1000);
274
- if (events.length === 0) return;
275
-
276
- await client.usageEvents.ingest({ events });
277
- await outbox.markIngested(events.map((event) => event.event_id));
278
- }
279
- ```
280
-
281
- If the worker crashes after Dodo accepts the batch but before `markIngested`, retry the same persisted events with the same IDs. Dodo ignores the already-ingested IDs.
282
-
283
- ### Track AI Token Usage
284
-
285
- ```typescript
286
- async function callAI(
287
- customerId: string,
288
- generationId: string,
289
- prompt: string,
290
- completedAt: string,
291
- ) {
292
- const response = await openai.chat.completions.create({
293
- model: 'gpt-4',
294
- messages: [{ role: 'user', content: prompt }],
295
- });
296
-
297
- // Track tokens after completion
298
- await client.usageEvents.ingest({
299
- events: [{
300
- event_id: `generation:${generationId}`,
301
- customer_id: customerId,
302
- event_name: 'ai.tokens',
303
- timestamp: completedAt,
304
- metadata: {
305
- tokens: response.usage.total_tokens.toString(),
306
- prompt_tokens: response.usage.prompt_tokens.toString(),
307
- completion_tokens: response.usage.completion_tokens.toString(),
308
- model: 'gpt-4',
309
- }
310
- }]
311
- });
312
-
313
- return response;
314
- }
315
- ```
316
-
317
- ### Track Storage Usage
318
-
319
- For snapshot-based metrics (current state), use the `last` aggregation:
320
-
321
- ```typescript
322
- async function updateStorageUsage(
323
- customerId: string,
324
- snapshotId: string,
325
- bytesUsed: number,
326
- capturedAt: string,
327
- ) {
328
- await client.usageEvents.ingest({
329
- events: [{
330
- event_id: `storage-snapshot:${snapshotId}`,
331
- customer_id: customerId,
332
- event_name: 'storage.snapshot',
333
- timestamp: capturedAt,
334
- metadata: {
335
- bytes: bytesUsed.toString(),
336
- gb: (bytesUsed / 1024 / 1024 / 1024).toFixed(2),
337
- }
338
- }]
339
- });
340
- }
341
-
342
- // Call periodically or after storage changes
343
- await updateStorageUsage(
344
- 'cus_abc',
345
- 'snapshot_01K1M4D2K9',
346
- 5368709120,
347
- '2026-08-01T10:30:00Z',
348
- ); // 5GB
349
- ```
350
-
351
- ---
242
+ - Track API Calls
243
+ - Track AI Token Usage
244
+ - Track Storage Usage
352
245
 
353
246
  ## Querying Usage for Display
354
247
 
355
248
  ### Retrieve Usage History
356
249
 
357
250
  ```typescript
358
- const usage = await client.subscriptions.retrieveUsageHistory(
359
- 'sub_abc123',
360
- { page_size: 100 }
361
- );
362
-
363
- console.log(usage.items); // Array of billing-period usage records
251
+ // Auto-paginates across ALL billing periods; narrow with start_date/end_date/meter_id.
252
+ for await (const period of client.subscriptions.retrieveUsageHistory('sub_abc123', {
253
+ start_date: '2025-01-01T00:00:00Z',
254
+ page_size: 20,
255
+ })) {
256
+ console.log(period); // one record per billing period, with per-meter usage
257
+ }
364
258
  ```
365
259
 
366
- This returns aggregated usage per meter for the subscription's current billing period.
260
+ This is paginated usage history organized by billing period, across the subscription's lifetime - not just the current period. Filter with `start_date`, `end_date`, and `meter_id`, and iterate pages (or `for await`) rather than reading only the first page.
367
261
 
368
262
  ---
369
263
 
@@ -378,7 +272,7 @@ To link a meter to credits:
378
272
  3. On the meter, enable **Bill usage in Credits**.
379
273
  4. Set `credit_entitlement_id` and `meter_units_per_credit` (e.g., 1,000 tokens = 1 credit).
380
274
 
381
- Usage under the free threshold is excluded. Approximately every minute, a background worker aggregates new usage, converts it using the meter-to-credit ratio, and consumes the oldest non-expired credit grants (FIFO). When credits run out, configured overage behavior applies.
275
+ The free threshold does **not** apply to credit-billed meters: every unit counts toward credit deduction (it only applies when the meter bills in money). Approximately every minute, a background worker aggregates new usage, converts it using the meter-to-credit ratio, and deducts from non-expired grants, earliest-expiring grants consumed first. When credits run out, configured overage behavior applies.
382
276
 
383
277
  ---
384
278
 
@@ -0,0 +1,119 @@
1
+ # Dodo Payments Usage-Based Billing: Instrumenting Your Application
2
+
3
+
4
+ ### Track API Calls
5
+
6
+ Persist the event before reporting the operation as complete, then ingest it from a retrying worker. The outbox or queue implementation must durably store the payload before `persist` resolves.
7
+
8
+ ```typescript
9
+ type PersistedUsageEvent = {
10
+ event_id: string;
11
+ customer_id: string;
12
+ event_name: string;
13
+ timestamp: string;
14
+ metadata: Record<string, string | number | boolean>;
15
+ };
16
+
17
+ interface UsageOutbox {
18
+ persist(event: PersistedUsageEvent): Promise<void>;
19
+ nextBatch(limit: number): Promise<PersistedUsageEvent[]>;
20
+ markIngested(eventIds: string[]): Promise<void>;
21
+ }
22
+
23
+ async function completeApiOperation(
24
+ outbox: UsageOutbox,
25
+ operationId: string,
26
+ customerId: string,
27
+ occurredAt: string,
28
+ ): Promise<void> {
29
+ await outbox.persist({
30
+ event_id: `api-call:${operationId}`,
31
+ customer_id: customerId,
32
+ event_name: 'api.call',
33
+ timestamp: occurredAt,
34
+ metadata: { endpoint: '/v1/users', method: 'GET', status: 200 },
35
+ });
36
+ }
37
+
38
+ async function ingestUsageOutbox(outbox: UsageOutbox): Promise<void> {
39
+ const events = await outbox.nextBatch(1000);
40
+ if (events.length === 0) return;
41
+
42
+ await client.usageEvents.ingest({ events });
43
+ await outbox.markIngested(events.map((event) => event.event_id));
44
+ }
45
+ ```
46
+
47
+ If the worker crashes after Dodo accepts the batch but before `markIngested`, retry the same persisted events with the same IDs. Dodo ignores the already-ingested IDs.
48
+
49
+ ### Track AI Token Usage
50
+
51
+ ```typescript
52
+ async function callAI(
53
+ customerId: string,
54
+ generationId: string,
55
+ prompt: string,
56
+ completedAt: string,
57
+ ) {
58
+ const response = await openai.chat.completions.create({
59
+ model: 'gpt-4',
60
+ messages: [{ role: 'user', content: prompt }],
61
+ });
62
+
63
+ // Track tokens after completion
64
+ await client.usageEvents.ingest({
65
+ events: [{
66
+ event_id: `generation:${generationId}`,
67
+ customer_id: customerId,
68
+ event_name: 'ai.tokens',
69
+ timestamp: completedAt,
70
+ // Aggregated properties (Sum/Max/Last) must be numbers, not strings.
71
+ metadata: {
72
+ tokens: response.usage.total_tokens,
73
+ prompt_tokens: response.usage.prompt_tokens,
74
+ completion_tokens: response.usage.completion_tokens,
75
+ model: 'gpt-4',
76
+ }
77
+ }]
78
+ });
79
+
80
+ return response;
81
+ }
82
+ ```
83
+
84
+ ### Track Storage Usage
85
+
86
+ For snapshot-based metrics (current state), use the `last` aggregation:
87
+
88
+ ```typescript
89
+ async function updateStorageUsage(
90
+ customerId: string,
91
+ snapshotId: string,
92
+ bytesUsed: number,
93
+ capturedAt: string,
94
+ ) {
95
+ await client.usageEvents.ingest({
96
+ events: [{
97
+ event_id: `storage-snapshot:${snapshotId}`,
98
+ customer_id: customerId,
99
+ event_name: 'storage.snapshot',
100
+ timestamp: capturedAt,
101
+ // Numeric values: a string property does not aggregate.
102
+ metadata: {
103
+ bytes: bytesUsed,
104
+ gb: Number((bytesUsed / 1024 / 1024 / 1024).toFixed(2)),
105
+ }
106
+ }]
107
+ });
108
+ }
109
+
110
+ // Call periodically or after storage changes
111
+ await updateStorageUsage(
112
+ 'cus_abc',
113
+ 'snapshot_01K1M4D2K9',
114
+ 5368709120,
115
+ '2026-08-01T10:30:00Z',
116
+ ); // 5GB
117
+ ```
118
+
119
+ ---