@dodopayments/opencode-plugin 0.6.0 → 0.6.1
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.
- package/README.md +1 -0
- package/opencode-plugin/README.md +1 -1
- package/package.json +1 -1
- package/skills/better-auth-integration/SKILL.md +12 -3
- package/skills/billing-sdk/SKILL.md +66 -33
- package/skills/checkout-integration/SKILL.md +26 -20
- package/skills/credit-based-billing/SKILL.md +7 -8
- package/skills/customer-management/SKILL.md +25 -17
- package/skills/discounts-and-promotions/SKILL.md +20 -17
- package/skills/dodo-best-practices/SKILL.md +18 -5
- package/skills/dodo-mcp-usage/SKILL.md +152 -0
- package/skills/framework-adapters/SKILL.md +167 -35
- package/skills/license-keys/SKILL.md +142 -97
- package/skills/localized-pricing/SKILL.md +51 -29
- package/skills/mobile-checkout/SKILL.md +168 -118
- package/skills/product-catalog-management/SKILL.md +39 -16
- package/skills/refunds-and-disputes/SKILL.md +91 -71
- package/skills/subscription-integration/SKILL.md +62 -9
- package/skills/testing-and-go-live/SKILL.md +22 -15
- package/skills/usage-based-billing/SKILL.md +18 -15
- package/skills/webhook-integration/SKILL.md +27 -10
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dodo-mcp-usage
|
|
3
|
+
description: How and when to use the Dodo Payments MCP servers (dodo-knowledge for docs search, dodopayments-api for the live API via Code Mode) safely. Use when looking up current Dodo payloads or event names, or inspecting or changing real products, checkouts, customers, subscriptions, webhooks or refunds.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Using the Dodo Payments MCP servers
|
|
7
|
+
|
|
8
|
+
The [Dodo Payments agent plugin](https://github.com/dodopayments/dodo-agent-plugin) ships two MCP servers. They do different jobs, so pick the right one before you call anything.
|
|
9
|
+
|
|
10
|
+
**Prerequisite:** this skill is useful only when the servers are connected. Installing these skills on their own (for example with `npx skills add`) does **not** connect them. Check your tool list for the Dodo tools; if they are missing, install the agent plugin, which registers both servers, or add them by hand as described under [Authentication](#authentication). Without them, fall back to the other skills and the docs at docs.dodopayments.com.
|
|
11
|
+
|
|
12
|
+
| Server | URL | Auth | Use it for |
|
|
13
|
+
|--------|-----|------|------------|
|
|
14
|
+
| `dodo-knowledge` | `https://knowledge.dodopayments.com/mcp` | None | Semantic search over Dodo Payments docs, API reference and SDK docs |
|
|
15
|
+
| `dodopayments-api` | `https://mcp.dodopayments.com/mcp` | OAuth in the browser, or a local API key | Reading and changing real resources in your Dodo account |
|
|
16
|
+
|
|
17
|
+
## When to call which
|
|
18
|
+
|
|
19
|
+
### Call `dodo-knowledge` first, before writing integration code
|
|
20
|
+
|
|
21
|
+
Your memory of the Dodo API can be stale. Search the docs whenever you need:
|
|
22
|
+
|
|
23
|
+
- Request and response shapes (for example checkout session fields, subscription plan change params).
|
|
24
|
+
- Webhook event names and payload fields.
|
|
25
|
+
- SDK method names and which endpoints are deprecated.
|
|
26
|
+
- Base URLs (`https://test.dodopayments.com` for test mode, `https://live.dodopayments.com` for live mode).
|
|
27
|
+
|
|
28
|
+
Prefer a docs result over what you remember. If the docs and your memory disagree, the docs win. Put the framework or language in the query for sharper results, for example "Next.js webhook handler verify signature".
|
|
29
|
+
|
|
30
|
+
### Call `dodopayments-api` to act on real data
|
|
31
|
+
|
|
32
|
+
Use it when the task needs the user's actual account:
|
|
33
|
+
|
|
34
|
+
- List or create products and prices.
|
|
35
|
+
- Create a checkout session to test a flow end to end.
|
|
36
|
+
- Look up a customer, payment or subscription by ID.
|
|
37
|
+
- Inspect or register webhook endpoints.
|
|
38
|
+
- Issue a refund or cancel a subscription (only after confirmation, see Safety).
|
|
39
|
+
|
|
40
|
+
Don't use the API server to answer "how does X work" questions. That's what `dodo-knowledge` is for.
|
|
41
|
+
|
|
42
|
+
## How the API server works: Code Mode
|
|
43
|
+
|
|
44
|
+
The Dodo Payments MCP server uses **Code Mode**. Instead of one tool per endpoint, it exposes two tools:
|
|
45
|
+
|
|
46
|
+
1. **Docs search**: query the API and SDK documentation.
|
|
47
|
+
2. **Code execution** (`execute`): write TypeScript against the `dodopayments` SDK. It runs in an isolated sandbox with no network or filesystem access, and whatever the code returns or prints comes back as the tool result.
|
|
48
|
+
|
|
49
|
+
What this means for you:
|
|
50
|
+
|
|
51
|
+
- Look at the tool list your client exposes for the exact tool names. Don't invent per-endpoint tool names like `create_product`; they don't exist in Code Mode.
|
|
52
|
+
- The API key is injected server-side. You never pass a key in tool parameters, and you shouldn't try to read one from inside the sandbox.
|
|
53
|
+
- Batch related calls into one script and return only what you need (IDs, statuses, counts), not whole objects. One script with 20 calls beats 20 round trips.
|
|
54
|
+
- Search docs (either server) for the SDK method shape before writing the script, then execute.
|
|
55
|
+
|
|
56
|
+
Typical read-only flow:
|
|
57
|
+
|
|
58
|
+
1. Search docs for the method, for example "list subscriptions for a customer".
|
|
59
|
+
2. Run a short `execute` script that calls it and returns a trimmed summary.
|
|
60
|
+
3. Report the result to the user.
|
|
61
|
+
|
|
62
|
+
## Safety rules
|
|
63
|
+
|
|
64
|
+
These are non-negotiable when the API server is connected.
|
|
65
|
+
|
|
66
|
+
1. **Default to test mode.** For the local server, `DODO_PAYMENTS_ENVIRONMENT` defaults to `live_mode` when unset, so set `test_mode` explicitly during development. Use `dodo_test_` keys with test mode and `dodo_live_` keys with live mode.
|
|
67
|
+
2. **Know which mode you're in before any write.** With the local server, it is `DODO_PAYMENTS_ENVIRONMENT`. With the remote OAuth server the mode follows the account the user signed in to and is not visible in the tool configuration, so **always ask the user to confirm test or live mode before the first write in a session**. A read can help you check: for example, if the user gives an ID, retrieving it succeeds only in the mode it was created in.
|
|
68
|
+
3. **Read before write.** Retrieve the resource first, show the user what will change, then act.
|
|
69
|
+
4. **Confirm every live mode write with the user**, naming the exact resource ID and the effect. This is mandatory for:
|
|
70
|
+
- refunds (full or partial),
|
|
71
|
+
- subscription cancellations and plan changes,
|
|
72
|
+
- anything that moves money or affects payouts,
|
|
73
|
+
- deleting or archiving products and discounts,
|
|
74
|
+
- changing or deleting webhook endpoints.
|
|
75
|
+
5. **Never print secrets.** Don't echo API keys, webhook signing keys, or full card or bank details into chat, logs, or files. Don't return them from `execute` scripts.
|
|
76
|
+
6. **Don't retry writes blindly.** If a create or refund call errors or times out, read the current state first (list refunds for the payment, fetch the subscription) before trying again, so you don't double-refund or create duplicates.
|
|
77
|
+
7. **Least privilege.** Dodo API keys can be created with a read-only access level. For inspection-only work with the local server, suggest a read-only key.
|
|
78
|
+
|
|
79
|
+
## Authentication
|
|
80
|
+
|
|
81
|
+
### Remote server (recommended)
|
|
82
|
+
|
|
83
|
+
The plugin points at `https://mcp.dodopayments.com/mcp`. The first time a tool is called, the client opens a browser window to sign in to Dodo Payments. Finish sign-in there, then retry the call.
|
|
84
|
+
|
|
85
|
+
Both servers speak Streamable HTTP, so clients connect to the URL directly (no bridge process). Client setup, if the user is wiring it up by hand:
|
|
86
|
+
|
|
87
|
+
- **Claude Code**: `claude mcp add --transport http dodopayments-api https://mcp.dodopayments.com/mcp`
|
|
88
|
+
- **Cursor and other `.mcp.json` clients**: `{"mcpServers": {"dodopayments-api": {"type": "http", "url": "https://mcp.dodopayments.com/mcp"}}}`
|
|
89
|
+
- **VS Code** (`.vscode/mcp.json` uses a top-level `servers` key): `{"servers": {"dodopayments-api": {"type": "http", "url": "https://mcp.dodopayments.com/mcp"}}}`
|
|
90
|
+
- **Codex CLI**: `codex mcp add dodopayments-api --url https://mcp.dodopayments.com/mcp`, then `codex mcp login dodopayments-api`
|
|
91
|
+
- **Claude.ai**: Settings, Connectors, Add Custom Connector, paste the URL.
|
|
92
|
+
|
|
93
|
+
`dodo-knowledge` is configured the same way with `https://knowledge.dodopayments.com/mcp` and needs no sign-in.
|
|
94
|
+
|
|
95
|
+
For a client that cannot dial Streamable HTTP or run MCP OAuth itself, the `mcp-remote` bridge still works as a fallback: run `npx -y mcp-remote@latest https://mcp.dodopayments.com/mcp` as a stdio server.
|
|
96
|
+
|
|
97
|
+
### Local stdio server
|
|
98
|
+
|
|
99
|
+
Run the server on the user's machine with an API key:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"mcpServers": {
|
|
104
|
+
"dodopayments_api": {
|
|
105
|
+
"command": "npx",
|
|
106
|
+
"args": ["-y", "dodopayments-mcp@latest"],
|
|
107
|
+
"env": {
|
|
108
|
+
"DODO_PAYMENTS_API_KEY": "dodo_test_...",
|
|
109
|
+
"DODO_PAYMENTS_WEBHOOK_KEY": "your_webhook_key",
|
|
110
|
+
"DODO_PAYMENTS_ENVIRONMENT": "test_mode"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Variable | Required | Notes |
|
|
118
|
+
|----------|----------|-------|
|
|
119
|
+
| `DODO_PAYMENTS_API_KEY` | Yes | From the dashboard, Developer, API Keys |
|
|
120
|
+
| `DODO_PAYMENTS_WEBHOOK_KEY` | No | Webhook signing key |
|
|
121
|
+
| `DODO_PAYMENTS_ENVIRONMENT` | No | `test_mode` or `live_mode`; defaults to `live_mode` |
|
|
122
|
+
|
|
123
|
+
The local `execute` tool needs Deno on `PATH` (Deno 2.8 or earlier for now) and works on macOS and Linux only. On Windows, use WSL2 or the remote server. Docs search still works without Deno.
|
|
124
|
+
|
|
125
|
+
Tell the user to keep the key in their client config or environment, never in a committed file.
|
|
126
|
+
|
|
127
|
+
## Troubleshooting
|
|
128
|
+
|
|
129
|
+
**"Needs authentication" or 401 from the remote server**
|
|
130
|
+
The OAuth sign-in hasn't happened or has expired. Ask the user to complete the browser sign-in and retry. If it keeps failing, sign out and back in from the client's MCP menu (for example `/mcp` in Claude Code, `codex mcp login`), then restart the client. Clients that still use the older `mcp-remote` bridge cache tokens in `~/.mcp-auth`; clearing that folder forces a fresh sign-in.
|
|
131
|
+
|
|
132
|
+
**Authentication errors with the local server**
|
|
133
|
+
The key and environment don't match. A `dodo_test_` key needs `DODO_PAYMENTS_ENVIRONMENT=test_mode`; a `dodo_live_` key needs `live_mode`. If it still fails, have the user generate a new key in the dashboard.
|
|
134
|
+
|
|
135
|
+
**Empty lists or "not found" for an ID the user just gave you**
|
|
136
|
+
You're probably in the wrong mode. Test mode and live mode data are fully separate. Confirm which mode the ID came from before concluding it doesn't exist.
|
|
137
|
+
|
|
138
|
+
**`execute` errors locally but docs search works**
|
|
139
|
+
Deno is missing, too new, or you're on Windows. See the local server notes above.
|
|
140
|
+
|
|
141
|
+
**Tool execution failures**
|
|
142
|
+
Read the API error in the result; it usually names the bad field. Re-check the method shape with `dodo-knowledge`, then fix the script. You can also have the user reproduce the call with curl against `https://test.dodopayments.com` to rule out the MCP layer.
|
|
143
|
+
|
|
144
|
+
**Rate limiting (429)**
|
|
145
|
+
Slow down, batch work into fewer scripts, and back off before retrying. Don't loop on writes.
|
|
146
|
+
|
|
147
|
+
## Related skills
|
|
148
|
+
|
|
149
|
+
- `checkout-integration`: building checkout sessions in app code.
|
|
150
|
+
- `webhook-integration`: verifying and handling webhook events.
|
|
151
|
+
- `testing-and-go-live`: moving from test mode to live mode safely.
|
|
152
|
+
- `dodo-best-practices`: overall integration guidance.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: framework-adapters
|
|
3
|
-
description:
|
|
3
|
+
description: Official @dodopayments/* framework adapters that mount Checkout, CustomerPortal, and Webhooks route handlers. Use when integrating Dodo Payments into Next.js, Express, Fastify, Hono, Astro, Remix, SvelteKit, Nuxt, TanStack, Bun, or Convex, such as @dodopayments/nextjs; use domain skills for payment and subscription logic.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Framework Adapters
|
|
@@ -21,10 +21,12 @@ Dodo publishes `@dodopayments/*` packages that wrap the core SDK with framework-
|
|
|
21
21
|
|
|
22
22
|
Each adapter exposes three handler families:
|
|
23
23
|
|
|
24
|
-
- **Checkout:** static (GET only), dynamic (POST
|
|
24
|
+
- **Checkout:** static (GET only), dynamic (POST, creates a payment or subscription via the deprecated `POST /payments` / `POST /subscriptions` endpoints - existing integrations only), or session (POST with a checkout session payload - use this for new integrations).
|
|
25
25
|
- **CustomerPortal:** generates a time-bound portal session link.
|
|
26
26
|
- **Webhooks:** verifies webhook signatures and dispatches typed events.
|
|
27
27
|
|
|
28
|
+
> **Security - CustomerPortal does not authenticate.** Every `CustomerPortal` handler opens the portal for whatever `?customer_id=` it receives. Mounted as-is, any visitor can open any customer's portal by guessing or enumerating IDs. Put the route behind your own authentication and resolve the customer ID **server-side from the signed-in session**; never accept it from the client. The portal examples below do this.
|
|
29
|
+
|
|
28
30
|
**Export names are not uniform across adapters.** Most export `Checkout` / `CustomerPortal` / `Webhooks`, but two differ, and the return shapes differ as well. Check this table before writing imports:
|
|
29
31
|
|
|
30
32
|
| Adapter | Checkout export | Portal export | Handler shape |
|
|
@@ -34,7 +36,7 @@ Each adapter exposes three handler families:
|
|
|
34
36
|
| `fastify` | `Checkout` | `CustomerPortal` | returns `{ getHandler, postHandler }` |
|
|
35
37
|
| `sveltekit` | `Checkout` | `CustomerPortal` | returns `{ GET, POST }` / `{ GET }` |
|
|
36
38
|
| `nuxt` | `checkoutHandler` (auto-imported) | `customerPortalHandler` | no import statement |
|
|
37
|
-
| `convex` | `
|
|
39
|
+
| `convex` | `checkout` (from `dodo.api()`) | `customerPortal` (from `dodo.api()`, uses `identify`) | `createDodoWebhookHandler` |
|
|
38
40
|
|
|
39
41
|
The adapters handle raw body preservation for webhook verification, environment variable mapping, and framework-specific request/response shapes. Checkout payload design belongs to the `checkout-integration` skill; webhook business logic belongs to `webhook-integration`; portal behavior belongs to `customer-management`.
|
|
40
42
|
|
|
@@ -71,7 +73,7 @@ DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success
|
|
|
71
73
|
NUXT_PRIVATE_BEARER_TOKEN=dodo_test_...
|
|
72
74
|
NUXT_PRIVATE_WEBHOOK_KEY=your-webhook-secret
|
|
73
75
|
NUXT_PRIVATE_ENVIRONMENT=test_mode
|
|
74
|
-
|
|
76
|
+
NUXT_PRIVATE_RETURN_URL=https://yourdomain.com/checkout/success
|
|
75
77
|
```
|
|
76
78
|
|
|
77
79
|
**Convex** uses dashboard environment variables (not local `.env`):
|
|
@@ -108,6 +110,7 @@ Defaulting to `test_mode` is deliberate: a missing or misspelled variable must n
|
|
|
108
110
|
```typescript
|
|
109
111
|
// app/api/checkout/route.ts
|
|
110
112
|
import { Checkout } from "@dodopayments/nextjs";
|
|
113
|
+
import { dodoEnvironment } from "@/lib/dodo-env";
|
|
111
114
|
|
|
112
115
|
export const GET = Checkout({
|
|
113
116
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
@@ -128,12 +131,26 @@ export const POST = Checkout({
|
|
|
128
131
|
|
|
129
132
|
```typescript
|
|
130
133
|
// app/api/customer-portal/route.ts
|
|
134
|
+
import { NextRequest } from "next/server";
|
|
131
135
|
import { CustomerPortal } from "@dodopayments/nextjs";
|
|
136
|
+
import { dodoEnvironment } from "@/lib/dodo-env";
|
|
137
|
+
// your auth helper
|
|
138
|
+
import { getSessionUser } from "@/lib/auth";
|
|
132
139
|
|
|
133
|
-
|
|
140
|
+
const portal = CustomerPortal({
|
|
134
141
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
135
142
|
environment: dodoEnvironment,
|
|
136
143
|
});
|
|
144
|
+
|
|
145
|
+
export async function GET(req: NextRequest) {
|
|
146
|
+
const user = await getSessionUser(req);
|
|
147
|
+
if (!user?.dodoCustomerId) return new Response("Unauthorized", { status: 401 });
|
|
148
|
+
|
|
149
|
+
// Overwrite any client-supplied ?customer_id= with the signed-in user's ID.
|
|
150
|
+
const url = new URL(req.url);
|
|
151
|
+
url.searchParams.set("customer_id", user.dodoCustomerId);
|
|
152
|
+
return portal(new NextRequest(url, req));
|
|
153
|
+
}
|
|
137
154
|
```
|
|
138
155
|
|
|
139
156
|
### Webhooks
|
|
@@ -164,6 +181,8 @@ import { checkoutHandler } from "@dodopayments/express";
|
|
|
164
181
|
import { dodoEnvironment } from "./lib/dodo-env";
|
|
165
182
|
|
|
166
183
|
const app = express();
|
|
184
|
+
// Required before the POST route: the session/dynamic handlers read req.body.
|
|
185
|
+
app.use(express.json());
|
|
167
186
|
|
|
168
187
|
app.get("/api/checkout", checkoutHandler({
|
|
169
188
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
@@ -182,13 +201,24 @@ app.post("/api/checkout", checkoutHandler({
|
|
|
182
201
|
|
|
183
202
|
### Customer Portal
|
|
184
203
|
|
|
204
|
+
The adapter's `CustomerPortal` reads `customer_id` from `req.query`, which you cannot safely rewrite in Express 5. Behind your auth middleware, call the SDK directly with the session's customer ID instead:
|
|
205
|
+
|
|
185
206
|
```typescript
|
|
186
|
-
import
|
|
207
|
+
import DodoPayments from "dodopayments";
|
|
208
|
+
import { dodoEnvironment } from "./lib/dodo-env";
|
|
187
209
|
|
|
188
|
-
|
|
210
|
+
const client = new DodoPayments({
|
|
189
211
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
190
212
|
environment: dodoEnvironment,
|
|
191
|
-
})
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// requireAuth = your session middleware; it must set req.user.
|
|
216
|
+
app.get("/api/customer-portal", requireAuth, async (req, res) => {
|
|
217
|
+
const customerId = req.user?.dodoCustomerId; // from the session, never from req.query
|
|
218
|
+
if (!customerId) return res.status(401).send("Unauthorized");
|
|
219
|
+
const session = await client.customers.customerPortal.create(customerId);
|
|
220
|
+
res.redirect(session.link);
|
|
221
|
+
});
|
|
192
222
|
```
|
|
193
223
|
|
|
194
224
|
### Webhooks
|
|
@@ -196,7 +226,10 @@ app.get("/api/customer-portal", CustomerPortal({
|
|
|
196
226
|
```typescript
|
|
197
227
|
import { Webhooks } from "@dodopayments/express";
|
|
198
228
|
|
|
199
|
-
|
|
229
|
+
// The Express handler verifies the signature against the PARSED req.body, so it
|
|
230
|
+
// needs express.json() (skip this line if already registered). Do NOT use express.raw() here - the
|
|
231
|
+
// handler would see a Buffer and reject every request.
|
|
232
|
+
app.use(express.json());
|
|
200
233
|
|
|
201
234
|
app.post("/api/webhook", Webhooks({
|
|
202
235
|
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
|
|
@@ -269,6 +302,7 @@ fastify.post("/api/webhook", Webhooks({
|
|
|
269
302
|
```typescript
|
|
270
303
|
import { Hono } from "hono";
|
|
271
304
|
import { Checkout } from "@dodopayments/hono";
|
|
305
|
+
import { dodoEnvironment } from "./lib/dodo-env";
|
|
272
306
|
|
|
273
307
|
const app = new Hono();
|
|
274
308
|
|
|
@@ -289,13 +323,24 @@ app.post("/api/checkout", Checkout({
|
|
|
289
323
|
|
|
290
324
|
### Customer Portal
|
|
291
325
|
|
|
326
|
+
The adapter's `CustomerPortal` reads `c.req.query("customer_id")`. Behind your auth middleware, call the SDK directly with the session's customer ID instead:
|
|
327
|
+
|
|
292
328
|
```typescript
|
|
293
|
-
import
|
|
329
|
+
import DodoPayments from "dodopayments";
|
|
330
|
+
import { dodoEnvironment } from "./lib/dodo-env";
|
|
294
331
|
|
|
295
|
-
|
|
332
|
+
const client = new DodoPayments({
|
|
296
333
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
297
334
|
environment: dodoEnvironment,
|
|
298
|
-
})
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Assumes your auth middleware ran first and stored the user on the context.
|
|
338
|
+
app.get("/api/customer-portal", async (c) => {
|
|
339
|
+
const customerId = c.get("user")?.dodoCustomerId; // from the session, never from the query
|
|
340
|
+
if (!customerId) return c.text("Unauthorized", 401);
|
|
341
|
+
const session = await client.customers.customerPortal.create(customerId);
|
|
342
|
+
return c.redirect(session.link);
|
|
343
|
+
});
|
|
299
344
|
```
|
|
300
345
|
|
|
301
346
|
### Webhooks
|
|
@@ -373,6 +418,7 @@ export const POST = Webhooks({
|
|
|
373
418
|
// app/routes/api.checkout.tsx
|
|
374
419
|
import { Checkout } from "@dodopayments/remix";
|
|
375
420
|
import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
|
|
421
|
+
import { dodoEnvironment } from "~/lib/dodo-env";
|
|
376
422
|
|
|
377
423
|
const checkoutHandler = Checkout({
|
|
378
424
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
@@ -391,13 +437,24 @@ export const action = ({ request }: ActionFunctionArgs) => checkoutHandler(reque
|
|
|
391
437
|
// app/routes/api.customer-portal.tsx
|
|
392
438
|
import { CustomerPortal } from "@dodopayments/remix";
|
|
393
439
|
import type { LoaderFunctionArgs } from "@remix-run/node";
|
|
440
|
+
import { dodoEnvironment } from "~/lib/dodo-env";
|
|
441
|
+
// your auth helper
|
|
442
|
+
import { getSessionUser } from "~/lib/auth.server";
|
|
394
443
|
|
|
395
444
|
const portalHandler = CustomerPortal({
|
|
396
445
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
397
446
|
environment: dodoEnvironment,
|
|
398
447
|
});
|
|
399
448
|
|
|
400
|
-
export const loader = ({ request }: LoaderFunctionArgs) =>
|
|
449
|
+
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
450
|
+
const user = await getSessionUser(request);
|
|
451
|
+
if (!user?.dodoCustomerId) return new Response("Unauthorized", { status: 401 });
|
|
452
|
+
|
|
453
|
+
// Overwrite any client-supplied ?customer_id= with the signed-in user's ID.
|
|
454
|
+
const url = new URL(request.url);
|
|
455
|
+
url.searchParams.set("customer_id", user.dodoCustomerId);
|
|
456
|
+
return portalHandler(new Request(url, request));
|
|
457
|
+
};
|
|
401
458
|
```
|
|
402
459
|
|
|
403
460
|
### Webhooks
|
|
@@ -428,15 +485,26 @@ export const action = ({ request }: ActionFunctionArgs) =>
|
|
|
428
485
|
import { Checkout } from "@dodopayments/sveltekit";
|
|
429
486
|
import { DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_RETURN_URL, DODO_PAYMENTS_ENVIRONMENT } from "$env/static/private";
|
|
430
487
|
|
|
431
|
-
const
|
|
488
|
+
const dodoEnvironment = DODO_PAYMENTS_ENVIRONMENT === "live_mode" ? "live_mode" : "test_mode";
|
|
489
|
+
|
|
490
|
+
// Checkout() returns { GET, POST }, not a handler. Export GET from a "static"
|
|
491
|
+
// handler - the GET of a "session"/"dynamic" handler returns 400.
|
|
492
|
+
const staticCheckout = Checkout({
|
|
493
|
+
bearerToken: DODO_PAYMENTS_API_KEY,
|
|
494
|
+
returnUrl: DODO_PAYMENTS_RETURN_URL,
|
|
495
|
+
environment: dodoEnvironment,
|
|
496
|
+
type: "static",
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const sessionCheckout = Checkout({
|
|
432
500
|
bearerToken: DODO_PAYMENTS_API_KEY,
|
|
433
501
|
returnUrl: DODO_PAYMENTS_RETURN_URL,
|
|
434
|
-
environment:
|
|
502
|
+
environment: dodoEnvironment,
|
|
435
503
|
type: "session",
|
|
436
504
|
});
|
|
437
505
|
|
|
438
|
-
export const GET =
|
|
439
|
-
export const POST =
|
|
506
|
+
export const GET = staticCheckout.GET;
|
|
507
|
+
export const POST = sessionCheckout.POST;
|
|
440
508
|
```
|
|
441
509
|
|
|
442
510
|
### Webhooks
|
|
@@ -469,7 +537,7 @@ export default defineNuxtConfig({
|
|
|
469
537
|
bearerToken: process.env.NUXT_PRIVATE_BEARER_TOKEN,
|
|
470
538
|
webhookKey: process.env.NUXT_PRIVATE_WEBHOOK_KEY,
|
|
471
539
|
environment: process.env.NUXT_PRIVATE_ENVIRONMENT,
|
|
472
|
-
returnUrl: process.env.
|
|
540
|
+
returnUrl: process.env.NUXT_PRIVATE_RETURN_URL,
|
|
473
541
|
},
|
|
474
542
|
},
|
|
475
543
|
});
|
|
@@ -518,7 +586,8 @@ export default Webhooks({
|
|
|
518
586
|
```typescript
|
|
519
587
|
// src/routes/api/checkout.ts
|
|
520
588
|
import { Checkout } from "@dodopayments/tanstack";
|
|
521
|
-
|
|
589
|
+
// src/lib/dodo-env.ts
|
|
590
|
+
import { dodoEnvironment } from "../../lib/dodo-env";
|
|
522
591
|
|
|
523
592
|
export const GET = Checkout({
|
|
524
593
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
@@ -545,6 +614,9 @@ TanStack Start's server-route definition API has changed across releases (`creat
|
|
|
545
614
|
|
|
546
615
|
```typescript
|
|
547
616
|
import { Checkout, CustomerPortal } from "@dodopayments/bun";
|
|
617
|
+
import { dodoEnvironment } from "./lib/dodo-env";
|
|
618
|
+
// your auth helper
|
|
619
|
+
import { getSessionUser } from "./lib/auth";
|
|
548
620
|
|
|
549
621
|
const checkoutHandler = Checkout({
|
|
550
622
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
@@ -560,14 +632,18 @@ const portalHandler = CustomerPortal({
|
|
|
560
632
|
|
|
561
633
|
Bun.serve({
|
|
562
634
|
port: 3000,
|
|
563
|
-
fetch(request) {
|
|
635
|
+
async fetch(request) {
|
|
564
636
|
const url = new URL(request.url);
|
|
565
637
|
|
|
566
638
|
if (url.pathname === "/api/checkout") {
|
|
567
639
|
return checkoutHandler(request);
|
|
568
640
|
}
|
|
569
641
|
if (url.pathname === "/api/customer-portal" && request.method === "GET") {
|
|
570
|
-
|
|
642
|
+
const user = await getSessionUser(request);
|
|
643
|
+
if (!user?.dodoCustomerId) return new Response("Unauthorized", { status: 401 });
|
|
644
|
+
// Overwrite any client-supplied ?customer_id= with the signed-in user's ID.
|
|
645
|
+
url.searchParams.set("customer_id", user.dodoCustomerId);
|
|
646
|
+
return portalHandler(new Request(url, request));
|
|
571
647
|
}
|
|
572
648
|
|
|
573
649
|
return new Response("Not Found", { status: 404 });
|
|
@@ -591,20 +667,74 @@ app.use(dodopayments);
|
|
|
591
667
|
export default app;
|
|
592
668
|
```
|
|
593
669
|
|
|
594
|
-
|
|
670
|
+
Run `npx convex dev` once to generate `components.dodopayments`. `identify` needs an app-owned lookup from the signed-in user to their Dodo customer ID. This assumes a `customers` table with `authId` and `dodoCustomerId` fields and a `by_auth_id` index; adapt it to your schema:
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
// convex/customers.ts
|
|
674
|
+
import { internalQuery } from "./_generated/server";
|
|
675
|
+
import { v } from "convex/values";
|
|
676
|
+
|
|
677
|
+
export const getByAuthId = internalQuery({
|
|
678
|
+
args: { authId: v.string() },
|
|
679
|
+
handler: async (ctx, { authId }) =>
|
|
680
|
+
ctx.db
|
|
681
|
+
.query("customers")
|
|
682
|
+
.withIndex("by_auth_id", (q) => q.eq("authId", authId))
|
|
683
|
+
.unique(),
|
|
684
|
+
});
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Then create a `DodoPayments` client in a local module and export its API methods:
|
|
595
688
|
|
|
596
689
|
```typescript
|
|
597
|
-
// convex/
|
|
598
|
-
import {
|
|
599
|
-
import { components } from "./_generated/
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
690
|
+
// convex/dodo.ts
|
|
691
|
+
import { DodoPayments } from "@dodopayments/convex";
|
|
692
|
+
import { components, internal } from "./_generated/api";
|
|
693
|
+
|
|
694
|
+
const apiKey = process.env.DODO_PAYMENTS_API_KEY;
|
|
695
|
+
if (!apiKey) throw new Error("DODO_PAYMENTS_API_KEY is not set");
|
|
696
|
+
|
|
697
|
+
export const dodo = new DodoPayments(components.dodopayments, {
|
|
698
|
+
// Maps the signed-in Convex user to a Dodo customer; used by customerPortal.
|
|
699
|
+
identify: async (ctx) => {
|
|
700
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
701
|
+
if (!identity) return null;
|
|
702
|
+
const customer = await ctx.runQuery(internal.customers.getByAuthId, {
|
|
703
|
+
authId: identity.subject,
|
|
704
|
+
});
|
|
705
|
+
return customer ? { dodoCustomerId: customer.dodoCustomerId } : null;
|
|
706
|
+
},
|
|
707
|
+
apiKey,
|
|
708
|
+
environment: process.env.DODO_PAYMENTS_ENVIRONMENT === "live_mode" ? "live_mode" : "test_mode",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
export const { checkout, customerPortal } = dodo.api();
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Call `checkout` from an **action** (mutations cannot make network calls), passing the checkout session fields in `payload`. The client sends a plan slug; the server maps it to a product ID and owns the return URL, so a caller cannot pick an arbitrary product or redirect target:
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
// convex/payments.ts
|
|
718
|
+
import { action } from "./_generated/server";
|
|
719
|
+
import { v } from "convex/values";
|
|
720
|
+
import { checkout } from "./dodo";
|
|
721
|
+
|
|
722
|
+
// Public plan slug -> Dodo product ID. Never accept a product_id from the client.
|
|
723
|
+
const PRODUCT_IDS: Readonly<Record<string, string>> = {
|
|
724
|
+
starter: "pdt_starter_monthly",
|
|
725
|
+
pro: "pdt_pro_monthly",
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
export const createCheckout = action({
|
|
729
|
+
args: { plan: v.string() },
|
|
730
|
+
handler: async (ctx, { plan }) => {
|
|
731
|
+
if (!Object.hasOwn(PRODUCT_IDS, plan)) throw new Error(`Unknown plan: ${plan}`);
|
|
732
|
+
// Returns { checkout_url }
|
|
733
|
+
return await checkout(ctx, {
|
|
734
|
+
payload: {
|
|
735
|
+
product_cart: [{ product_id: PRODUCT_IDS[plan], quantity: 1 }],
|
|
736
|
+
return_url: process.env.DODO_PAYMENTS_RETURN_URL,
|
|
737
|
+
},
|
|
608
738
|
});
|
|
609
739
|
},
|
|
610
740
|
});
|
|
@@ -618,7 +748,7 @@ Convex only supports session checkout, not static or dynamic modes.
|
|
|
618
748
|
|
|
619
749
|
2. **Wrong webhook variable name:** Check whether your adapter uses `DODO_PAYMENTS_WEBHOOK_KEY` or `DODO_PAYMENTS_WEBHOOK_SECRET`. The docs are inconsistent; use the name your adapter actually references.
|
|
620
750
|
|
|
621
|
-
3. **Forgetting raw body preservation:** Webhook handlers must receive the raw request body, not a re-parsed JSON object. Fastify requires an explicit string body parser; Express
|
|
751
|
+
3. **Forgetting raw body preservation:** Webhook handlers must receive the raw request body, not a re-parsed JSON object. Fastify requires an explicit string body parser; other frameworks handle this automatically. **Express is the exception:** its `Webhooks` handler verifies against the parsed `req.body`, so register `express.json()` before the route and do not use `express.raw()`, which makes every verification fail. Webhook signature verification is covered in the `webhook-integration` skill.
|
|
622
752
|
|
|
623
753
|
4. **Mixing framework conventions:** Each framework has its own request/response shape. Don't try to use a Next.js handler in Express or vice versa. Use the adapter for your framework.
|
|
624
754
|
|
|
@@ -626,7 +756,9 @@ Convex only supports session checkout, not static or dynamic modes.
|
|
|
626
756
|
|
|
627
757
|
6. **Skipping environment setup:** The adapters won't work without `DODO_PAYMENTS_API_KEY` and `DODO_PAYMENTS_ENVIRONMENT`. Set these before testing.
|
|
628
758
|
|
|
629
|
-
7. **
|
|
759
|
+
7. **Exposing CustomerPortal unauthenticated:** The portal handlers trust `?customer_id=` from the request. Resolve the ID from the signed-in session on the server, as in the examples above, or anyone can open any customer's portal.
|
|
760
|
+
|
|
761
|
+
8. **Using `@dodopayments/core` directly:** The core package is an internal dependency, not a documented public entry point. Use the framework adapter for your stack.
|
|
630
762
|
|
|
631
763
|
## Resources
|
|
632
764
|
|