@dodopayments/opencode-plugin 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,501 +1,488 @@
1
1
  ---
2
2
  name: billing-sdk
3
- description: Guide for using BillingSDK - open-source React components for pricing tables, subscription management, and billing UI with Dodo Payments.
3
+ description: Guide for building billing UI with BillingSDK - the open-source React component library for pricing tables, subscription management, usage meters, invoice history, and customer portal flows wired to Dodo Payments.
4
4
  ---
5
5
 
6
- # BillingSDK Integration
6
+ # BillingSDK
7
7
 
8
- **Reference: [docs.dodopayments.com/developer-resources/billingsdk](https://docs.dodopayments.com/developer-resources/billingsdk) | [billingsdk.com](https://billingsdk.com)**
8
+ BillingSDK is an open-source React component library for billing interfaces, maintained by Dodo Payments
9
+ at `github.com/dodopayments/billingsdk`. Components are copied into your repository as source files
10
+ (shadcn/ui model) rather than imported from a runtime npm package.
9
11
 
10
- BillingSDK provides open-source, customizable React components for billing interfaces - pricing tables, subscription management, usage meters, and more.
12
+ Use this skill when building the front end. All Dodo Payments API calls belong in server-side route
13
+ handlers; the components are presentation only.
11
14
 
12
- ---
15
+ ## When to use this skill
13
16
 
14
- ## Overview
17
+ - Building a pricing page that starts a Dodo checkout.
18
+ - Building an account/billing page with subscription details and a "Manage billing" button.
19
+ - Rendering usage meters, credit balances, invoice history, or upcoming charges.
20
+ - Adding cancel / upgrade / downgrade UI to an existing React or Next.js app.
21
+ - Deciding whether to scaffold with `@billingsdk/cli init` or add individual components.
15
22
 
16
- BillingSDK offers:
17
- - **React Components**: Pre-built, customizable billing components
18
- - **CLI Tooling**: Project initialization and component management
19
- - **Framework Support**: Next.js, Express.js, Hono, Fastify, React
20
- - **Payment Provider**: Full integration with Dodo Payments
23
+ ## Core concepts
21
24
 
22
- ---
25
+ **Source-installed, not a dependency.** The repository root package `billingsdk` is private. There is no
26
+ verified general-purpose runtime npm component package. You install components through the CLI or the
27
+ shadcn registry, and the `.tsx` files land in your project under `components/billingsdk/`.
28
+
29
+ **The installed file is the prop contract.** Because components are vendored into your repo, the exported
30
+ props type in `components/billingsdk/<component>.tsx` is authoritative for your version. Read it before
31
+ wiring callbacks. Do not assume prop names from another project.
32
+
33
+ **The registry is the installability source of truth.** Use
34
+ [`registry.json`](https://github.com/dodopayments/billingsdk/blob/main/registry.json). The docs navigation
35
+ has drifted from it — for example `payment-success-dialog` appears in navigation but not in the current
36
+ registry. If `add` fails, the block is not in the registry.
23
37
 
24
- ## Quick Start Options
38
+ **Components never call Dodo.** They receive data and fire callbacks. Every Dodo Payments call runs on your
39
+ server with `DODO_PAYMENTS_API_KEY`. That key is `dodo_test_...` or `dodo_live_...` and must never reach
40
+ the browser.
25
41
 
26
- ### Option 1: New Project (Recommended)
27
- Complete project setup with framework configuration and API routes:
42
+ ## Installation
43
+
44
+ Scaffold a new integration (framework config, API routes, `useBilling` hooks, `lib/dodopayments.ts`,
45
+ dependencies, and env vars `DODO_PAYMENTS_API_KEY`, `DODO_PAYMENTS_ENVIRONMENT`,
46
+ `DODO_PAYMENTS_WEBHOOK_KEY`):
28
47
 
29
48
  ```bash
30
49
  npx @billingsdk/cli init
31
50
  ```
32
51
 
33
- The CLI will:
34
- - Configure your framework (Next.js App Router)
35
- - Set up Dodo Payments integration
36
- - Generate API routes for checkout, customers, webhooks
37
- - Install dependencies
38
- - Create configuration files
39
-
40
- ### Option 2: Add to Existing Project
41
- Add individual components using the CLI:
52
+ Add a single component to an existing project:
42
53
 
43
54
  ```bash
44
55
  npx @billingsdk/cli add pricing-table-one
45
- npx @billingsdk/cli add subscription-management
46
- npx @billingsdk/cli add usage-meter-circle
47
56
  ```
48
57
 
49
- ### Option 3: Manual via shadcn/ui
50
- Install directly using shadcn registry:
58
+ Or install through the shadcn registry using the `@billingsdk/` namespace:
51
59
 
52
60
  ```bash
53
61
  npx shadcn@latest add @billingsdk/pricing-table-one
54
62
  ```
55
63
 
56
- ---
64
+ The published CLI package is `@billingsdk/cli`, most recently seen at version 0.9.0.
57
65
 
58
- ## CLI Reference
66
+ ## Component inventory
59
67
 
60
- ### Initialize Project
68
+ From the current official registry, grouped by purpose:
61
69
 
62
- ```bash
63
- npx @billingsdk/cli init
64
- ```
70
+ | Group | Blocks |
71
+ |---|---|
72
+ | Pricing | `pricing-table-one` through `pricing-table-eight` |
73
+ | Subscription | `subscription-management`, `usage-based-pricing`, `invoice-history`, `update-plan-card`, `update-plan-dialog`, `proration-preview`, `cancel-subscription-card`, `cancel-subscription-dialog` |
74
+ | Usage and billing | `usage-meter-linear`, `usage-meter-circle`, `usage-table`, `detailed-usage-table`, `billing-screen`, `billing-settings`, `billing-settings-2`, `upcoming-charges` |
75
+ | Payments | `payment-details`, `payment-details-two`, `payment-method-selector`, `payment-card`, `payment-failure` |
76
+ | Promotion and trials | `banner`, `limited-offer-dialog`, `trial-expiry-card` |
65
77
 
66
- Interactive setup prompts:
67
- 1. Select framework (Next.js, Express.js, Hono, Fastify, React)
68
- 2. Select payment provider (Dodo Payments)
69
- 3. Configure project settings
78
+ `pricing-table-one` is the only block with an officially published prop example, reproduced below. For
79
+ every other block, run `add` and read the generated file's props type.
70
80
 
71
- ### Add Components
81
+ ## Server client
72
82
 
73
- ```bash
74
- npx @billingsdk/cli add <component-name>
75
- ```
83
+ One module, imported only by route handlers and server components.
76
84
 
77
- **Available components:**
78
- - `pricing-table-one` - Simple pricing table
79
- - `pricing-table-two` - Feature-rich pricing table
80
- - `subscription-management` - Manage active subscriptions
81
- - `usage-meter-circle` - Circular usage visualization
82
- - More components available...
85
+ ```typescript
86
+ // lib/dodopayments.ts
87
+ import DodoPayments from 'dodopayments';
83
88
 
84
- ### What happens when adding:
85
- 1. Downloads component from registry
86
- 2. Installs files to `components/billingsdk/`
87
- 3. Updates project configuration
88
- 4. Installs additional dependencies
89
+ if (!process.env.DODO_PAYMENTS_API_KEY) {
90
+ throw new Error('DODO_PAYMENTS_API_KEY is not set');
91
+ }
89
92
 
90
- ---
93
+ export const dodo = new DodoPayments({
94
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
95
+ environment: process.env.DODO_PAYMENTS_ENVIRONMENT === 'live_mode' ? 'live_mode' : 'test_mode',
96
+ });
97
+ ```
91
98
 
92
- ## Components
99
+ `environment` defaults to `'live_mode'` when omitted, so set it explicitly during development. Base URLs
100
+ are `https://test.dodopayments.com` and `https://live.dodopayments.com`.
93
101
 
94
- ### Pricing Table One
102
+ ## Pricing page
95
103
 
96
- Simple, clean pricing table for displaying plans.
104
+ ### 1. Plan catalogue
97
105
 
98
- **Installation:**
99
- ```bash
100
- npx @billingsdk/cli add pricing-table-one
101
- # or
102
- npx shadcn@latest add @billingsdk/pricing-table-one
103
- ```
106
+ Keep the product ids and the display copy in one module. The `price` here is a display number rendered by
107
+ the component — the amount actually charged comes from the Dodo product catalogue, and Dodo API amounts
108
+ are always in the smallest currency unit (cents). Treat this file as a mirror of the dashboard and
109
+ reconcile it when you change a price.
104
110
 
105
- **Usage:**
106
- ```tsx
107
- import { PricingTableOne } from "@/components/billingsdk/pricing-table-one";
111
+ ```typescript
112
+ // lib/plans.ts
113
+ export type PlanId = 'starter' | 'pro';
114
+
115
+ export interface Plan {
116
+ id: PlanId;
117
+ title: string;
118
+ price: number;
119
+ period: string;
120
+ features: string[];
121
+ popular: boolean;
122
+ }
108
123
 
109
- const plans = [
124
+ export const PLANS: readonly Plan[] = [
110
125
  {
111
- id: 'prod_free',
112
- name: 'Free',
113
- price: 0,
114
- interval: 'month',
115
- features: ['5 projects', 'Basic support'],
126
+ id: 'starter',
127
+ title: 'Starter',
128
+ price: 9,
129
+ period: 'month',
130
+ features: ['100 requests', 'Basic support', '1 project'],
131
+ popular: false,
116
132
  },
117
133
  {
118
- id: 'prod_pro',
119
- name: 'Pro',
134
+ id: 'pro',
135
+ title: 'Pro',
120
136
  price: 29,
121
- interval: 'month',
122
- features: ['Unlimited projects', 'Priority support', 'API access'],
137
+ period: 'month',
138
+ features: ['Unlimited requests', 'Priority support', '10 projects'],
123
139
  popular: true,
124
140
  },
125
- {
126
- id: 'prod_enterprise',
127
- name: 'Enterprise',
128
- price: 99,
129
- interval: 'month',
130
- features: ['Everything in Pro', 'Custom integrations', 'Dedicated support'],
131
- },
132
141
  ];
142
+ ```
133
143
 
134
- export function PricingPage() {
135
- const handleSelectPlan = async (planId: string) => {
136
- // Create checkout session
137
- const response = await fetch('/api/checkout', {
138
- method: 'POST',
139
- headers: { 'Content-Type': 'application/json' },
140
- body: JSON.stringify({ productId: planId }),
141
- });
142
-
143
- const { checkoutUrl } = await response.json();
144
- window.location.href = checkoutUrl;
145
- };
144
+ ### 2. Product id map — server only
146
145
 
147
- return (
148
- <PricingTableOne
149
- plans={plans}
150
- onSelectPlan={handleSelectPlan}
151
- />
152
- );
146
+ Never let the browser choose an arbitrary `product_id`. Map the public plan slug to a `pdt_` id on the
147
+ server and reject anything else.
148
+
149
+ ```typescript
150
+ // lib/plan-products.server.ts
151
+ import 'server-only';
152
+ import type { PlanId } from './plans';
153
+
154
+ const PRODUCT_IDS: Record<PlanId, string> = {
155
+ starter: 'pdt_starter_monthly',
156
+ pro: 'pdt_pro_monthly',
157
+ };
158
+
159
+ export function productIdForPlan(value: unknown): string | null {
160
+ if (typeof value !== 'string') return null;
161
+ return PRODUCT_IDS[value as PlanId] ?? null;
153
162
  }
154
163
  ```
155
164
 
156
- ### Pricing Table Two
165
+ ### 3. Checkout route
157
166
 
158
- Feature-comparison pricing table with toggle for monthly/yearly.
167
+ ```typescript
168
+ // app/api/checkout/route.ts
169
+ import { NextResponse } from 'next/server';
170
+ import { dodo } from '@/lib/dodopayments';
171
+ import { productIdForPlan } from '@/lib/plan-products.server';
172
+ import { getCurrentUser } from '@/lib/auth';
173
+
174
+ export async function POST(request: Request) {
175
+ const user = await getCurrentUser();
176
+ if (!user) {
177
+ return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
178
+ }
159
179
 
160
- **Installation:**
161
- ```bash
162
- npx @billingsdk/cli add pricing-table-two
180
+ const body: unknown = await request.json();
181
+ const planId = (body as { planId?: unknown }).planId;
182
+ const productId = productIdForPlan(planId);
183
+
184
+ if (!productId) {
185
+ return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
186
+ }
187
+
188
+ try {
189
+ const session = await dodo.checkoutSessions.create({
190
+ product_cart: [{ product_id: productId, quantity: 1 }],
191
+ customer: { email: user.email, name: user.name },
192
+ return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/return`,
193
+ cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
194
+ metadata: { app_user_id: user.id },
195
+ });
196
+
197
+ if (!session.checkout_url) {
198
+ return NextResponse.json({ error: 'No checkout URL returned' }, { status: 502 });
199
+ }
200
+
201
+ return NextResponse.json({ checkoutUrl: session.checkout_url });
202
+ } catch (error) {
203
+ console.error('Checkout session creation failed', error);
204
+ return NextResponse.json({ error: 'Could not start checkout' }, { status: 500 });
205
+ }
206
+ }
163
207
  ```
164
208
 
165
- **Usage:**
166
- ```tsx
167
- import { PricingTableTwo } from "@/components/billingsdk/pricing-table-two";
209
+ `checkout_url` is nullable — it is absent when the session is created with `payment_method_id` — so guard
210
+ it rather than redirecting to `undefined`. Checkout URLs are single-use and normally expire after 24 hours.
168
211
 
169
- const plans = [
170
- {
171
- id: 'prod_starter_monthly',
172
- yearlyId: 'prod_starter_yearly',
173
- name: 'Starter',
174
- monthlyPrice: 19,
175
- yearlyPrice: 190,
176
- features: [
177
- { name: 'Projects', value: '10' },
178
- { name: 'Storage', value: '5 GB' },
179
- { name: 'Support', value: 'Email' },
180
- ],
181
- },
182
- {
183
- id: 'prod_pro_monthly',
184
- yearlyId: 'prod_pro_yearly',
185
- name: 'Pro',
186
- monthlyPrice: 49,
187
- yearlyPrice: 490,
188
- popular: true,
189
- features: [
190
- { name: 'Projects', value: 'Unlimited' },
191
- { name: 'Storage', value: '50 GB' },
192
- { name: 'Support', value: 'Priority' },
193
- ],
194
- },
195
- ];
212
+ Use `checkoutSessions.create`. `payments.create` and `subscriptions.create` are deprecated for new
213
+ integrations. Checkout parameters are covered in depth in the `checkout-integration` skill.
214
+
215
+ ### 4. Client component
216
+
217
+ ```tsx
218
+ // components/pricing-plans.tsx
219
+ 'use client';
220
+
221
+ import { useState } from 'react';
222
+ import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
223
+ import { PLANS } from '@/lib/plans';
224
+
225
+ export function PricingPlans() {
226
+ const [error, setError] = useState<string | null>(null);
227
+
228
+ async function handlePlanSelect(planId: string) {
229
+ setError(null);
230
+ try {
231
+ const response = await fetch('/api/checkout', {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({ planId }),
235
+ });
236
+
237
+ if (!response.ok) {
238
+ setError('Could not start checkout. Please try again.');
239
+ return;
240
+ }
241
+
242
+ const { checkoutUrl } = (await response.json()) as { checkoutUrl: string };
243
+ window.location.href = checkoutUrl;
244
+ } catch {
245
+ setError('Network error. Please try again.');
246
+ }
247
+ }
196
248
 
197
- export function PricingPage() {
198
249
  return (
199
- <PricingTableTwo
200
- plans={plans}
201
- onSelectPlan={(planId, billingInterval) => {
202
- console.log(`Selected: ${planId}, Interval: ${billingInterval}`);
203
- }}
204
- />
250
+ <>
251
+ <PricingTableOne
252
+ plans={PLANS}
253
+ title="Choose your plan"
254
+ description="Select the plan that works best for you"
255
+ onPlanSelect={handlePlanSelect}
256
+ theme="classic"
257
+ size="medium"
258
+ />
259
+ {error ? <p role="alert">{error}</p> : null}
260
+ </>
205
261
  );
206
262
  }
207
263
  ```
208
264
 
209
- ### Subscription Management
265
+ Render `<PricingPlans />` from `app/pricing/page.tsx`. Nothing secret crosses into the client component:
266
+ it sends a plan slug and receives a URL.
267
+
268
+ ## Subscription management and customer portal
210
269
 
211
- Allow users to view and manage their subscription.
270
+ Install the block, then read its props from the generated file:
212
271
 
213
- **Installation:**
214
272
  ```bash
215
273
  npx @billingsdk/cli add subscription-management
216
274
  ```
217
275
 
218
- **Usage:**
219
- ```tsx
220
- import { SubscriptionManagement } from "@/components/billingsdk/subscription-management";
276
+ The portal route. The method is `customers.customerPortal.create(customerID, { ...params })` and the
277
+ session's URL field is `link`.
221
278
 
222
- export function AccountPage() {
223
- const subscription = {
224
- plan: 'Pro',
225
- status: 'active',
226
- currentPeriodEnd: '2025-02-21',
227
- amount: 49,
228
- interval: 'month',
229
- };
279
+ ```typescript
280
+ // app/api/portal/route.ts
281
+ import { NextResponse } from 'next/server';
282
+ import { dodo } from '@/lib/dodopayments';
283
+ import { getCurrentUser } from '@/lib/auth';
284
+
285
+ export async function POST() {
286
+ const user = await getCurrentUser();
287
+ if (!user?.dodoCustomerId) {
288
+ return NextResponse.json({ error: 'No billing account' }, { status: 400 });
289
+ }
230
290
 
231
- return (
232
- <SubscriptionManagement
233
- subscription={subscription}
234
- onManageBilling={async () => {
235
- // Open customer portal
236
- const response = await fetch('/api/portal', { method: 'POST' });
237
- const { url } = await response.json();
238
- window.location.href = url;
239
- }}
240
- onCancelSubscription={async () => {
241
- if (confirm('Are you sure you want to cancel?')) {
242
- await fetch('/api/subscription/cancel', { method: 'POST' });
243
- }
244
- }}
245
- />
246
- );
291
+ try {
292
+ const session = await dodo.customers.customerPortal.create(user.dodoCustomerId, {
293
+ return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
294
+ });
295
+
296
+ return NextResponse.json({ url: session.link });
297
+ } catch (error) {
298
+ console.error('Customer portal session failed', error);
299
+ return NextResponse.json({ error: 'Could not open billing portal' }, { status: 500 });
300
+ }
247
301
  }
248
302
  ```
249
303
 
250
- ### Usage Meter
304
+ Portal links expire after 24 hours, so mint one per click instead of caching it. In the portal a customer
305
+ can view and cancel subscriptions, change plans within enabled product collections, update payment
306
+ methods, download invoices, and retrieve license keys.
251
307
 
252
- Display usage-based billing metrics.
308
+ Client side:
253
309
 
254
- **Installation:**
255
- ```bash
256
- npx @billingsdk/cli add usage-meter-circle
257
- ```
258
-
259
- **Usage:**
260
310
  ```tsx
261
- import { UsageMeterCircle } from "@/components/billingsdk/usage-meter-circle";
311
+ // components/manage-billing-button.tsx
312
+ 'use client';
313
+
314
+ import { useState } from 'react';
315
+
316
+ export function ManageBillingButton() {
317
+ const [pending, setPending] = useState(false);
318
+
319
+ async function openPortal() {
320
+ setPending(true);
321
+ try {
322
+ const response = await fetch('/api/portal', { method: 'POST' });
323
+ if (!response.ok) {
324
+ setPending(false);
325
+ return;
326
+ }
327
+ const { url } = (await response.json()) as { url: string };
328
+ window.location.href = url;
329
+ } catch {
330
+ setPending(false);
331
+ }
332
+ }
262
333
 
263
- export function UsageDashboard() {
264
334
  return (
265
- <div className="grid grid-cols-3 gap-4">
266
- <UsageMeterCircle
267
- label="API Calls"
268
- current={8500}
269
- limit={10000}
270
- unit="calls"
271
- />
272
- <UsageMeterCircle
273
- label="Storage"
274
- current={3.2}
275
- limit={5}
276
- unit="GB"
277
- />
278
- <UsageMeterCircle
279
- label="Bandwidth"
280
- current={45}
281
- limit={100}
282
- unit="GB"
283
- />
284
- </div>
335
+ <button type="button" onClick={openPortal} disabled={pending}>
336
+ {pending ? 'Opening…' : 'Manage billing'}
337
+ </button>
285
338
  );
286
339
  }
287
340
  ```
288
341
 
289
- ---
342
+ Wire `openPortal` to whichever callback prop your installed `subscription-management.tsx` exposes, or
343
+ render the standalone button next to it. Load the subscription itself in the server component that hosts
344
+ the page — `await dodo.subscriptions.retrieve(user.dodoSubscriptionId)` — and pass plain data down.
290
345
 
291
- ## Next.js Integration
346
+ ## Usage and credit display
292
347
 
293
- ### Project Structure (after `init`)
348
+ Install a meter block:
294
349
 
295
- ```
296
- your-project/
297
- ├── app/
298
- │ ├── api/
299
- │ │ ├── checkout/
300
- │ │ │ └── route.ts
301
- │ │ ├── portal/
302
- │ │ │ └── route.ts
303
- │ │ └── webhooks/
304
- │ │ └── dodo/
305
- │ │ └── route.ts
306
- │ └── pricing/
307
- │ └── page.tsx
308
- ├── components/
309
- │ └── billingsdk/
310
- │ ├── pricing-table-one.tsx
311
- │ └── subscription-management.tsx
312
- ├── lib/
313
- │ ├── dodo.ts
314
- │ └── billingsdk-config.ts
315
- └── .env.local
350
+ ```bash
351
+ npx @billingsdk/cli add usage-meter-linear
316
352
  ```
317
353
 
318
- ### Generated API Routes
354
+ Fetch the numbers server-side. Credit balances come from `creditEntitlements.balances.retrieve`:
319
355
 
320
- **Checkout Route (`app/api/checkout/route.ts`):**
321
356
  ```typescript
322
- import { NextRequest, NextResponse } from 'next/server';
323
- import { dodo } from '@/lib/dodo';
357
+ // app/api/usage/credits/route.ts
358
+ import { NextResponse } from 'next/server';
359
+ import { dodo } from '@/lib/dodopayments';
360
+ import { getCurrentUser } from '@/lib/auth';
324
361
 
325
- export async function POST(req: NextRequest) {
326
- const { productId, email } = await req.json();
362
+ const CREDIT_ENTITLEMENT_ID = process.env.DODO_CREDIT_ENTITLEMENT_ID;
327
363
 
328
- const session = await dodo.checkoutSessions.create({
329
- product_cart: [{ product_id: productId, quantity: 1 }],
330
- customer: { email },
331
- return_url: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
332
- });
364
+ export async function GET() {
365
+ const user = await getCurrentUser();
366
+ if (!user?.dodoCustomerId || !CREDIT_ENTITLEMENT_ID) {
367
+ return NextResponse.json({ error: 'No billing account' }, { status: 400 });
368
+ }
333
369
 
334
- return NextResponse.json({ checkoutUrl: session.checkout_url });
335
- }
336
- ```
370
+ try {
371
+ const balance = await dodo.creditEntitlements.balances.retrieve(user.dodoCustomerId, {
372
+ credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
373
+ });
337
374
 
338
- **Portal Route (`app/api/portal/route.ts`):**
339
- ```typescript
340
- import { NextRequest, NextResponse } from 'next/server';
341
- import { dodo } from '@/lib/dodo';
342
- import { getSession } from '@/lib/auth';
343
-
344
- export async function POST(req: NextRequest) {
345
- const session = await getSession();
346
-
347
- const portal = await dodo.customers.createPortalSession({
348
- customer_id: session.user.customerId,
349
- return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
350
- });
351
-
352
- return NextResponse.json({ url: portal.url });
375
+ return NextResponse.json({ balance: balance.balance, overage: balance.overage });
376
+ } catch (error) {
377
+ console.error('Credit balance lookup failed', error);
378
+ return NextResponse.json({ error: 'Could not read balance' }, { status: 500 });
379
+ }
353
380
  }
354
381
  ```
355
382
 
356
- ### Configuration File
383
+ Pass the result into the meter block using the prop names in your installed
384
+ `components/billingsdk/usage-meter-linear.tsx`. Related blocks for the same surface: `usage-meter-circle`,
385
+ `usage-table`, `detailed-usage-table`, `upcoming-charges`, `invoice-history`.
357
386
 
358
- **`lib/billingsdk-config.ts`:**
359
- ```typescript
360
- export const plans = [
361
- {
362
- id: process.env.NEXT_PUBLIC_PLAN_FREE_ID!,
363
- name: 'Free',
364
- description: 'Perfect for trying out',
365
- price: 0,
366
- interval: 'month' as const,
367
- features: [
368
- '5 projects',
369
- '1 GB storage',
370
- 'Community support',
371
- ],
372
- },
373
- {
374
- id: process.env.NEXT_PUBLIC_PLAN_PRO_ID!,
375
- name: 'Pro',
376
- description: 'For professionals',
377
- price: 29,
378
- interval: 'month' as const,
379
- popular: true,
380
- features: [
381
- 'Unlimited projects',
382
- '50 GB storage',
383
- 'Priority support',
384
- 'API access',
385
- ],
386
- },
387
- ];
387
+ Credit balances are eventually consistent — meter-to-credit deduction runs on a background worker roughly
388
+ once a minute — so the displayed balance is informational. Do not gate a request on it. Metering, credit
389
+ entitlements, and ledger entries are covered in the `usage-based-billing` and `credit-based-billing` skills.
388
390
 
389
- export const config = {
390
- returnUrl: process.env.NEXT_PUBLIC_APP_URL + '/success',
391
- portalReturnUrl: process.env.NEXT_PUBLIC_APP_URL + '/account',
392
- };
393
- ```
391
+ ## Theming and customization
394
392
 
395
- ---
393
+ Components are Tailwind CSS plus shadcn/ui conventions, so there are three layers:
396
394
 
397
- ## Customization
395
+ 1. **Theme tokens.** Override the shadcn CSS variables in `globals.css`; every block inherits them.
398
396
 
399
- ### Styling with Tailwind
397
+ ```css
398
+ @layer base {
399
+ :root {
400
+ --primary: 220 90% 56%;
401
+ --primary-foreground: 0 0% 100%;
402
+ }
403
+ }
404
+ ```
400
405
 
401
- Components use Tailwind CSS and shadcn/ui patterns. Customize via:
406
+ 2. **Component props.** `pricing-table-one` accepts `theme` (`"classic"` in the official example) and
407
+ `size` (`"medium"`). Check the installed file for the full union types.
402
408
 
403
- 1. **Theme variables** in `globals.css`
404
- 2. **Direct class overrides** on components
405
- 3. **Component source modification** (files are local)
409
+ 3. **Source edits.** The files are yours. Editing them is the supported path for structural changes, at
410
+ the cost of manual reconciliation when you re-run `add`.
406
411
 
407
- **Example - Custom colors:**
408
- ```css
409
- /* globals.css */
410
- @layer base {
411
- :root {
412
- --primary: 220 90% 56%;
413
- --primary-foreground: 0 0% 100%;
414
- }
415
- }
416
- ```
412
+ The hosted Dodo checkout page is themed separately, through `customization.theme_config` on the checkout
413
+ session — not by these components.
417
414
 
418
- ### Component Props
415
+ ## Framework support
419
416
 
420
- Most components accept standard styling props:
417
+ The components are **not** framework-agnostic. They require React or Next.js plus Tailwind CSS and
418
+ shadcn/ui conventions.
421
419
 
422
- ```tsx
423
- <PricingTableOne
424
- plans={plans}
425
- onSelectPlan={handleSelect}
426
- className="max-w-4xl mx-auto"
427
- containerClassName="gap-8"
428
- cardClassName="border-2"
429
- />
430
- ```
420
+ The CLI additionally ships server integration templates for Next.js, Express, Hono, Fastify, and React.
421
+ That covers the route-handler side only. Caution: Dodo's integration page and BillingSDK's own
422
+ introduction have disagreed about which server templates are shipping versus "coming soon" — confirm the
423
+ adapter you want with the current CLI before committing to it.
431
424
 
432
- ---
425
+ For non-React front ends, call Dodo through a framework adapter (`@dodopayments/nextjs`,
426
+ `@dodopayments/express`, `@dodopayments/sveltekit`, and others) and build your own UI.
433
427
 
434
- ## Environment Variables
428
+ ## Environment variables
435
429
 
436
430
  ```bash
437
- # .env.local
438
-
439
- # Dodo Payments
440
- DODO_PAYMENTS_API_KEY=sk_live_xxxxx
441
- DODO_PAYMENTS_WEBHOOK_SECRET=whsec_xxxxx
431
+ # .env.local — server-side only, never prefixed with NEXT_PUBLIC_
432
+ DODO_PAYMENTS_API_KEY=dodo_test_xxxxxxxxxxxxxxxx
433
+ DODO_PAYMENTS_ENVIRONMENT=test_mode
434
+ DODO_PAYMENTS_WEBHOOK_KEY=xxxxxxxxxxxxxxxx
435
+ DODO_CREDIT_ENTITLEMENT_ID=cde_xxxxxxxxxxxxxxxx
442
436
 
443
- # Product IDs (from dashboard)
444
- NEXT_PUBLIC_PLAN_FREE_ID=prod_xxxxx
445
- NEXT_PUBLIC_PLAN_PRO_ID=prod_xxxxx
446
- NEXT_PUBLIC_PLAN_ENTERPRISE_ID=prod_xxxxx
447
-
448
- # App
437
+ # Safe to expose
449
438
  NEXT_PUBLIC_APP_URL=https://yoursite.com
450
439
  ```
451
440
 
452
- ---
441
+ Switch to `dodo_live_...` and `DODO_PAYMENTS_ENVIRONMENT=live_mode` together. A live key against
442
+ `test_mode` fails, and a test key against `live_mode` fails.
453
443
 
454
- ## Best Practices
444
+ ## Common mistakes
455
445
 
456
- ### 1. Use Product IDs from Environment
457
- Keep product IDs in environment variables for easy staging/production switching.
446
+ **Exposing the API key to the browser.** `NEXT_PUBLIC_DODO_PAYMENTS_API_KEY` inlines the secret into the
447
+ JS bundle for anyone to read. There is no publishable key in Dodo Payments — every key is secret. Import
448
+ `lib/dodopayments.ts` only from route handlers and server components.
458
449
 
459
- ### 2. Handle Loading States
460
- Components should show loading states during checkout:
450
+ **Calling `customers.createPortalSession`.** That method does not exist. Use
451
+ `client.customers.customerPortal.create(customerID, { ...params })` and read `.link`, not `.url`.
461
452
 
462
- ```tsx
463
- const [loading, setLoading] = useState(false);
453
+ **Trusting client-rendered plan state.** The selected plan in React state, or the fact that the user
454
+ returned to `return_url`, does not mean the subscription is active. Grant entitlements only from
455
+ webhook-confirmed subscription events persisted in your database, then render UI from that. Webhook
456
+ signature verification is covered in the `webhook-integration` skill.
464
457
 
465
- const handleSelect = async (planId: string) => {
466
- setLoading(true);
467
- try {
468
- const response = await fetch('/api/checkout', {...});
469
- const { checkoutUrl } = await response.json();
470
- window.location.href = checkoutUrl;
471
- } finally {
472
- setLoading(false);
473
- }
474
- };
475
- ```
458
+ **Accepting a raw `product_id` from the request body.** A caller can then check out against any product in
459
+ your catalogue, including internal or discounted ones. Map an opaque plan slug to a `pdt_` id server-side.
476
460
 
477
- ### 3. Server-Side Data Fetching
478
- Fetch subscription data server-side when possible:
461
+ **Hardcoding prices in the client.** `PLANS[].price` is display text. When you change a price in the Dodo
462
+ dashboard, the checkout charges the new amount while the pricing page keeps advertising the old one.
463
+ Reconcile `lib/plans.ts` with the dashboard as part of any pricing change.
479
464
 
480
- ```tsx
481
- // app/account/page.tsx
482
- import { getSubscription } from '@/lib/subscription';
465
+ **`npm install billingsdk`.** The root repository package is private. Install through
466
+ `@billingsdk/cli` or the shadcn registry instead.
483
467
 
484
- export default async function AccountPage() {
485
- const subscription = await getSubscription();
486
-
487
- return <SubscriptionManagement subscription={subscription} />;
488
- }
489
- ```
468
+ **Guessing prop names.** `pricing-table-one` is the only block with a published prop example. Open the
469
+ generated file under `components/billingsdk/` and read the exported props type before wiring anything else.
490
470
 
491
- ### 4. Implement Webhooks
492
- Always use webhooks as source of truth for subscription status, not client-side data.
471
+ **Using `DODO_PAYMENTS_WEBHOOK_SECRET`.** The SDK reads `DODO_PAYMENTS_WEBHOOK_KEY` for the `webhookKey`
472
+ option.
493
473
 
494
- ---
474
+ **Gating requests on the displayed credit balance.** Deduction is asynchronous, so the reported balance
475
+ lags. Dodo explicitly warns against using it as strict per-request authorization.
476
+
477
+ **Using `payments.create` or `subscriptions.create` for a purchase.** Both are deprecated. Use
478
+ `checkoutSessions.create`.
495
479
 
496
480
  ## Resources
497
481
 
498
- - [BillingSDK Documentation](https://billingsdk.com/docs)
499
- - [Dodo Payments Integration](https://docs.dodopayments.com/developer-resources/billingsdk)
500
- - [Component Gallery](https://billingsdk.com/docs/components)
501
- - [GitHub Repository](https://github.com/dodopayments/billingsdk)
482
+ - [BillingSDK on Dodo docs](https://docs.dodopayments.com/developer-resources/billingsdk)
483
+ - [BillingSDK quick start](https://billingsdk.com/docs/quick-start)
484
+ - [Component catalogue](https://billingsdk.com/docs/components)
485
+ - [`registry.json` — installability source of truth](https://github.com/dodopayments/billingsdk/blob/main/registry.json)
486
+ - [Checkout Sessions guide](https://docs.dodopayments.com/developer-resources/checkout-session)
487
+ - [Customer Portal](https://docs.dodopayments.com/features/customer-portal)
488
+ - [Credit-based billing](https://docs.dodopayments.com/features/credit-based-billing)