@dodopayments/opencode-plugin 0.2.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.
@@ -0,0 +1,571 @@
1
+ ---
2
+ name: subscription-integration
3
+ description: Guide for implementing subscription billing with Dodo Payments - trials, upgrades, downgrades, and on-demand billing.
4
+ ---
5
+
6
+ # Dodo Payments Subscription Integration
7
+
8
+ **Reference: [docs.dodopayments.com/developer-resources/subscription-integration-guide](https://docs.dodopayments.com/developer-resources/subscription-integration-guide)**
9
+
10
+ Implement recurring billing with trials, plan changes, and usage-based pricing.
11
+
12
+ ---
13
+
14
+ ## Quick Start
15
+
16
+ ### 1. Create Subscription Product
17
+ In the dashboard (Products → Create Product):
18
+ - Select "Subscription" type
19
+ - Set billing interval (monthly, yearly, etc.)
20
+ - Configure pricing
21
+
22
+ ### 2. Create Checkout Session
23
+
24
+ ```typescript
25
+ import DodoPayments from 'dodopayments';
26
+
27
+ const client = new DodoPayments({
28
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
29
+ });
30
+
31
+ const session = await client.checkoutSessions.create({
32
+ product_cart: [
33
+ { product_id: 'prod_monthly_plan', quantity: 1 }
34
+ ],
35
+ subscription_data: {
36
+ trial_period_days: 14, // Optional trial
37
+ },
38
+ customer: {
39
+ email: 'subscriber@example.com',
40
+ name: 'Jane Doe',
41
+ },
42
+ return_url: 'https://yoursite.com/success',
43
+ });
44
+
45
+ // Redirect to session.checkout_url
46
+ ```
47
+
48
+ ### 3. Handle Webhook Events
49
+ ```typescript
50
+ // subscription.active - Grant access
51
+ // subscription.cancelled - Schedule access revocation
52
+ // subscription.renewed - Log renewal
53
+ // payment.succeeded - Track payments
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Subscription Lifecycle
59
+
60
+ ```
61
+ ┌─────────────┐ ┌─────────┐ ┌────────┐
62
+ │ Created │ ──▶ │ Trial │ ──▶ │ Active │
63
+ └─────────────┘ └─────────┘ └────────┘
64
+ │
65
+ ┌────────────────────┼────────────────────┐
66
+ ▼ ▼ ▼
67
+ ┌──────────┐ ┌───────────┐ ┌───────────┐
68
+ │ On Hold │ │ Cancelled │ │ Renewed │
69
+ └──────────┘ └───────────┘ └───────────┘
70
+ │ │
71
+ ▼ ▼
72
+ ┌──────────┐ ┌───────────┐
73
+ │ Failed │ │ Expired │
74
+ └──────────┘ └───────────┘
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Webhook Events
80
+
81
+ | Event | When | Action |
82
+ |-------|------|--------|
83
+ | `subscription.active` | Subscription starts | Grant access |
84
+ | `subscription.updated` | Any field changes | Sync state |
85
+ | `subscription.on_hold` | Payment fails | Notify user, retry |
86
+ | `subscription.renewed` | Successful renewal | Log, send receipt |
87
+ | `subscription.plan_changed` | Upgrade/downgrade | Update entitlements |
88
+ | `subscription.cancelled` | User cancels | Schedule end of access |
89
+ | `subscription.failed` | Mandate creation fails | Notify, retry options |
90
+ | `subscription.expired` | Term ends | Revoke access |
91
+
92
+ ---
93
+
94
+ ## Implementation Examples
95
+
96
+ ### Full Subscription Handler
97
+
98
+ ```typescript
99
+ // app/api/webhooks/subscription/route.ts
100
+ import { NextRequest, NextResponse } from 'next/server';
101
+ import { prisma } from '@/lib/prisma';
102
+
103
+ export async function POST(req: NextRequest) {
104
+ const event = await req.json();
105
+ const data = event.data;
106
+
107
+ switch (event.type) {
108
+ case 'subscription.active':
109
+ await handleSubscriptionActive(data);
110
+ break;
111
+ case 'subscription.cancelled':
112
+ await handleSubscriptionCancelled(data);
113
+ break;
114
+ case 'subscription.on_hold':
115
+ await handleSubscriptionOnHold(data);
116
+ break;
117
+ case 'subscription.renewed':
118
+ await handleSubscriptionRenewed(data);
119
+ break;
120
+ case 'subscription.plan_changed':
121
+ await handlePlanChanged(data);
122
+ break;
123
+ case 'subscription.expired':
124
+ await handleSubscriptionExpired(data);
125
+ break;
126
+ }
127
+
128
+ return NextResponse.json({ received: true });
129
+ }
130
+
131
+ async function handleSubscriptionActive(data: any) {
132
+ const {
133
+ subscription_id,
134
+ customer,
135
+ product_id,
136
+ next_billing_date,
137
+ recurring_pre_tax_amount,
138
+ payment_frequency_interval,
139
+ } = data;
140
+
141
+ // Create or update user subscription
142
+ await prisma.subscription.upsert({
143
+ where: { externalId: subscription_id },
144
+ create: {
145
+ externalId: subscription_id,
146
+ userId: customer.customer_id,
147
+ email: customer.email,
148
+ productId: product_id,
149
+ status: 'active',
150
+ currentPeriodEnd: new Date(next_billing_date),
151
+ amount: recurring_pre_tax_amount,
152
+ interval: payment_frequency_interval,
153
+ },
154
+ update: {
155
+ status: 'active',
156
+ currentPeriodEnd: new Date(next_billing_date),
157
+ },
158
+ });
159
+
160
+ // Grant access
161
+ await prisma.user.update({
162
+ where: { id: customer.customer_id },
163
+ data: {
164
+ subscriptionStatus: 'active',
165
+ plan: product_id,
166
+ },
167
+ });
168
+
169
+ // Send welcome email
170
+ await sendWelcomeEmail(customer.email, product_id);
171
+ }
172
+
173
+ async function handleSubscriptionCancelled(data: any) {
174
+ const { subscription_id, customer, cancelled_at, cancel_at_next_billing_date } = data;
175
+
176
+ await prisma.subscription.update({
177
+ where: { externalId: subscription_id },
178
+ data: {
179
+ status: 'cancelled',
180
+ cancelledAt: new Date(cancelled_at),
181
+ // Keep access until end of billing period if cancel_at_next_billing_date
182
+ accessEndsAt: cancel_at_next_billing_date
183
+ ? new Date(data.next_billing_date)
184
+ : new Date(),
185
+ },
186
+ });
187
+
188
+ // Send cancellation email
189
+ await sendCancellationEmail(customer.email, cancel_at_next_billing_date);
190
+ }
191
+
192
+ async function handleSubscriptionOnHold(data: any) {
193
+ const { subscription_id, customer } = data;
194
+
195
+ await prisma.subscription.update({
196
+ where: { externalId: subscription_id },
197
+ data: { status: 'on_hold' },
198
+ });
199
+
200
+ // Notify user about payment issue
201
+ await sendPaymentFailedEmail(customer.email);
202
+ }
203
+
204
+ async function handleSubscriptionRenewed(data: any) {
205
+ const { subscription_id, next_billing_date } = data;
206
+
207
+ await prisma.subscription.update({
208
+ where: { externalId: subscription_id },
209
+ data: {
210
+ status: 'active',
211
+ currentPeriodEnd: new Date(next_billing_date),
212
+ },
213
+ });
214
+ }
215
+
216
+ async function handlePlanChanged(data: any) {
217
+ const { subscription_id, product_id, recurring_pre_tax_amount } = data;
218
+
219
+ await prisma.subscription.update({
220
+ where: { externalId: subscription_id },
221
+ data: {
222
+ productId: product_id,
223
+ amount: recurring_pre_tax_amount,
224
+ },
225
+ });
226
+
227
+ // Update user entitlements based on new plan
228
+ await updateUserEntitlements(subscription_id, product_id);
229
+ }
230
+
231
+ async function handleSubscriptionExpired(data: any) {
232
+ const { subscription_id, customer } = data;
233
+
234
+ await prisma.subscription.update({
235
+ where: { externalId: subscription_id },
236
+ data: { status: 'expired' },
237
+ });
238
+
239
+ // Revoke access
240
+ await prisma.user.update({
241
+ where: { id: customer.customer_id },
242
+ data: {
243
+ subscriptionStatus: 'expired',
244
+ plan: null,
245
+ },
246
+ });
247
+ }
248
+ ```
249
+
250
+ ### Subscription with Trial
251
+
252
+ ```typescript
253
+ const session = await client.checkoutSessions.create({
254
+ product_cart: [
255
+ { product_id: 'prod_pro_monthly', quantity: 1 }
256
+ ],
257
+ subscription_data: {
258
+ trial_period_days: 14,
259
+ },
260
+ customer: {
261
+ email: 'user@example.com',
262
+ name: 'John Doe',
263
+ },
264
+ return_url: 'https://yoursite.com/welcome',
265
+ });
266
+ ```
267
+
268
+ ### Customer Portal for Self-Service
269
+
270
+ Allow customers to manage their subscription:
271
+
272
+ ```typescript
273
+ // Create portal session
274
+ const portal = await client.customers.createPortalSession({
275
+ customer_id: 'cust_xxxxx',
276
+ return_url: 'https://yoursite.com/account',
277
+ });
278
+
279
+ // Redirect to portal.url
280
+ ```
281
+
282
+ Portal features:
283
+ - View subscription details
284
+ - Update payment method
285
+ - Cancel subscription
286
+ - View billing history
287
+
288
+ ---
289
+
290
+ ## On-Demand (Usage-Based) Subscriptions
291
+
292
+ For metered/usage-based billing:
293
+
294
+ ### Create Subscription with Mandate
295
+
296
+ ```typescript
297
+ const session = await client.checkoutSessions.create({
298
+ product_cart: [
299
+ { product_id: 'prod_usage_based', quantity: 1 }
300
+ ],
301
+ customer: { email: 'user@example.com' },
302
+ return_url: 'https://yoursite.com/success',
303
+ });
304
+ ```
305
+
306
+ ### Charge for Usage
307
+
308
+ ```typescript
309
+ // When usage occurs, create a charge
310
+ const charge = await client.subscriptions.charge({
311
+ subscription_id: 'sub_xxxxx',
312
+ amount: 1500, // $15.00 in cents
313
+ description: 'API calls for January 2025',
314
+ });
315
+ ```
316
+
317
+ ### Track Usage Events
318
+
319
+ ```typescript
320
+ // payment.succeeded - Charge succeeded
321
+ // payment.failed - Charge failed, implement retry logic
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Subscriptions with Credit Entitlements
327
+
328
+ Attach credit entitlements to subscription products to grant credits each billing cycle:
329
+
330
+ ### Setup
331
+
332
+ 1. Create a credit entitlement (Dashboard → Products → Credits)
333
+ 2. Create/edit a subscription product
334
+ 3. In **Entitlements** section, click **Attach** next to Credits
335
+ 4. Configure: credits per cycle, trial credits, proration, low balance threshold
336
+
337
+ ### Checkout with Credits
338
+
339
+ ```typescript
340
+ // Product has credit entitlement attached (e.g., 10,000 AI tokens/month)
341
+ const session = await client.checkoutSessions.create({
342
+ product_cart: [
343
+ { product_id: 'prod_pro_with_credits', quantity: 1 }
344
+ ],
345
+ subscription_data: {
346
+ trial_period_days: 14, // Trial credits can differ from regular amount
347
+ },
348
+ customer: { email: 'user@example.com' },
349
+ return_url: 'https://yoursite.com/success',
350
+ });
351
+ ```
352
+
353
+ ### Credit Lifecycle per Cycle
354
+
355
+ Each billing cycle:
356
+ 1. **New credits issued** — `credit.added` webhook fires
357
+ 2. **Usage deducts credits** — Automatically via meters or manually via API
358
+ 3. **Cycle ends** — Unused credits expire or roll over based on settings
359
+ 4. **Overage handled** — Forgiven, billed, or carried as deficit
360
+
361
+ ### Handle Credit Webhooks in Subscription Context
362
+
363
+ ```typescript
364
+ case 'credit.added':
365
+ // Credits issued with subscription renewal
366
+ await syncCreditBalance(data.customer_id, data.credit_entitlement_id, data.balance_after);
367
+ break;
368
+ case 'credit.balance_low':
369
+ // Notify customer or suggest upgrade
370
+ await sendLowBalanceAlert(data.customer_id, data.credit_entitlement_name, data.available_balance);
371
+ break;
372
+ case 'credit.deducted':
373
+ // Track consumption for analytics
374
+ await logCreditUsage(data.customer_id, data.amount);
375
+ break;
376
+ ```
377
+
378
+ ### Plan Changes with Credits
379
+
380
+ When customers upgrade/downgrade, credit proration can be enabled:
381
+ - **Proration enabled**: Remaining credits are prorated based on time left in cycle
382
+ - **Proration disabled**: Credits continue as-is until next cycle
383
+
384
+ ## Plan Changes
385
+
386
+ ### Upgrade/Downgrade Flow
387
+
388
+ ```typescript
389
+ // Get available plans
390
+ const plans = await client.products.list({
391
+ type: 'subscription',
392
+ });
393
+
394
+ // Change plan
395
+ await client.subscriptions.update({
396
+ subscription_id: 'sub_xxxxx',
397
+ product_id: 'prod_new_plan',
398
+ proration_behavior: 'create_prorations', // or 'none'
399
+ });
400
+ ```
401
+
402
+ ### Handling `subscription.plan_changed`
403
+
404
+ ```typescript
405
+ async function handlePlanChanged(data: any) {
406
+ const { subscription_id, product_id, customer } = data;
407
+
408
+ // Map product to features/limits
409
+ const planFeatures = getPlanFeatures(product_id);
410
+
411
+ await prisma.user.update({
412
+ where: { externalId: customer.customer_id },
413
+ data: {
414
+ plan: product_id,
415
+ features: planFeatures,
416
+ apiLimit: planFeatures.apiLimit,
417
+ storageLimit: planFeatures.storageLimit,
418
+ },
419
+ });
420
+ }
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Access Control Pattern
426
+
427
+ ### Middleware Example (Next.js)
428
+
429
+ ```typescript
430
+ // middleware.ts
431
+ import { NextResponse } from 'next/server';
432
+ import type { NextRequest } from 'next/server';
433
+
434
+ export async function middleware(request: NextRequest) {
435
+ // Check subscription status
436
+ const session = await getSession(request);
437
+
438
+ if (!session?.user) {
439
+ return NextResponse.redirect(new URL('/login', request.url));
440
+ }
441
+
442
+ const subscription = await getSubscription(session.user.id);
443
+
444
+ // Check if accessing premium feature
445
+ if (request.nextUrl.pathname.startsWith('/dashboard/pro')) {
446
+ if (!subscription || subscription.status !== 'active') {
447
+ return NextResponse.redirect(new URL('/pricing', request.url));
448
+ }
449
+
450
+ // Check if plan includes this feature
451
+ if (!subscription.features.includes('pro')) {
452
+ return NextResponse.redirect(new URL('/upgrade', request.url));
453
+ }
454
+ }
455
+
456
+ return NextResponse.next();
457
+ }
458
+ ```
459
+
460
+ ### React Hook for Subscription State
461
+
462
+ ```typescript
463
+ // hooks/useSubscription.ts
464
+ import useSWR from 'swr';
465
+
466
+ export function useSubscription() {
467
+ const { data, error, mutate } = useSWR('/api/subscription', fetcher);
468
+
469
+ return {
470
+ subscription: data,
471
+ isLoading: !error && !data,
472
+ isError: error,
473
+ isActive: data?.status === 'active',
474
+ isPro: data?.plan?.includes('pro'),
475
+ refresh: mutate,
476
+ };
477
+ }
478
+
479
+ // Usage in component
480
+ function PremiumFeature() {
481
+ const { isActive, isPro } = useSubscription();
482
+
483
+ if (!isActive) {
484
+ return <UpgradePrompt />;
485
+ }
486
+
487
+ if (!isPro) {
488
+ return <ProUpgradePrompt />;
489
+ }
490
+
491
+ return <ActualFeature />;
492
+ }
493
+ ```
494
+
495
+ ---
496
+
497
+ ## Common Patterns
498
+
499
+ ### Grace Period for Failed Payments
500
+
501
+ ```typescript
502
+ async function handleSubscriptionOnHold(data: any) {
503
+ const gracePeriodDays = 7;
504
+
505
+ await prisma.subscription.update({
506
+ where: { externalId: data.subscription_id },
507
+ data: {
508
+ status: 'on_hold',
509
+ gracePeriodEnds: new Date(Date.now() + gracePeriodDays * 24 * 60 * 60 * 1000),
510
+ },
511
+ });
512
+
513
+ // Schedule job to revoke access after grace period
514
+ await scheduleAccessRevocation(data.subscription_id, gracePeriodDays);
515
+ }
516
+ ```
517
+
518
+ ### Prorated Upgrades
519
+
520
+ When upgrading mid-cycle:
521
+ ```typescript
522
+ // Dodo handles proration automatically
523
+ // Customer pays difference for remaining days
524
+ await client.subscriptions.update({
525
+ subscription_id: 'sub_xxxxx',
526
+ product_id: 'prod_higher_plan',
527
+ proration_behavior: 'create_prorations',
528
+ });
529
+ ```
530
+
531
+ ### Cancellation with End-of-Period Access
532
+
533
+ ```typescript
534
+ // subscription.cancelled event includes:
535
+ // - cancel_at_next_billing_date: boolean
536
+ // - next_billing_date: string (when access should end)
537
+
538
+ if (data.cancel_at_next_billing_date) {
539
+ // Keep access until next_billing_date
540
+ await scheduleAccessRevocation(
541
+ data.subscription_id,
542
+ new Date(data.next_billing_date)
543
+ );
544
+ }
545
+ ```
546
+
547
+ ---
548
+
549
+ ## Testing
550
+
551
+ ### Test Scenarios
552
+ 1. New subscription → `subscription.active`
553
+ 2. Renewal success → `subscription.renewed` + `payment.succeeded`
554
+ 3. Renewal failure → `subscription.on_hold` + `payment.failed`
555
+ 4. Plan upgrade → `subscription.plan_changed`
556
+ 5. Cancellation → `subscription.cancelled`
557
+ 6. Expiration → `subscription.expired`
558
+
559
+ ### Test in Dashboard
560
+ Use test mode and trigger events manually from the webhook settings.
561
+
562
+ ---
563
+
564
+ ## Resources
565
+
566
+ - [Subscription Guide](https://docs.dodopayments.com/developer-resources/subscription-integration-guide)
567
+ - [On-Demand Subscriptions](https://docs.dodopayments.com/developer-resources/ondemand-subscriptions)
568
+ - [Webhook Events](https://docs.dodopayments.com/developer-resources/webhooks/intents/subscription)
569
+ - [Customer Portal](https://docs.dodopayments.com/developer-resources/customer-portal)
570
+ - [Credit-Based Billing](https://docs.dodopayments.com/features/credit-based-billing)
571
+ - [Credit Webhook Events](https://docs.dodopayments.com/developer-resources/webhooks/intents/credit)