@dodopayments/opencode-plugin 0.6.1 → 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 (36) hide show
  1. package/package.json +1 -1
  2. package/skills/billing-sdk/SKILL.md +8 -193
  3. package/skills/billing-sdk/references/pricing-page.md +197 -0
  4. package/skills/checkout-integration/SKILL.md +14 -449
  5. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  6. package/skills/checkout-integration/references/server-examples.md +317 -0
  7. package/skills/credit-based-billing/SKILL.md +5 -151
  8. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  9. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  10. package/skills/framework-adapters/SKILL.md +38 -527
  11. package/skills/framework-adapters/references/astro.md +52 -0
  12. package/skills/framework-adapters/references/bun.md +45 -0
  13. package/skills/framework-adapters/references/convex.md +91 -0
  14. package/skills/framework-adapters/references/express.md +72 -0
  15. package/skills/framework-adapters/references/fastify.md +54 -0
  16. package/skills/framework-adapters/references/hono.md +63 -0
  17. package/skills/framework-adapters/references/nuxt.md +53 -0
  18. package/skills/framework-adapters/references/remix.md +65 -0
  19. package/skills/framework-adapters/references/sveltekit.md +49 -0
  20. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  21. package/skills/license-keys/SKILL.md +8 -303
  22. package/skills/license-keys/references/cli-tool.md +143 -0
  23. package/skills/license-keys/references/desktop-app.md +170 -0
  24. package/skills/mobile-checkout/SKILL.md +25 -313
  25. package/skills/mobile-checkout/references/android.md +69 -0
  26. package/skills/mobile-checkout/references/flutter.md +97 -0
  27. package/skills/mobile-checkout/references/ios.md +66 -0
  28. package/skills/mobile-checkout/references/react-native.md +100 -0
  29. package/skills/subscription-integration/SKILL.md +3 -77
  30. package/skills/subscription-integration/references/webhook-events.md +82 -0
  31. package/skills/usage-based-billing/SKILL.md +5 -114
  32. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  33. package/skills/webhook-integration/SKILL.md +28 -277
  34. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  35. package/skills/webhook-integration/references/event-catalog.md +103 -0
  36. package/skills/webhook-integration/references/local-testing.md +42 -0
@@ -0,0 +1,45 @@
1
+ # Framework Adapters: Bun
2
+
3
+
4
+ **Package:** `@dodopayments/bun`
5
+
6
+ ### Checkout and Portal
7
+
8
+ ```typescript
9
+ import { Checkout, CustomerPortal } from "@dodopayments/bun";
10
+ import { dodoEnvironment } from "./lib/dodo-env";
11
+ // your auth helper
12
+ import { getSessionUser } from "./lib/auth";
13
+
14
+ const checkoutHandler = Checkout({
15
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
16
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
17
+ environment: dodoEnvironment,
18
+ type: "session",
19
+ });
20
+
21
+ const portalHandler = CustomerPortal({
22
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
23
+ environment: dodoEnvironment,
24
+ });
25
+
26
+ Bun.serve({
27
+ port: 3000,
28
+ async fetch(request) {
29
+ const url = new URL(request.url);
30
+
31
+ if (url.pathname === "/api/checkout") {
32
+ return checkoutHandler(request);
33
+ }
34
+ if (url.pathname === "/api/customer-portal" && request.method === "GET") {
35
+ const user = await getSessionUser(request);
36
+ if (!user?.dodoCustomerId) return new Response("Unauthorized", { status: 401 });
37
+ // Overwrite any client-supplied ?customer_id= with the signed-in user's ID.
38
+ url.searchParams.set("customer_id", user.dodoCustomerId);
39
+ return portalHandler(new Request(url, request));
40
+ }
41
+
42
+ return new Response("Not Found", { status: 404 });
43
+ },
44
+ });
45
+ ```
@@ -0,0 +1,91 @@
1
+ # Framework Adapters: Convex
2
+
3
+
4
+ **Package:** `@dodopayments/convex`
5
+
6
+ Convex uses a component-based architecture. Register the component in `convex.config.ts`:
7
+
8
+ ```typescript
9
+ // convex/convex.config.ts
10
+ import { defineApp } from "convex/server";
11
+ import dodopayments from "@dodopayments/convex/convex.config";
12
+
13
+ const app = defineApp();
14
+ app.use(dodopayments);
15
+ export default app;
16
+ ```
17
+
18
+ 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:
19
+
20
+ ```typescript
21
+ // convex/customers.ts
22
+ import { internalQuery } from "./_generated/server";
23
+ import { v } from "convex/values";
24
+
25
+ export const getByAuthId = internalQuery({
26
+ args: { authId: v.string() },
27
+ handler: async (ctx, { authId }) =>
28
+ ctx.db
29
+ .query("customers")
30
+ .withIndex("by_auth_id", (q) => q.eq("authId", authId))
31
+ .unique(),
32
+ });
33
+ ```
34
+
35
+ Then create a `DodoPayments` client in a local module and export its API methods:
36
+
37
+ ```typescript
38
+ // convex/dodo.ts
39
+ import { DodoPayments } from "@dodopayments/convex";
40
+ import { components, internal } from "./_generated/api";
41
+
42
+ const apiKey = process.env.DODO_PAYMENTS_API_KEY;
43
+ if (!apiKey) throw new Error("DODO_PAYMENTS_API_KEY is not set");
44
+
45
+ export const dodo = new DodoPayments(components.dodopayments, {
46
+ // Maps the signed-in Convex user to a Dodo customer; used by customerPortal.
47
+ identify: async (ctx) => {
48
+ const identity = await ctx.auth.getUserIdentity();
49
+ if (!identity) return null;
50
+ const customer = await ctx.runQuery(internal.customers.getByAuthId, {
51
+ authId: identity.subject,
52
+ });
53
+ return customer ? { dodoCustomerId: customer.dodoCustomerId } : null;
54
+ },
55
+ apiKey,
56
+ environment: process.env.DODO_PAYMENTS_ENVIRONMENT === "live_mode" ? "live_mode" : "test_mode",
57
+ });
58
+
59
+ export const { checkout, customerPortal } = dodo.api();
60
+ ```
61
+
62
+ 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:
63
+
64
+ ```typescript
65
+ // convex/payments.ts
66
+ import { action } from "./_generated/server";
67
+ import { v } from "convex/values";
68
+ import { checkout } from "./dodo";
69
+
70
+ // Public plan slug -> Dodo product ID. Never accept a product_id from the client.
71
+ const PRODUCT_IDS: Readonly<Record<string, string>> = {
72
+ starter: "pdt_starter_monthly",
73
+ pro: "pdt_pro_monthly",
74
+ };
75
+
76
+ export const createCheckout = action({
77
+ args: { plan: v.string() },
78
+ handler: async (ctx, { plan }) => {
79
+ if (!Object.hasOwn(PRODUCT_IDS, plan)) throw new Error(`Unknown plan: ${plan}`);
80
+ // Returns { checkout_url }
81
+ return await checkout(ctx, {
82
+ payload: {
83
+ product_cart: [{ product_id: PRODUCT_IDS[plan], quantity: 1 }],
84
+ return_url: process.env.DODO_PAYMENTS_RETURN_URL,
85
+ },
86
+ });
87
+ },
88
+ });
89
+ ```
90
+
91
+ Convex only supports session checkout, not static or dynamic modes.
@@ -0,0 +1,72 @@
1
+ # Framework Adapters: Express
2
+
3
+
4
+ **Package:** `@dodopayments/express`
5
+
6
+ ### Checkout
7
+
8
+ The Express adapter names its checkout export `checkoutHandler` in lowercase, unlike every other adapter. `import { Checkout } from "@dodopayments/express"` does not resolve.
9
+
10
+ ```typescript
11
+ import express from "express";
12
+ import { checkoutHandler } from "@dodopayments/express";
13
+ import { dodoEnvironment } from "./lib/dodo-env";
14
+
15
+ const app = express();
16
+ // Required before the POST route: the session/dynamic handlers read req.body.
17
+ app.use(express.json());
18
+
19
+ app.get("/api/checkout", checkoutHandler({
20
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
21
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
22
+ environment: dodoEnvironment,
23
+ type: "static",
24
+ }));
25
+
26
+ app.post("/api/checkout", checkoutHandler({
27
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
28
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
29
+ environment: dodoEnvironment,
30
+ type: "session",
31
+ }));
32
+ ```
33
+
34
+ ### Customer Portal
35
+
36
+ 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:
37
+
38
+ ```typescript
39
+ import DodoPayments from "dodopayments";
40
+ import { dodoEnvironment } from "./lib/dodo-env";
41
+
42
+ const client = new DodoPayments({
43
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
44
+ environment: dodoEnvironment,
45
+ });
46
+
47
+ // requireAuth = your session middleware; it must set req.user.
48
+ app.get("/api/customer-portal", requireAuth, async (req, res) => {
49
+ const customerId = req.user?.dodoCustomerId; // from the session, never from req.query
50
+ if (!customerId) return res.status(401).send("Unauthorized");
51
+ const session = await client.customers.customerPortal.create(customerId);
52
+ res.redirect(session.link);
53
+ });
54
+ ```
55
+
56
+ ### Webhooks
57
+
58
+ ```typescript
59
+ import { Webhooks } from "@dodopayments/express";
60
+
61
+ // The Express handler verifies the signature against the PARSED req.body, so it
62
+ // needs express.json() (skip this line if already registered). Do NOT use express.raw() here - the
63
+ // handler would see a Buffer and reject every request.
64
+ app.use(express.json());
65
+
66
+ app.post("/api/webhook", Webhooks({
67
+ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
68
+ onPayload: async (payload) => {
69
+ console.log("Webhook:", payload.type);
70
+ },
71
+ }));
72
+ ```
@@ -0,0 +1,54 @@
1
+ # Framework Adapters: Fastify
2
+
3
+
4
+ **Package:** `@dodopayments/fastify`
5
+
6
+ Fastify requires a string body parser to preserve the raw body for webhook verification.
7
+
8
+ ### Checkout
9
+
10
+ `Checkout(config)` returns an object with `getHandler` and `postHandler`, not a single callable. Build it once and mount each method, rather than calling the result.
11
+
12
+ ```typescript
13
+ import Fastify from "fastify";
14
+ import { Checkout } from "@dodopayments/fastify";
15
+ import { dodoEnvironment } from "./lib/dodo-env";
16
+
17
+ const fastify = Fastify();
18
+
19
+ const staticCheckout = Checkout({
20
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
21
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
22
+ environment: dodoEnvironment,
23
+ type: "static",
24
+ });
25
+
26
+ const sessionCheckout = Checkout({
27
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
28
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
29
+ environment: dodoEnvironment,
30
+ type: "session",
31
+ });
32
+
33
+ fastify.get("/api/checkout", staticCheckout.getHandler);
34
+ fastify.post("/api/checkout", sessionCheckout.postHandler);
35
+ ```
36
+
37
+ ### Webhooks
38
+
39
+ ```typescript
40
+ import { Webhooks } from "@dodopayments/fastify";
41
+
42
+ fastify.addContentTypeParser(
43
+ "application/json",
44
+ { parseAs: "string" },
45
+ (req, body, done) => done(null, body)
46
+ );
47
+
48
+ fastify.post("/api/webhook", Webhooks({
49
+ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
50
+ onPayload: async (payload) => {
51
+ console.log("Webhook:", payload.type);
52
+ },
53
+ }));
54
+ ```
@@ -0,0 +1,63 @@
1
+ # Framework Adapters: Hono
2
+
3
+
4
+ **Package:** `@dodopayments/hono`
5
+
6
+ ### Checkout
7
+
8
+ ```typescript
9
+ import { Hono } from "hono";
10
+ import { Checkout } from "@dodopayments/hono";
11
+ import { dodoEnvironment } from "./lib/dodo-env";
12
+
13
+ const app = new Hono();
14
+
15
+ app.get("/api/checkout", Checkout({
16
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
17
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
18
+ environment: dodoEnvironment,
19
+ type: "static",
20
+ }));
21
+
22
+ app.post("/api/checkout", Checkout({
23
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
24
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
25
+ environment: dodoEnvironment,
26
+ type: "session",
27
+ }));
28
+ ```
29
+
30
+ ### Customer Portal
31
+
32
+ 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:
33
+
34
+ ```typescript
35
+ import DodoPayments from "dodopayments";
36
+ import { dodoEnvironment } from "./lib/dodo-env";
37
+
38
+ const client = new DodoPayments({
39
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
40
+ environment: dodoEnvironment,
41
+ });
42
+
43
+ // Assumes your auth middleware ran first and stored the user on the context.
44
+ app.get("/api/customer-portal", async (c) => {
45
+ const customerId = c.get("user")?.dodoCustomerId; // from the session, never from the query
46
+ if (!customerId) return c.text("Unauthorized", 401);
47
+ const session = await client.customers.customerPortal.create(customerId);
48
+ return c.redirect(session.link);
49
+ });
50
+ ```
51
+
52
+ ### Webhooks
53
+
54
+ ```typescript
55
+ import { Webhooks } from "@dodopayments/hono";
56
+
57
+ app.post("/api/webhook", Webhooks({
58
+ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
59
+ onPayload: async (payload) => {
60
+ console.log("Webhook:", payload.type);
61
+ },
62
+ }));
63
+ ```
@@ -0,0 +1,53 @@
1
+ # Framework Adapters: Nuxt
2
+
3
+
4
+ **Package:** `@dodopayments/nuxt`
5
+
6
+ Add the module to `nuxt.config.ts` and configure runtime variables:
7
+
8
+ ```typescript
9
+ // nuxt.config.ts
10
+ export default defineNuxtConfig({
11
+ modules: ["@dodopayments/nuxt"],
12
+ runtimeConfig: {
13
+ private: {
14
+ bearerToken: process.env.NUXT_PRIVATE_BEARER_TOKEN,
15
+ webhookKey: process.env.NUXT_PRIVATE_WEBHOOK_KEY,
16
+ environment: process.env.NUXT_PRIVATE_ENVIRONMENT,
17
+ returnUrl: process.env.NUXT_PRIVATE_RETURN_URL,
18
+ },
19
+ },
20
+ });
21
+ ```
22
+
23
+ ### Checkout
24
+
25
+ The Nuxt module registers its handlers with `addServerImportsDir`, so `checkoutHandler`, `customerPortalHandler`, and `Webhooks` are **auto-imported** inside `server/`. Do not import them from `@dodopayments/nuxt` — that entry point exports only the Nuxt module itself, and a named import from it will not resolve.
26
+
27
+ ```typescript
28
+ // server/routes/api/checkout.ts
29
+ // checkoutHandler and useRuntimeConfig are auto-imported by the module.
30
+ const config = useRuntimeConfig();
31
+
32
+ export default checkoutHandler({
33
+ bearerToken: config.private.bearerToken,
34
+ returnUrl: config.private.returnUrl,
35
+ environment: config.private.environment,
36
+ type: "session",
37
+ });
38
+ ```
39
+
40
+ ### Webhooks
41
+
42
+ ```typescript
43
+ // server/routes/api/webhook.ts
44
+ // Webhooks and useRuntimeConfig are auto-imported by the module.
45
+ const config = useRuntimeConfig();
46
+
47
+ export default Webhooks({
48
+ webhookKey: config.private.webhookKey,
49
+ onPayload: async (payload) => {
50
+ console.log("Webhook:", payload.type);
51
+ },
52
+ });
53
+ ```
@@ -0,0 +1,65 @@
1
+ # Framework Adapters: Remix
2
+
3
+
4
+ **Package:** `@dodopayments/remix`
5
+
6
+ ### Checkout
7
+
8
+ ```typescript
9
+ // app/routes/api.checkout.tsx
10
+ import { Checkout } from "@dodopayments/remix";
11
+ import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
12
+ import { dodoEnvironment } from "~/lib/dodo-env";
13
+
14
+ const checkoutHandler = Checkout({
15
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
16
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
17
+ environment: dodoEnvironment,
18
+ type: "session",
19
+ });
20
+
21
+ export const loader = ({ request }: LoaderFunctionArgs) => checkoutHandler(request);
22
+ export const action = ({ request }: ActionFunctionArgs) => checkoutHandler(request);
23
+ ```
24
+
25
+ ### Customer Portal
26
+
27
+ ```typescript
28
+ // app/routes/api.customer-portal.tsx
29
+ import { CustomerPortal } from "@dodopayments/remix";
30
+ import type { LoaderFunctionArgs } from "@remix-run/node";
31
+ import { dodoEnvironment } from "~/lib/dodo-env";
32
+ // your auth helper
33
+ import { getSessionUser } from "~/lib/auth.server";
34
+
35
+ const portalHandler = CustomerPortal({
36
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
37
+ environment: dodoEnvironment,
38
+ });
39
+
40
+ export const loader = async ({ request }: LoaderFunctionArgs) => {
41
+ const user = await getSessionUser(request);
42
+ if (!user?.dodoCustomerId) return new Response("Unauthorized", { status: 401 });
43
+
44
+ // Overwrite any client-supplied ?customer_id= with the signed-in user's ID.
45
+ const url = new URL(request.url);
46
+ url.searchParams.set("customer_id", user.dodoCustomerId);
47
+ return portalHandler(new Request(url, request));
48
+ };
49
+ ```
50
+
51
+ ### Webhooks
52
+
53
+ ```typescript
54
+ // app/routes/api.webhook.tsx
55
+ import { Webhooks } from "@dodopayments/remix";
56
+ import type { ActionFunctionArgs } from "@remix-run/node";
57
+
58
+ export const action = ({ request }: ActionFunctionArgs) =>
59
+ Webhooks({
60
+ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
61
+ onPayload: async (payload) => {
62
+ console.log("Webhook:", payload.type);
63
+ },
64
+ })(request);
65
+ ```
@@ -0,0 +1,49 @@
1
+ # Framework Adapters: SvelteKit
2
+
3
+
4
+ **Package:** `@dodopayments/sveltekit`
5
+ **Route placement:** `src/routes/api/checkout/+server.ts`, `src/routes/api/customer-portal/+server.ts`, `src/routes/api/webhook/+server.ts`
6
+
7
+ ### Checkout
8
+
9
+ ```typescript
10
+ // src/routes/api/checkout/+server.ts
11
+ import { Checkout } from "@dodopayments/sveltekit";
12
+ import { DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_RETURN_URL, DODO_PAYMENTS_ENVIRONMENT } from "$env/static/private";
13
+
14
+ const dodoEnvironment = DODO_PAYMENTS_ENVIRONMENT === "live_mode" ? "live_mode" : "test_mode";
15
+
16
+ // Checkout() returns { GET, POST }, not a handler. Export GET from a "static"
17
+ // handler - the GET of a "session"/"dynamic" handler returns 400.
18
+ const staticCheckout = Checkout({
19
+ bearerToken: DODO_PAYMENTS_API_KEY,
20
+ returnUrl: DODO_PAYMENTS_RETURN_URL,
21
+ environment: dodoEnvironment,
22
+ type: "static",
23
+ });
24
+
25
+ const sessionCheckout = Checkout({
26
+ bearerToken: DODO_PAYMENTS_API_KEY,
27
+ returnUrl: DODO_PAYMENTS_RETURN_URL,
28
+ environment: dodoEnvironment,
29
+ type: "session",
30
+ });
31
+
32
+ export const GET = staticCheckout.GET;
33
+ export const POST = sessionCheckout.POST;
34
+ ```
35
+
36
+ ### Webhooks
37
+
38
+ ```typescript
39
+ // src/routes/api/webhook/+server.ts
40
+ import { Webhooks } from "@dodopayments/sveltekit";
41
+ import { DODO_PAYMENTS_WEBHOOK_KEY } from "$env/static/private";
42
+
43
+ export const POST = Webhooks({
44
+ webhookKey: DODO_PAYMENTS_WEBHOOK_KEY,
45
+ onPayload: async (payload) => {
46
+ console.log("Webhook:", payload.type);
47
+ },
48
+ });
49
+ ```
@@ -0,0 +1,31 @@
1
+ # Framework Adapters: TanStack Start
2
+
3
+
4
+ **Package:** `@dodopayments/tanstack`
5
+
6
+ ### Checkout
7
+
8
+ `Checkout(config)` returns a plain `(request: Request) => Promise<Response>`, so export it directly as the route's method handler. The adapter's own documented usage is `export const GET = Checkout(config)`.
9
+
10
+ ```typescript
11
+ // src/routes/api/checkout.ts
12
+ import { Checkout } from "@dodopayments/tanstack";
13
+ // src/lib/dodo-env.ts
14
+ import { dodoEnvironment } from "../../lib/dodo-env";
15
+
16
+ export const GET = Checkout({
17
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
18
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
19
+ environment: dodoEnvironment,
20
+ type: "static",
21
+ });
22
+
23
+ export const POST = Checkout({
24
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
25
+ returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
26
+ environment: dodoEnvironment,
27
+ type: "session",
28
+ });
29
+ ```
30
+
31
+ TanStack Start's server-route definition API has changed across releases (`createServerFileRoute` was removed). Wrap these exports in whatever route helper your installed version provides; the adapter handlers themselves are unaffected.