dodo-best-practices
Guide for initial Dodo Payments setup, including SDK installation, test and live environments, API keys, and the canonical checkout-to-webhook architecture.
Works with
---
name: dodo-best-practices
description: Guide for initial Dodo Payments setup, including SDK installation, test and live environments, API keys, and the canonical checkout-to-webhook architecture.
license: MIT
---
# 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/dodopayments-go@v1.110.0
```
```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;
$client = new Client(
bearerToken: getenv('DODO_PAYMENTS_API_KEY') ?: 'My Bearer Token',
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
DodoPaymentsClient client = DodoPaymentsOkHttpClient.fromEnv();
// Reads DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_WEBHOOK_KEY, DODO_PAYMENTS_BASE_URL
```
### Kotlin
```xml
<dependency>
<groupId>com.dodopayments.api</groupId>
<artifactId>dodo-payments-kotlin</artifactId>
<version>1.110.0</version>
</dependency>
```
```kotlin
val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.fromEnv()
```
---
## 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: 'customer@example.com' },
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: 'customer@example.com' },
},
{ 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)More API Design skills
lark-event
larksuite/cli
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses.
lark-contact
larksuite/cli
飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名,以及按关键词搜索当前用户可见的机器人 / 智能体(agent)。当用户提到一个名字要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。
lark-openapi-explorer
larksuite/cli
飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。

