@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
package/README.md
CHANGED
|
@@ -175,6 +175,7 @@ If you prefer the local stdio API server with your own API key instead of the de
|
|
|
175
175
|
| `dodo-best-practices` | SDK setup, environments, API keys, and the canonical checkout-to-webhook architecture |
|
|
176
176
|
| `framework-adapters` | Official `@dodopayments/*` handlers for Next.js, Express, Hono, Astro, Remix, SvelteKit, Nuxt, Fastify, TanStack, Bun, Convex |
|
|
177
177
|
| `testing-and-go-live` | Test mode, test payment methods, webhook testing, production launch checklist |
|
|
178
|
+
| `dodo-mcp-usage` | When to use the documentation vs API MCP servers, test-mode safety rules, auth, troubleshooting |
|
|
178
179
|
|
|
179
180
|
**Accepting payments**
|
|
180
181
|
|
|
@@ -37,7 +37,7 @@ opencode run "List every skill available to you by name."
|
|
|
37
37
|
|
|
38
38
|
## What you get
|
|
39
39
|
|
|
40
|
-
Agent skills (auto-loaded when relevant): `dodo-best-practices`, `framework-adapters`, `testing-and-go-live`, `checkout-integration`, `subscription-integration`, `mobile-checkout`, `webhook-integration`, `credit-based-billing`, `usage-based-billing`, `license-keys`, `product-catalog-management`, `discounts-and-promotions`, `localized-pricing`, `customer-management`, `refunds-and-disputes`, `billing-sdk`, `better-auth-integration`.
|
|
40
|
+
Agent skills (auto-loaded when relevant): `dodo-best-practices`, `dodo-mcp-usage`, `framework-adapters`, `testing-and-go-live`, `checkout-integration`, `subscription-integration`, `mobile-checkout`, `webhook-integration`, `credit-based-billing`, `usage-based-billing`, `license-keys`, `product-catalog-management`, `discounts-and-promotions`, `localized-pricing`, `customer-management`, `refunds-and-disputes`, `billing-sdk`, `better-auth-integration`.
|
|
41
41
|
|
|
42
42
|
Two MCP servers (registered automatically):
|
|
43
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: better-auth-integration
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments plugin for Better Auth (@dodopayments/better-auth) covering customer sync on signup, authenticated checkout, customer portal, usage ingestion, and verified webhooks. Use when an app uses Better Auth and needs the dodopayments, dodopaymentsClient, checkout, portal, usage, or webhooks plugins; for other auth stacks use framework-adapters.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Better Auth Integration
|
|
@@ -18,9 +18,11 @@ Use `@dodopayments/better-auth` to synchronize Better Auth users with Dodo Payme
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install @dodopayments/better-auth dodopayments better-auth
|
|
21
|
+
npm install @dodopayments/better-auth dodopayments better-auth zod
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Requires Better Auth 1.4 or a later 1.x release.
|
|
25
|
+
|
|
24
26
|
The package exports the lowercase server functions `dodopayments`, `checkout`, `portal`, `usage`, and `webhooks`. Its client entry point exports `dodopaymentsClient`.
|
|
25
27
|
|
|
26
28
|
## Server setup
|
|
@@ -76,6 +78,13 @@ export const auth = betterAuth({
|
|
|
76
78
|
|
|
77
79
|
`products` maps an application slug to a Dodo product ID. `authenticatedUsersOnly: true` requires a Better Auth session for checkout. `getCustomerParams` adds application metadata when customer synchronization runs.
|
|
78
80
|
|
|
81
|
+
The plugin adds a `dodoCustomerId` field to the Better Auth `user` table. After adding the plugin, update your database schema with the [Better Auth CLI](https://www.better-auth.com/docs/concepts/cli):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx auth generate # generate the ORM schema or SQL migration, then apply it yourself
|
|
85
|
+
npx auth migrate # or apply it directly (built-in Kysely adapter only)
|
|
86
|
+
```
|
|
87
|
+
|
|
79
88
|
## Browser client setup
|
|
80
89
|
|
|
81
90
|
```typescript
|
|
@@ -170,7 +179,7 @@ if (error) {
|
|
|
170
179
|
}
|
|
171
180
|
```
|
|
172
181
|
|
|
173
|
-
The plugin derives `customer_id` from the authenticated session. `event_id` is the event's idempotency key; reuse it when retrying the same event.
|
|
182
|
+
Both `usage.ingest` and `usage.meters.list` require a signed-in user with a verified email address. The plugin derives `customer_id` from the authenticated session. `event_id` is the event's idempotency key; reuse it when retrying the same event.
|
|
174
183
|
|
|
175
184
|
## Webhook endpoint
|
|
176
185
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: billing-sdk
|
|
3
|
-
description:
|
|
3
|
+
description: BillingSDK, the open-source React and shadcn component library for Dodo Payments billing UI. Use when building pricing tables, pricing pages, account billing pages, usage meters, credit balances, invoice history, or cancel and upgrade UI in React or Next.js, or running @billingsdk/cli init; keep Dodo API calls server-side.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# BillingSDK
|
|
@@ -75,8 +75,10 @@ From the current official registry, grouped by purpose:
|
|
|
75
75
|
| Payments | `payment-details`, `payment-details-two`, `payment-method-selector`, `payment-card`, `payment-failure` |
|
|
76
76
|
| Promotion and trials | `banner`, `limited-offer-dialog`, `trial-expiry-card` |
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
Most blocks have a published usage example and props table on their page at
|
|
79
|
+
[billingsdk.com/docs/components](https://billingsdk.com/docs/components) (for example `subscription-management`
|
|
80
|
+
and `billing-settings`); `pricing-table-one` is reproduced below. The generated file's props type is still
|
|
81
|
+
authoritative for the version you installed, so read it after running `add`.
|
|
80
82
|
|
|
81
83
|
## Server client
|
|
82
84
|
|
|
@@ -103,44 +105,75 @@ are `https://test.dodopayments.com` and `https://live.dodopayments.com`.
|
|
|
103
105
|
|
|
104
106
|
### 1. Plan catalogue
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
`pricing-table-one` takes BillingSDK's `Plan[]`, not an arbitrary shape. Adding the component generates
|
|
109
|
+
`lib/billingsdk-config.ts`, which exports the `Plan` interface and a sample `plans` array — edit that array
|
|
110
|
+
rather than defining your own type. Prices are display **strings** (`monthlyPrice`, `yearlyPrice`) and
|
|
111
|
+
`features` are objects. The amount actually charged comes from the Dodo product catalogue (always in the
|
|
112
|
+
smallest currency unit), so treat this file as a mirror of the dashboard and reconcile it when you change a
|
|
113
|
+
price.
|
|
110
114
|
|
|
111
115
|
```typescript
|
|
112
|
-
// lib/
|
|
113
|
-
export type PlanId = 'starter' | 'pro';
|
|
114
|
-
|
|
116
|
+
// lib/billingsdk-config.ts (generated by `add pricing-table-one`; edit the plans)
|
|
115
117
|
export interface Plan {
|
|
116
|
-
id:
|
|
118
|
+
id: string;
|
|
117
119
|
title: string;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
description: string;
|
|
121
|
+
highlight?: boolean;
|
|
122
|
+
type?: 'monthly' | 'yearly';
|
|
123
|
+
currency?: string;
|
|
124
|
+
monthlyPrice: string;
|
|
125
|
+
yearlyPrice: string;
|
|
126
|
+
buttonText: string;
|
|
127
|
+
badge?: string;
|
|
128
|
+
features: {
|
|
129
|
+
name: string;
|
|
130
|
+
icon: string;
|
|
131
|
+
iconColor?: string;
|
|
132
|
+
}[];
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
export const
|
|
135
|
+
export const plans: Plan[] = [
|
|
125
136
|
{
|
|
126
137
|
id: 'starter',
|
|
127
138
|
title: 'Starter',
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
description: 'For individuals getting started.',
|
|
140
|
+
currency: '$',
|
|
141
|
+
monthlyPrice: '9',
|
|
142
|
+
yearlyPrice: '90',
|
|
143
|
+
buttonText: 'Choose Starter',
|
|
144
|
+
features: [
|
|
145
|
+
{ name: '100 requests', icon: 'check' },
|
|
146
|
+
{ name: 'Basic support', icon: 'check' },
|
|
147
|
+
{ name: '1 project', icon: 'check' },
|
|
148
|
+
],
|
|
132
149
|
},
|
|
133
150
|
{
|
|
134
151
|
id: 'pro',
|
|
135
152
|
title: 'Pro',
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
description: 'For teams running in production.',
|
|
154
|
+
currency: '$',
|
|
155
|
+
monthlyPrice: '29',
|
|
156
|
+
yearlyPrice: '290',
|
|
157
|
+
buttonText: 'Choose Pro',
|
|
158
|
+
badge: 'Most popular',
|
|
159
|
+
highlight: true,
|
|
160
|
+
features: [
|
|
161
|
+
{ name: 'Unlimited requests', icon: 'check' },
|
|
162
|
+
{ name: 'Priority support', icon: 'check' },
|
|
163
|
+
{ name: '10 projects', icon: 'check' },
|
|
164
|
+
],
|
|
140
165
|
},
|
|
141
166
|
];
|
|
142
167
|
```
|
|
143
168
|
|
|
169
|
+
The generated file is the contract for your installed version; if its `Plan` differs from the above, follow
|
|
170
|
+
the file. `onPlanSelect` receives only the plan `id`, so the ids here are the slugs the server maps below.
|
|
171
|
+
|
|
172
|
+
This example sells **monthly plans only**: `yearlyPrice` is required by the `Plan` type and is displayed,
|
|
173
|
+
but the server map below has only monthly products. To sell yearly too, create the yearly products in Dodo,
|
|
174
|
+
track the table's billing-period toggle in your own state (it is not passed to `onPlanSelect`), send it with
|
|
175
|
+
the plan id, and key the server map by plan and period (for example `pro:yearly`).
|
|
176
|
+
|
|
144
177
|
### 2. Product id map — server only
|
|
145
178
|
|
|
146
179
|
Never let the browser choose an arbitrary `product_id`. Map the public plan slug to a `pdt_` id on the
|
|
@@ -149,16 +182,16 @@ server and reject anything else.
|
|
|
149
182
|
```typescript
|
|
150
183
|
// lib/plan-products.server.ts
|
|
151
184
|
import 'server-only';
|
|
152
|
-
import type { PlanId } from './plans';
|
|
153
185
|
|
|
154
|
-
|
|
186
|
+
// Keys must match the `id`s in lib/billingsdk-config.ts.
|
|
187
|
+
const PRODUCT_IDS: Readonly<Record<string, string>> = {
|
|
155
188
|
starter: 'pdt_starter_monthly',
|
|
156
189
|
pro: 'pdt_pro_monthly',
|
|
157
190
|
};
|
|
158
191
|
|
|
159
192
|
export function productIdForPlan(value: unknown): string | null {
|
|
160
|
-
if (typeof value !== 'string') return null;
|
|
161
|
-
return PRODUCT_IDS[value
|
|
193
|
+
if (typeof value !== 'string' || !Object.hasOwn(PRODUCT_IDS, value)) return null;
|
|
194
|
+
return PRODUCT_IDS[value];
|
|
162
195
|
}
|
|
163
196
|
```
|
|
164
197
|
|
|
@@ -220,7 +253,7 @@ integrations. Checkout parameters are covered in depth in the `checkout-integrat
|
|
|
220
253
|
|
|
221
254
|
import { useState } from 'react';
|
|
222
255
|
import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
|
|
223
|
-
import {
|
|
256
|
+
import { plans } from '@/lib/billingsdk-config';
|
|
224
257
|
|
|
225
258
|
export function PricingPlans() {
|
|
226
259
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -249,7 +282,7 @@ export function PricingPlans() {
|
|
|
249
282
|
return (
|
|
250
283
|
<>
|
|
251
284
|
<PricingTableOne
|
|
252
|
-
plans={
|
|
285
|
+
plans={plans}
|
|
253
286
|
title="Choose your plan"
|
|
254
287
|
description="Select the plan that works best for you"
|
|
255
288
|
onPlanSelect={handlePlanSelect}
|
|
@@ -458,14 +491,14 @@ signature verification is covered in the `webhook-integration` skill.
|
|
|
458
491
|
**Accepting a raw `product_id` from the request body.** A caller can then check out against any product in
|
|
459
492
|
your catalogue, including internal or discounted ones. Map an opaque plan slug to a `pdt_` id server-side.
|
|
460
493
|
|
|
461
|
-
**Hardcoding prices in the client.** `
|
|
494
|
+
**Hardcoding prices in the client.** `plans[].monthlyPrice` / `yearlyPrice` are display text. When you change a price in the Dodo
|
|
462
495
|
dashboard, the checkout charges the new amount while the pricing page keeps advertising the old one.
|
|
463
|
-
Reconcile `lib/
|
|
496
|
+
Reconcile `lib/billingsdk-config.ts` with the dashboard as part of any pricing change.
|
|
464
497
|
|
|
465
498
|
**`npm install billingsdk`.** The root repository package is private. Install through
|
|
466
499
|
`@billingsdk/cli` or the shadcn registry instead.
|
|
467
500
|
|
|
468
|
-
**Guessing prop names.**
|
|
501
|
+
**Guessing prop names.** Check the block's page on billingsdk.com/docs/components, then open the
|
|
469
502
|
generated file under `components/billingsdk/` and read the exported props type before wiring anything else.
|
|
470
503
|
|
|
471
504
|
**Using `DODO_PAYMENTS_WEBHOOK_SECRET`.** The SDK reads `DODO_PAYMENTS_WEBHOOK_KEY` for the `webhookKey`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: checkout-integration
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments checkout via hosted Checkout Sessions (client.checkoutSessions.create), static payment links, and overlay or inline checkout for one-time and subscription products. Use when building a pay button, checkout page, trial signup, custom fields, discount codes at checkout, or return_url redirects; use subscription-integration for post-checkout lifecycle.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dodo Payments Checkout Integration
|
|
@@ -38,7 +38,7 @@ Dodo Payments offers three ways to collect payment:
|
|
|
38
38
|
|
|
39
39
|
**Checkout Session:** A single-use session that generates a hosted checkout URL. Expires after 24 hours (or 15 minutes if `confirm=true`).
|
|
40
40
|
|
|
41
|
-
**Return URL:** Where the customer lands after payment.
|
|
41
|
+
**Return URL:** Where the customer lands after payment. Dodo appends `payment_id` (one-time) or `subscription_id` (subscription), `status`, and when available `license_key` and `email`. It is a UI hint only, never proof of payment.
|
|
42
42
|
|
|
43
43
|
**Entitlement on webhook:** The browser redirect is not the source of truth. Always verify payment via webhook before granting access. See `webhook-integration` skill for verification.
|
|
44
44
|
|
|
@@ -637,31 +637,27 @@ async function createCheckoutUrl(planId: PlanId, quantity = 1): Promise<string>
|
|
|
637
637
|
return data.checkoutUrl;
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
640
|
+
// Call ONCE when the app loads (e.g. in your root component or entry point),
|
|
641
|
+
// not before every checkout. Pick the display type your page uses.
|
|
642
|
+
export function initCheckout(displayType: 'overlay' | 'inline'): void {
|
|
643
643
|
DodoPayments.Initialize({
|
|
644
644
|
mode: 'test', // or 'live'
|
|
645
|
-
displayType
|
|
645
|
+
displayType,
|
|
646
646
|
onEvent: (event) => {
|
|
647
647
|
console.log('Checkout event:', event);
|
|
648
648
|
}
|
|
649
649
|
});
|
|
650
|
+
}
|
|
650
651
|
|
|
652
|
+
// Requires initCheckout('overlay') to have run at app load.
|
|
653
|
+
export async function openOverlayCheckout(planId: PlanId): Promise<void> {
|
|
654
|
+
const checkoutUrl = await createCheckoutUrl(planId);
|
|
651
655
|
DodoPayments.Checkout.open({ checkoutUrl });
|
|
652
656
|
}
|
|
653
657
|
|
|
658
|
+
// Requires initCheckout('inline') to have run at app load.
|
|
654
659
|
export async function openInlineCheckout(planId: PlanId): Promise<void> {
|
|
655
660
|
const checkoutUrl = await createCheckoutUrl(planId);
|
|
656
|
-
|
|
657
|
-
DodoPayments.Initialize({
|
|
658
|
-
mode: 'test', // or 'live'
|
|
659
|
-
displayType: 'inline',
|
|
660
|
-
onEvent: (event) => {
|
|
661
|
-
console.log('Checkout event:', event);
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
|
|
665
661
|
DodoPayments.Checkout.open({
|
|
666
662
|
checkoutUrl,
|
|
667
663
|
elementId: 'dodo-inline-checkout'
|
|
@@ -685,6 +681,8 @@ No-code shareable links. No server-side API call needed.
|
|
|
685
681
|
https://checkout.dodopayments.com/buy/{productid}
|
|
686
682
|
```
|
|
687
683
|
|
|
684
|
+
In test mode the host is `test.checkout.dodopayments.com` (for example `https://test.checkout.dodopayments.com/buy/{productid}`); a test-mode product does not exist on the live host.
|
|
685
|
+
|
|
688
686
|
Example:
|
|
689
687
|
```text
|
|
690
688
|
https://checkout.dodopayments.com/buy/pdt_example
|
|
@@ -789,12 +787,20 @@ const session = await client.checkoutSessions.create({
|
|
|
789
787
|
After payment, the customer is redirected to your `return_url` with query parameters:
|
|
790
788
|
|
|
791
789
|
```text
|
|
792
|
-
|
|
790
|
+
# One-time payment
|
|
791
|
+
https://yoursite.com/success?payment_id=pay_xxx&status=succeeded&email=customer%40example.com
|
|
792
|
+
|
|
793
|
+
# Subscription (with license keys)
|
|
794
|
+
https://yoursite.com/success?subscription_id=sub_xxx&status=active&license_key=LK-001,LK-002&email=customer%40example.com
|
|
793
795
|
```
|
|
794
796
|
|
|
795
797
|
Query parameters:
|
|
796
|
-
- `
|
|
797
|
-
- `
|
|
798
|
+
- `payment_id` (one-time payments) or `subscription_id` (subscriptions)
|
|
799
|
+
- `status`: `succeeded` (one-time) or `active` (subscription) on success; `failed` if declined; `processing` (or a `requires_*` value) if it settles later; `expired` if the session expired
|
|
800
|
+
- `license_key`: present when the product issues license keys (comma-separated if several)
|
|
801
|
+
- `email`: present when the customer has an email on record
|
|
802
|
+
|
|
803
|
+
There is no `session_id` parameter. Treat `processing`/missing status as "unknown" and wait for the webhook.
|
|
798
804
|
|
|
799
805
|
### Verify Payment Server-Side
|
|
800
806
|
|
|
@@ -818,8 +824,8 @@ Do not use `client.payments.create()` or `client.subscriptions.create()` for new
|
|
|
818
824
|
**3. Forgetting the `environment` flag**
|
|
819
825
|
The default is `live_mode`. Always set `environment: 'test_mode'` during development to avoid charging real cards.
|
|
820
826
|
|
|
821
|
-
**4.
|
|
822
|
-
|
|
827
|
+
**4. Using the deprecated `discount_code`**
|
|
828
|
+
Use `discount_codes` (an array). The singular `discount_code` string is deprecated (still accepted for backward compatibility) and cannot be combined with `discount_codes` in the same request.
|
|
823
829
|
|
|
824
830
|
**5. Amounts in wrong unit**
|
|
825
831
|
All amounts are in the smallest currency unit (cents for USD). $10 is `1000`, not `10`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: credit-based-billing
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments credit-based billing with credit entitlements, grants, balances, ledger deductions, rollover, expiry, low-balance alerts, and overage. Use when selling prepaid or top-up credit packs, including credits in a plan, deducting AI token or API credits via meters, or granting promo credits; use usage-based-billing for direct per-unit charges.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dodo Payments Credit-Based Billing
|
|
@@ -203,7 +203,7 @@ Attach the credit entitlement to the same usage-based product as the meter. The
|
|
|
203
203
|
| Field | Purpose |
|
|
204
204
|
|---|---|
|
|
205
205
|
| `meter_id` | Meter to aggregate usage. |
|
|
206
|
-
| `free_threshold` |
|
|
206
|
+
| `free_threshold` | Free usage excluded before rating - applies **only** when the meter bills in money. It is ignored for credit-billed meters: every unit is deducted from the credit balance. |
|
|
207
207
|
| `credit_entitlement_id` | Credit pool consumed by this meter. |
|
|
208
208
|
| `meter_units_per_credit` | Usage units required to deduct one credit; required when `credit_entitlement_id` is set. |
|
|
209
209
|
| `price_per_unit` | Direct per-unit monetary price when using pure usage pricing; decimal string. |
|
|
@@ -297,7 +297,7 @@ If the same `idempotency_key` already exists, the API can return `409`. Treat th
|
|
|
297
297
|
|
|
298
298
|
## Deduct credits manually
|
|
299
299
|
|
|
300
|
-
Use the same method with `entry_type: 'debit'`. Debits consume the
|
|
300
|
+
Use the same method with `entry_type: 'debit'`. Debits consume the earliest-expiring grants first and can return `400` when the balance is insufficient.
|
|
301
301
|
|
|
302
302
|
```typescript
|
|
303
303
|
async function deductJobCredits(
|
|
@@ -356,10 +356,9 @@ Persist the Dodo ledger entry ID with internal order, support-case, or job IDs.
|
|
|
356
356
|
2. Add the meter to the same product.
|
|
357
357
|
3. Enable **Bill usage in Credits** for that meter.
|
|
358
358
|
4. Set `credit_entitlement_id` and `meter_units_per_credit`.
|
|
359
|
-
5.
|
|
360
|
-
6. Ingest events whose case-sensitive `event_name` matches the meter.
|
|
359
|
+
5. Ingest events whose case-sensitive `event_name` matches the meter.
|
|
361
360
|
|
|
362
|
-
A background worker processes new usage approximately every minute, aggregates it according to the meter, converts meter units using `meter_units_per_credit`, and
|
|
361
|
+
A background worker processes new usage approximately every minute, aggregates it according to the meter, converts meter units using `meter_units_per_credit`, and deducts from non-expired grants with the earliest-expiring grants consumed first. There is no free threshold on a credit-billed meter. Multiple meters can consume one shared credit pool at different conversion rates.
|
|
363
362
|
|
|
364
363
|
This delay is material: an accepted usage event does not imply that a balance read immediately afterward includes its deduction.
|
|
365
364
|
|
|
@@ -467,7 +466,7 @@ The lifecycle is:
|
|
|
467
466
|
1. The model returns actual prompt and completion token counts.
|
|
468
467
|
2. The app ingests one uniquely identified `ai.tokens` event.
|
|
469
468
|
3. The meter aggregates the `tokens` metadata value.
|
|
470
|
-
4. The worker converts units into credits and deducts
|
|
469
|
+
4. The worker converts units into credits and deducts from non-expired grants, earliest-expiring first.
|
|
471
470
|
5. Dodo emits `credit.deducted`; when configured threshold conditions are met, it emits `credit.balance_low`.
|
|
472
471
|
6. The app notifies the customer and offers checkout for the attached top-up product.
|
|
473
472
|
|
|
@@ -588,7 +587,7 @@ Return an application-level payment/credit-required response with a top-up or up
|
|
|
588
587
|
16. **Sending duplicate IDs within one batch.** Duplicate `event_id` values in the same ingestion request reject the entire request. Previously ingested IDs are ignored on retry.
|
|
589
588
|
17. **Inventing a deduplication window.** Current docs require unique event IDs but do not publish a retention duration.
|
|
590
589
|
18. **Getting conversion backward.** `meter_units_per_credit` is usage units required for one credit, and is required when `credit_entitlement_id` is set.
|
|
591
|
-
19. **
|
|
590
|
+
19. **Expecting a free threshold on credit-billed meters.** `free_threshold` applies only to meters billed in money. When **Bill usage in Credits** is on, every unit deducts credits; model included usage as granted credits instead.
|
|
592
591
|
20. **Supplying one rollover timeframe field.** `rollover_timeframe_count` and `rollover_timeframe_interval` must be supplied together.
|
|
593
592
|
21. **Using the wrong rollover enum case.** Confirmed values are `Day`, `Week`, `Month`, and `Year`.
|
|
594
593
|
22. **Assuming expiry happens before rollover.** Rollover is applied first; only the remainder expires.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: customer-management
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments customer management, covering customer CRUD, saved payment methods, real-money customer wallets and ledger entries, entitlements, and hosted customer portal sessions. Use when linking app users to cus_ IDs, opening a Manage billing portal, listing or deleting payment methods, or crediting wallets; use credit-based-billing for usage credits.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Customer Management
|
|
@@ -68,19 +68,28 @@ await client.customers.update('cus_abc123', {
|
|
|
68
68
|
|
|
69
69
|
## Customer Portal
|
|
70
70
|
|
|
71
|
-
Create a time-bound session that redirects customers to a hosted portal.
|
|
71
|
+
Create a time-bound session that redirects customers to a hosted portal. In the portal, customers can view invoices, cancel subscriptions, change plans, update payment methods, recover on-hold subscriptions, and download license keys.
|
|
72
|
+
|
|
73
|
+
Never call the SDK from the browser: it uses your secret API key. Create the session in a server route and return only the link.
|
|
72
74
|
|
|
73
75
|
```typescript
|
|
74
|
-
//
|
|
76
|
+
// Server route: create a portal session
|
|
75
77
|
const session = await client.customers.customerPortal.create('cus_abc123', {
|
|
76
78
|
return_url: 'https://yourapp.com/dashboard',
|
|
79
|
+
// send_email: true, // optionally also email the link to the customer
|
|
77
80
|
});
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
window.location.href = session.link;
|
|
82
|
+
return Response.json({ link: session.link });
|
|
81
83
|
```
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
```typescript
|
|
86
|
+
// Browser: redirect the customer
|
|
87
|
+
const res = await fetch('/api/portal', { method: 'POST' });
|
|
88
|
+
const { link } = await res.json();
|
|
89
|
+
window.location.href = link;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The portal session link is valid for 24 hours. Pass `send_email: true` to have Dodo also email the link to the customer. Customers cannot:
|
|
84
93
|
- Create new subscriptions (use checkout for that)
|
|
85
94
|
- Refund themselves
|
|
86
95
|
- Change their email or name (contact support)
|
|
@@ -225,23 +234,22 @@ const found = customers.items.find(c => c.email === user.email);
|
|
|
225
234
|
|
|
226
235
|
## Common mistakes
|
|
227
236
|
|
|
228
|
-
**Creating duplicate customers per checkout.**
|
|
237
|
+
**Creating duplicate customers per checkout.** Passing `customer: { email }` does not create a new customer each time: by default, checkout uses the email to find an existing customer and attaches the session to it. Duplicates come from setting `feature_flags.always_create_new_customer: true`, or from calling `customers.create` before every checkout. Create the customer once (or let the first checkout create it), store its `customer_id`, and pass that on later checkouts.
|
|
229
238
|
|
|
230
239
|
```typescript
|
|
231
|
-
// WRONG
|
|
240
|
+
// WRONG: forces a new customer record on every checkout
|
|
232
241
|
const session = await client.checkoutSessions.create({
|
|
233
|
-
product_cart: [
|
|
234
|
-
customer: { email: user.email },
|
|
242
|
+
product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
|
|
243
|
+
customer: { email: user.email },
|
|
244
|
+
feature_flags: { always_create_new_customer: true },
|
|
235
245
|
});
|
|
236
246
|
|
|
237
|
-
// RIGHT
|
|
238
|
-
const customer = await client.customers.create({
|
|
239
|
-
email: user.email,
|
|
240
|
-
name: user.name,
|
|
241
|
-
});
|
|
247
|
+
// RIGHT: reuse the stored customer ID, or fall back to email (matched to an existing customer by default)
|
|
242
248
|
const session = await client.checkoutSessions.create({
|
|
243
|
-
product_cart: [
|
|
244
|
-
customer:
|
|
249
|
+
product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
|
|
250
|
+
customer: user.dodo_customer_id
|
|
251
|
+
? { customer_id: user.dodo_customer_id }
|
|
252
|
+
: { email: user.email },
|
|
245
253
|
});
|
|
246
254
|
```
|
|
247
255
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: discounts-and-promotions
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments discount codes and promotional pricing, covering discount CRUD, percentage and flat amounts, eligibility rules, usage limits, stacking via discount_codes, subscription-cycle limits, and preservation on plan changes. Use when creating coupons or promo codes, applying or validating a code at checkout, or debugging a discount that won't apply.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Discounts and Promotions
|
|
@@ -20,7 +20,7 @@ This skill covers discount codes, coupons, and promotional pricing in Dodo Payme
|
|
|
20
20
|
|
|
21
21
|
**Discount types:** Dodo supports both percentage and flat-amount discounts. Percentage amounts are expressed as integers where 1500 means 15% (100 = 1%). Flat amounts are in the smallest currency unit (cents for USD).
|
|
22
22
|
|
|
23
|
-
**Discount code:** A human-readable string (e.g., `SUMMER2025`) that customers enter at checkout.
|
|
23
|
+
**Discount code:** A human-readable string (e.g., `SUMMER2025`) that customers enter at checkout. The API uppercases codes on create (`summer2025` is stored as `SUMMER2025`); codes must be at least 3 characters, and omitting `code` generates a random 16-character uppercase code.
|
|
24
24
|
|
|
25
25
|
**Eligibility:** Discounts can be restricted to specific products, customers, or date ranges. A discount without restrictions applies to any product and any customer.
|
|
26
26
|
|
|
@@ -119,7 +119,7 @@ await client.discounts.delete('discount_id');
|
|
|
119
119
|
|
|
120
120
|
## Discount types and configuration
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
Two types are supported: `percentage` and `flat` (shown as **Amount** in the dashboard). A `flat` discount requires `currency_options` with at least one resolvable default currency; for a `percentage` discount, `currency_options` is optional and caps the discount per currency. A flat deduction is pooled across the whole cart rather than applied per line item.
|
|
123
123
|
|
|
124
124
|
### Percentage discounts
|
|
125
125
|
|
|
@@ -177,19 +177,12 @@ const discount = await client.discounts.create({
|
|
|
177
177
|
customer_eligibility: 'specific',
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
'Content-Type': 'application/json',
|
|
187
|
-
},
|
|
188
|
-
body: JSON.stringify({
|
|
189
|
-
customer_ids: ['cus_vip_001', 'cus_vip_002'],
|
|
190
|
-
}),
|
|
191
|
-
},
|
|
192
|
-
);
|
|
180
|
+
// The SDK has no named method for this endpoint yet. `client.post` reuses the
|
|
181
|
+
// client's auth and base URL (test.dodopayments.com for `test_mode`,
|
|
182
|
+
// live.dodopayments.com for `live_mode`), so nothing is hardcoded.
|
|
183
|
+
await client.post(`/discounts/${discount.discount_id}/customers`, {
|
|
184
|
+
body: { customer_ids: ['cus_vip_001', 'cus_vip_002'] },
|
|
185
|
+
});
|
|
193
186
|
```
|
|
194
187
|
|
|
195
188
|
### Date range
|
|
@@ -236,7 +229,10 @@ const discount = await client.discounts.create({
|
|
|
236
229
|
|
|
237
230
|
### Single code
|
|
238
231
|
|
|
232
|
+
Never call the SDK from the browser: it uses your secret API key. Create the session in a server route and return only the URL.
|
|
233
|
+
|
|
239
234
|
```typescript
|
|
235
|
+
// Server route
|
|
240
236
|
const session = await client.checkoutSessions.create({
|
|
241
237
|
product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
|
|
242
238
|
discount_codes: ['SUMMER2025'],
|
|
@@ -244,7 +240,14 @@ const session = await client.checkoutSessions.create({
|
|
|
244
240
|
return_url: 'https://yoursite.com/return'
|
|
245
241
|
});
|
|
246
242
|
|
|
247
|
-
|
|
243
|
+
return Response.json({ checkout_url: session.checkout_url });
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// Browser
|
|
248
|
+
const res = await fetch('/api/checkout', { method: 'POST' });
|
|
249
|
+
const { checkout_url } = await res.json();
|
|
250
|
+
window.location.href = checkout_url;
|
|
248
251
|
```
|
|
249
252
|
|
|
250
253
|
### Multiple codes (stacking)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dodo-best-practices
|
|
3
|
-
description:
|
|
3
|
+
description: Starting point for any Dodo Payments integration, covering Merchant of Record basics, SDK installation and client setup, test and live environments, API keys, and the canonical checkout-to-webhook flow. Use when starting a new integration, initializing the dodopayments client, choosing an architecture, or unsure which Dodo Payments skill applies.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dodo Payments Integration Guide
|
|
@@ -125,8 +125,13 @@ composer require "dodopayments/client:6.19.0"
|
|
|
125
125
|
```php
|
|
126
126
|
use Dodopayments\Client;
|
|
127
127
|
|
|
128
|
+
$apiKey = getenv('DODO_PAYMENTS_API_KEY');
|
|
129
|
+
if ($apiKey === false || $apiKey === '') {
|
|
130
|
+
throw new RuntimeException('DODO_PAYMENTS_API_KEY is not set');
|
|
131
|
+
}
|
|
132
|
+
|
|
128
133
|
$client = new Client(
|
|
129
|
-
bearerToken:
|
|
134
|
+
bearerToken: $apiKey,
|
|
130
135
|
environment: 'test_mode',
|
|
131
136
|
);
|
|
132
137
|
```
|
|
@@ -155,8 +160,12 @@ dodo_payments = Dodopayments::Client.new(
|
|
|
155
160
|
```
|
|
156
161
|
|
|
157
162
|
```java
|
|
158
|
-
|
|
159
|
-
//
|
|
163
|
+
// fromEnv() reads DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_WEBHOOK_KEY, DODO_PAYMENTS_BASE_URL.
|
|
164
|
+
// Without testMode() (or a test DODO_PAYMENTS_BASE_URL) the client targets live mode.
|
|
165
|
+
DodoPaymentsClient client = DodoPaymentsOkHttpClient.builder()
|
|
166
|
+
.fromEnv()
|
|
167
|
+
.testMode()
|
|
168
|
+
.build();
|
|
160
169
|
```
|
|
161
170
|
|
|
162
171
|
### Kotlin
|
|
@@ -170,7 +179,11 @@ DodoPaymentsClient client = DodoPaymentsOkHttpClient.fromEnv();
|
|
|
170
179
|
```
|
|
171
180
|
|
|
172
181
|
```kotlin
|
|
173
|
-
|
|
182
|
+
// Without testMode() (or a test DODO_PAYMENTS_BASE_URL) the client targets live mode.
|
|
183
|
+
val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.builder()
|
|
184
|
+
.fromEnv()
|
|
185
|
+
.testMode()
|
|
186
|
+
.build()
|
|
174
187
|
```
|
|
175
188
|
|
|
176
189
|
---
|