dodo-best-practices — Claude Code skill: what it does, how to install
Guides Dodo Payments integration, from multi-language SDK setup to test environment configuration and webhook flows.
npx skills add dodopayments/skills --skill dodo-best-practices -a claude-codeWhat dodo-best-practices does
This skill supplies your coding assistant with foundational instructions for using Dodo Payments as a Merchant of Record. Dodo acts as the legal seller, managing jurisdiction taxes and dispute resolution so your app only needs to create checkout sessions and handle webhooks.
The skill covers client setup across TypeScript, Python, Go, PHP, Ruby, Java, and Kotlin. It also references framework adapters such as @dodopayments/nextjs, Express, and Fastify. Legitimate base endpoints are strictly https://live.dodopayments.com and https://test.dodopayments.com. The address api.dodopayments.com lacks a DNS entry and will fail.
Without explicit parameters, the SDK defaults to live_mode. Passing test_mode explicitly alongside a dodo_test_... key is required to avoid sending test traffic to production.
What it helps with
- set up the Dodo Payments client in TypeScript, Python, Go, or Java
- configure test mode with
dodo_test_...keys and valid base URLs - pick the right
@dodopayments/*adapter for Next.js, Fastify, or Nuxt - wire environment variables for API tokens and webhook verification
Example prompts for the agent
- Show how to initialize the Dodo Payments client in test mode using Python.
- Which Dodo Payments adapter should I install for Next.js App Router?
- Set up checkout session handling with Dodo Payments in TypeScript.
How to install dodo-best-practices
1Skills CLI (Vercel)
npx skills add dodopayments/skills --skill dodo-best-practices -a claude-code2Manually: put the files in place
~/.claude/skills/dodo-best-practices/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/dodo-best-practices/
1Skills CLI (Vercel)
npx skills add dodopayments/skills --skill dodo-best-practices -a codex2Manually: put the files in place
.agents/skills/dodo-best-practices/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add dodopayments/skills --skill dodo-best-practices -a cursor2Manually: put the files in place
.cursor/skills/dodo-best-practices/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install dodopayments/skills dodo-best-practices2Manually: put the files in place
.github/skills/dodo-best-practices/SKILL.md1Skills CLI (Vercel)
npx skills add dodopayments/skills --skill dodo-best-practices -a gemini-cli2Platform CLI
gemini skills install https://github.com/dodopayments/skills --path dodo-payments/dodo-best-practicesManually: put the files in place
.devin/skills/dodo-best-practices/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add dodopayments/skills --skill dodo-best-practices -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about dodo-best-practices
How do I install dodo-best-practices in Claude Code?
Run npx skills add dodopayments/skills --skill dodo-best-practices -a claude-code. The file will be saved at ~/.claude/skills/dodo-best-practices/SKILL.md.
What does the dodo-best-practices skill do?
It provides setup patterns for Dodo Payments SDKs, framework adapters, environment variables, and the canonical checkout lifecycle.
What environment does the Dodo Payments SDK use by default?
The client targets live_mode when the environment option is omitted. You must pass test_mode explicitly during local development.
Source text
SKILL.md · MIT
---
name: dodo-best-practices
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.
---
# Dodo Payments Integration Guide
This skill covers the foundational concepts and setup for Dodo Payments. Use it when starting a new integration, setting up the SDK, or understanding the core payment flow.
## When to use this skill
- You're building a new payment integration and need to understand Dodo's architecture
- You need to install and initialize the SDK in your language
- You want to understand the canonical payment flow and webhook verification
- You're choosing between framework adapters or payment methods
- You need to know what Dodo handles versus what you must build
---
## What is Dodo Payments
Dodo Payments is a Merchant of Record (MoR). That means Dodo is the legal seller on every transaction, handles sales tax registration and calculation across all jurisdictions, remits taxes to authorities, and manages card-network disputes and chargebacks. As a developer, you don't build a sales-tax engine or dispute-handling system. You create checkout sessions, listen to webhooks, and grant access when payment succeeds.
---
## Environment Setup
### Base URLs
Only two real base URLs exist:
- **Live**: `https://live.dodopayments.com`
- **Test**: `https://test.dodopayments.com`
Never use `api.dodopayments.com` — it has no DNS record and cannot be reached.
### API Keys
Keys have two formats:
- **Test**: `dodo_test_...`
- **Live**: `dodo_live_...`
### Environment Variables
```bash
DODO_PAYMENTS_API_KEY # Bearer token for API requests
DODO_PAYMENTS_WEBHOOK_KEY # Secret for webhook signature verification
```
### Default Environment
The `environment` parameter defaults to `live_mode` if omitted. Always pass `test_mode` explicitly during development.
---
## SDK Installation & Initialization
### TypeScript/Node.js
```bash
npm install dodopayments
```
```typescript
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode', // defaults to 'live_mode'
});
```
Constructor options:
| Option | Type | Default | Notes |
|---|---|---|---|
| `bearerToken` | string | env `DODO_PAYMENTS_API_KEY` | Required for API calls |
| `environment` | `'test_mode' \| 'live_mode'` | `'live_mode'` | Explicit in dev |
| `webhookKey` | string | env `DODO_PAYMENTS_WEBHOOK_KEY` | For `webhooks.unwrap()` |
| `baseURL` | string | — | Override; mutually exclusive with `environment` |
### Python
```bash
pip install dodopayments
```
```python
import os
from dodopayments import DodoPayments
client = DodoPayments(
bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
environment="test_mode", # defaults to "live_mode"
)
```
### Go
```bash
go get -u github.com/dodopayments/[email protected]
```
```go
import (
"os"
"github.com/dodopayments/dodopayments-go"
"github.com/dodopayments/dodopayments-go/option"
)
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
option.WithEnvironmentTestMode(), // defaults to live
)
```
### PHP
```bash
composer require "dodopayments/client:6.19.0"
```
```php
use Dodopayments\Client;
$apiKey = getenv('DODO_PAYMENTS_API_KEY');
if ($apiKey === false || $apiKey === '') {
throw new RuntimeException('DODO_PAYMENTS_API_KEY is not set');
}
$client = new Client(
bearerToken: $apiKey,
environment: 'test_mode',
);
```
### Ruby
```bash
gem "dodopayments", "~> 2.22.0"
```
```ruby
dodo_payments = Dodopayments::Client.new(
bearer_token: ENV["DODO_PAYMENTS_API_KEY"],
environment: "test_mode"
)
```
### Java
```xml
<dependency>
<groupId>com.dodopayments.api</groupId>
<artifactId>dodo-payments-java</artifactId>
<version>1.110.0</version>
</dependency>
```
```java
// fromEnv() reads DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_WEBHOOK_KEY, DODO_PAYMENTS_BASE_URL.
// Without testMode() (or a test DODO_PAYMENTS_BASE_URL) the client targets live mode.
DodoPaymentsClient client = DodoPaymentsOkHttpClient.builder()
.fromEnv()
.testMode()
.build();
```
### Kotlin
```xml
<dependency>
<groupId>com.dodopayments.api</groupId>
<artifactId>dodo-payments-kotlin</artifactId>
<version>1.110.0</version>
</dependency>
```
```kotlin
// Without testMode() (or a test DODO_PAYMENTS_BASE_URL) the client targets live mode.
val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.builder()
.fromEnv()
.testMode()
.build()
```
---
## Framework Adapters
Dodo publishes framework-specific packages under `@dodopayments/*`. Use the one matching your stack:
| Framework | Package | Use if |
|---|---|---|
| Next.js | `@dodopayments/nextjs` | Building with Next.js App Router |
| Nuxt | `@dodopayments/nuxt` | Building with Nuxt 3+ |
| Express | `@dodopayments/express` | Using Express.js |
| Fastify | `@dodopayments/fastify` | Using Fastify |
| Hono | `@dodopayments/hono` | Using Hono (Edge/Node) |
| Astro | `@dodopayments/astro` | Using Astro endpoints |
| SvelteKit | `@dodopayments/sveltekit` | Using SvelteKit server routes |
| Remix | `@dodopayments/remix` | Using Remix loaders/actions |
| TanStack Start | `@dodopayments/tanstack` | Using TanStack Start |
| Better Auth | `@dodopayments/better-auth` | Integrating with Better Auth |
| Convex | `@dodopayments/convex` | Using Convex backend |
| Bun | `@dodopayments/bun` | Using Bun.serve() |
Each adapter provides checkout, customer portal, and webhook handlers tailored to the framework's conventions.
---
## Core Concepts
### Products
Items you sell. Create in the dashboard or via API. Types:
- **One-time**: Single purchase
- **Subscription**: Recurring billing
- **Usage-based**: Metered consumption
### Customers
Represent buyers. Can have multiple payment methods, subscriptions, and credit balances. Create explicitly or implicitly during checkout.
### Checkout Sessions
The primary payment collection method. Create a session server-side, redirect the customer to the hosted checkout URL, and listen for webhooks to confirm payment.
See the `checkout-integration` skill for detailed checkout configuration and the `subscription-integration` skill for recurring lifecycle management.
### Subscriptions
Recurring charges on a schedule. Managed through checkout sessions or the subscriptions API. See the `subscription-integration` skill for lifecycle, trials, plan changes, and on-demand charging.
### Webhooks
Real-time event notifications. Dodo sends events like `payment.succeeded`, `subscription.active`, `refund.succeeded`, and `credit.deducted`. Webhook signature verification is covered in the `webhook-integration` skill.
### Credit Entitlements
Virtual balances (API calls, tokens, compute hours) attached to products. Configured per product with rollover, overage, and expiration rules. See the `credit-based-billing` skill.
---
## The Canonical Payment Flow
1. **Create checkout session** on your server with product ID and customer email.
2. **Redirect customer** to the `checkout_url` returned.
3. **Customer pays** on the hosted checkout.
4. **Dodo sends webhook** (e.g., `payment.succeeded`) to your endpoint.
5. **Verify the webhook signature** using `client.webhooks.unwrap()`.
6. **Grant access** only after webhook verification succeeds.
Never grant access based on the browser `return_url` redirect alone. The webhook is the authoritative confirmation.
```typescript
import express from 'express';
const app = express();
// 1. Create session
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_example', quantity: 1 }],
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
// 2. Redirect to session.checkout_url
// 3. Listen for webhook with the exact raw request bytes
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
try {
// 4. Verify signature
const webhookId = req.headers['webhook-id'] as string;
const event = client.webhooks.unwrap(req.body.toString(), {
headers: {
'webhook-id': webhookId,
'webhook-signature': req.headers['webhook-signature'] as string,
'webhook-timestamp': req.headers['webhook-timestamp'] as string,
},
});
// 5. Suppress duplicates with an atomic unique insert and run side effects
// in the same database transaction.
const handled = await processWebhookOnce(webhookId, async () => {
if (event.type === 'payment.succeeded') {
// ONE-TIME purchases only. Subscription access starts on subscription.active.
const payment = event.data;
await grantOneTimeAccess(payment.customer.customer_id);
}
if (event.type === 'subscription.active') {
await grantSubscriptionAccess(event.data);
}
});
res.json({ received: true, duplicate: !handled });
} catch (error) {
res.status(401).json({ error: 'Invalid signature' });
}
});
```
---
## API Foundations
### Authentication
All requests use Bearer token authentication:
```http
Authorization: Bearer dodo_live_...
```
### Pagination
List endpoints are page-numbered. They accept `page_size` and `page_number`, and the response
exposes the rows on `items`:
```typescript
const payments = await client.payments.list({
page_size: 50,
page_number: 0,
});
for (const payment of payments.items) {
console.log(payment.payment_id);
}
```
The SDK can also walk every page for you:
```typescript
for await (const payment of client.payments.list()) {
console.log(payment.payment_id);
}
```
### Rate Limits
Dodo enforces rate limits. The SDK automatically retries `429` responses as described below.
### SDK Error Classes
The SDK throws typed errors. Catch and inspect:
```typescript
try {
await client.checkoutSessions.create({...});
} catch (error) {
if (error instanceof DodoPayments.APIError) {
console.error(error.status, error.message);
}
}
```
### Retries
The SDK retries twice by default with a short exponential backoff on connection errors and `408`, `409`, `429`, and `5xx` responses. Do not add an unconditional custom retry loop.
Override the default for all requests when constructing the client:
```typescript
const clientWithoutRetries = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
maxRetries: 0,
});
```
Or override it for one request:
```typescript
await client.checkoutSessions.create(
{
product_cart: [{ product_id: 'pdt_example', quantity: 1 }],
customer: { email: '[email protected]' },
},
{ maxRetries: 0 },
);
```
---
## Webhook Verification
Webhook signature verification is mandatory. Never trust the payload without verification.
Dodo implements the Standard Webhooks spec. The signed message is `webhook-id.webhook-timestamp.raw_body` (period-joined), HMAC-SHA256, base64-encoded.
**Use the SDK helper:**
```typescript
const event = client.webhooks.unwrap(req.body.toString(), {
headers: {
'webhook-id': req.headers['webhook-id'] as string,
'webhook-signature': req.headers['webhook-signature'] as string,
'webhook-timestamp': req.headers['webhook-timestamp'] as string,
},
});
```
The `unwrap()` method verifies the signature and parses the payload. If verification fails, it throws an error.
For detailed webhook setup, event types, and testing, see the `webhook-integration` skill.
---
## Common Mistakes
### 1. Granting access on `return_url` redirect
The browser redirect is not proof of payment. Always wait for the webhook.
```typescript
// WRONG
app.get('/success', (req, res) => {
grantAccess(req.query.customer_id); // No verification!
});
// CORRECT
app.post('/webhook', async (req, res) => {
const event = client.webhooks.unwrap(...);
if (event.type === 'payment.succeeded') {
grantAccess(event.data.customer.customer_id);
}
});
```
### 2. Hand-rolling webhook verification
Don't implement HMAC verification yourself. Use `client.webhooks.unwrap()`.
The old approach of signing just the payload is wrong because Standard Webhooks signs `webhook-id.webhook-timestamp.raw_body`. Hand-rolled HMAC will never match.
### 3. Re-serializing the request body
Webhook verification requires the exact raw body. If you parse JSON and re-stringify it, the signature breaks.
```typescript
// WRONG
const body = JSON.parse(req.body);
const event = client.webhooks.unwrap(JSON.stringify(body), {...});
// CORRECT
const event = client.webhooks.unwrap(req.body.toString(), {...});
```
### 4. Using deprecated APIs
Don't use `client.payments.create()` or `client.subscriptions.create()` for new integrations. Both are deprecated. Use `client.checkoutSessions.create()`.
### 5. Forgetting to set `environment: 'test_mode'`
The default is `live_mode`. Always pass `test_mode` explicitly during development to avoid charging real cards.
### 6. Storing API keys in code
Never hardcode keys. Always use environment variables.
### 7. Ignoring the `webhook-timestamp` header
The timestamp prevents replay attacks. `client.webhooks.unwrap()` validates it automatically, but if you hand-roll verification, check that the timestamp is recent (within a few minutes).
### 8. Using `unsafeUnwrap()` in production
`unsafeUnwrap()` skips signature verification. Use it only for unsigned test payloads from `dodo wh trigger`. Never use it for production webhooks.
---
## Resources
- [Dodo Payments Docs](https://docs.dodopayments.com)
- [API Reference](https://docs.dodopayments.com/api-reference/introduction)
- [SDK Repositories](https://github.com/dodopayments)
- [Checkout Sessions Guide](https://docs.dodopayments.com/developer-resources/checkout-session)
- [Webhook Guide](https://docs.dodopayments.com/developer-resources/webhooks)
- [Customer Portal](https://docs.dodopayments.com/features/customer-portal)
- [Subscription Integration](https://docs.dodopayments.com/developer-resources/subscription-integration-guide)
- [Credit-Based Billing](https://docs.dodopayments.com/features/credit-based-billing)
© dodopayments · MIT · dodopayments/skills