关于 dodo-best-practices
dodo-best-practices 面向集成 Dodo Payments 的开发者,介绍 SDK 配置、支付流程、框架适配器和 Webhook 验签,涵盖多种语言。
主要功能
- 01多种语言的 SDK 配置
- 02针对框架的适配器
- 03结账与订阅概念
- 04Webhook 签名验证
- 05API 错误、分页与重试
适用场景
- 01启动 Dodo Payments 集成
- 02为 Web 框架选择适配器
- 03实现经过验证的支付 Webhook
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dodopayments/ skills --skill dodo-best-practices安装后在 Claude Code 中输入/dodo-best-practices确认已加载。
详细介绍
Dodo Payments 集成指南
本技能介绍 Dodo Payments 的基础概念和设置。可在开始新的集成、设置 SDK 或了解核心支付流程时使用。
何时使用此技能
- 你正在构建新的支付集成,需要了解 Dodo 的架构
- 你需要使用自己的语言安装并初始化 SDK
- 你想了解标准支付流程和 Webhook 验证
- 你正在选择框架适配器或支付方式
- 你需要了解哪些工作由 Dodo 处理,哪些需要你自行构建
什么是 Dodo Payments
Dodo Payments 是名义商户(MoR)。这意味着 Dodo 是每笔交易的法定卖家,负责在所有司法管辖区办理销售税登记并计算税额、向相关机构缴纳税款,以及处理银行卡网络争议和拒付。作为开发者,你无需构建销售税引擎或争议处理系统。你只需创建结账会话、监听 Webhook,并在支付成功后授予访问权限。
环境设置
基础 URL
只有两个有效的基础 URL:
- 正式环境:
https://live.dodopayments.com - 测试环境:
https://test.dodopayments.com
切勿使用 api.dodopayments.com——它没有 DNS 记录,无法访问。
API 密钥
密钥有两种格式:
- 测试环境:
dodo_test_... - 正式环境:
dodo_live_...
环境变量
DODO_PAYMENTS_API_KEY # Bearer token for API requests
DODO_PAYMENTS_WEBHOOK_KEY # Secret for webhook signature verification
默认环境
如果省略 environment 参数,其默认值为 live_mode。开发时务必显式传入 test_mode。
SDK 安装与初始化
TypeScript/Node.js
npm install dodopayments
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode', // defaults to 'live_mode'
});
构造函数选项:
| 选项 | 类型 | 默认值 | 备注 |
|---|---|---|---|
bearerToken | string | 环境变量 DODO_PAYMENTS_API_KEY | API 调用必需 |
environment | 'test_mode' | 'live_mode' | 'live_mode' | 开发时显式指定 |
webhookKey | string | 环境变量 DODO_PAYMENTS_WEBHOOK_KEY | 用于 webhooks.unwrap() |
baseURL | string | — | 覆盖项;不能与 environment 同时使用 |
Python
pip install dodopayments
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
go get -u github.com/dodopayments/[email protected]
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
composer require "dodopayments/client:6.19.0"
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
gem "dodopayments", "~> 2.22.0"
dodo_payments = Dodopayments::Client.new(
bearer_token: ENV["DODO_PAYMENTS_API_KEY"],
environment: "test_mode"
)
Java
<dependency>
<groupId>com.dodopayments.api</groupId>
<artifactId>dodo-payments-java</artifactId>
<version>1.110.0</version>
</dependency>
// 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
<dependency>
<groupId>com.dodopayments.api</groupId>
<artifactId>dodo-payments-kotlin</artifactId>
<version>1.110.0</version>
</dependency>
// Without testMode() (or a test DODO_PAYMENTS_BASE_URL) the client targets live mode.
val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.builder()
.fromEnv()
.testMode()
.build()
框架适配器
Dodo 以 @dodopayments/* 的形式发布特定框架的软件包。请根据你的技术栈选择对应的软件包:
| 框架 | 软件包 | 适用场景 |
|---|---|---|
| Next.js | @dodopayments/nextjs | 使用 Next.js App Router 构建应用 |
| Nuxt | @dodopayments/nuxt | 使用 Nuxt 3+ 构建应用 |
| Express | @dodopayments/express | 使用 Express.js |
| Fastify | @dodopayments/fastify | 使用 Fastify |
| Hono | @dodopayments/hono | 使用 Hono(Edge/Node) |
| Astro | @dodopayments/astro | 使用 Astro 端点 |
| SvelteKit | @dodopayments/sveltekit | 使用 SvelteKit 服务端路由 |
| Remix | @dodopayments/remix | 使用 Remix loaders/actions |
| TanStack Start | @dodopayments/tanstack | 使用 TanStack Start |
| Better Auth | @dodopayments/better-auth | 与 Better Auth 集成 |
| Convex | @dodopayments/convex | 使用 Convex 后端 |
| Bun | @dodopayments/bun | 使用 Bun.serve() |
每个适配器都提供结账、客户门户和 Webhook 处理程序,并遵循相应框架的惯例。
核心概念
商品
你销售的商品。可在控制面板中创建,也可通过 API 创建。类型包括:
- 一次性:单次购买
- 订阅:定期计费
- 按用量计费:按计量用量收费
客户
代表买家。可拥有多个支付方式、订阅和余额。可显式创建,也可在结账时自动创建。
结账会话
主要的收款方式。在服务端创建会话,将客户重定向到托管结账 URL,并监听 Webhook 以确认支付。
有关详细的结账配置,请参阅 checkout-integration 技能;有关定期订阅生命周期管理,请参阅 subscription-integration 技能。
订阅
按计划定期扣费。通过结账会话或订阅 API 管理。有关生命周期、试用、套餐变更和按需扣费,请参阅 subscription-integration 技能。
Webhook
实时事件通知。Dodo 会发送 payment.succeeded、subscription.active、refund.succeeded 和 credit.deducted 等事件。webhook-integration 技能介绍了 Webhook 签名验证。
积分权益
附加到产品的虚拟余额(API 调用次数、令牌数、计算时长)。可按产品配置结转、超额使用和过期规则。请参阅 credit-based-billing 技能。
标准支付流程
- 在服务器上创建结账会话,并提供产品 ID 和客户邮箱。
- 将客户重定向到返回的
checkout_url。 - 客户在托管结账页面付款。
- Dodo 向你的端点发送 Webhook(例如
payment.succeeded)。 - 使用
client.webhooks.unwrap()验证 Webhook 签名。 - 仅在 Webhook 验证成功后授予访问权限。
切勿仅凭浏览器的 return_url 重定向就授予访问权限。Webhook 才是权威确认。
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 基础
身份验证
所有请求都使用 Bearer 令牌身份验证:
Authorization: Bearer dodo_live_...
分页
列表端点采用页码分页。它们接受 page_size 和 page_number,响应会在 items 中提供数据行:
const payments = await client.payments.list({
page_size: 50,
page_number: 0,
});
for (const payment of payments.items) {
console.log(payment.payment_id);
}
SDK 也可以为你遍历所有页面:
for await (const payment of client.payments.list()) {
console.log(payment.payment_id);
}
速率限制
Dodo 会实施速率限制。SDK 会按照下文所述自动重试 429 响应。
SDK 错误类
SDK 会抛出类型化错误。捕获并检查:
try {
await client.checkoutSessions.create({...});
} catch (error) {
if (error instanceof DodoPayments.APIError) {
console.error(error.status, error.message);
}
}
重试
默认情况下,SDK 会在连接错误以及 408、409、429 和 5xx 响应时,采用短暂的指数退避重试两次。不要添加无条件的自定义重试循环。
构造客户端时,可覆盖所有请求的默认设置:
const clientWithoutRetries = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
maxRetries: 0,
});
也可以针对单个请求覆盖:
await client.checkoutSessions.create(
{
product_cart: [{ product_id: 'pdt_example', quantity: 1 }],
customer: { email: '[email protected]' },
},
{ maxRetries: 0 },
);
Webhook 验证
必须验证 Webhook 签名。未经验证,切勿信任负载。
Dodo 实现了 Standard Webhooks 规范。签名消息为 webhook-id.webhook-timestamp.raw_body(以句点连接),使用 HMAC-SHA256 并以 base64 编码。
使用 SDK 助手:
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,
},
});
unwrap() 方法会验证签名并解析负载。如果验证失败,它会抛出错误。
有关 Webhook 设置、事件类型和测试的详细信息,请参阅 webhook-integration 技能。
常见错误
1. 在 return_url 重定向时授予访问权限
浏览器重定向不能证明付款已完成。请务必等待 Webhook。
// 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. 手动实现 Webhook 验证
不要自行实现 HMAC 验证。请使用 client.webhooks.unwrap()。
仅对负载签名的旧方法是错误的,因为 Standard Webhooks 会对 webhook-id.webhook-timestamp.raw_body 签名。手动实现的 HMAC 永远无法匹配。
3. 重新序列化请求正文
Webhook 验证需要完全一致的原始正文。如果你解析 JSON 后再重新字符串化,签名就会失效。
// 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. 使用已弃用的 API
新集成不要使用 client.payments.create() 或 client.subscriptions.create()。这两者都已弃用。请使用 client.checkoutSessions.create()。
5. 忘记设置 environment: 'test_mode'
默认值为 live_mode。开发期间请务必显式传入 test_mode,以免对真实卡片扣款。
6. 将 API 密钥存储在代码中
切勿将密钥硬编码。请始终使用环境变量。
7. 忽略 webhook-timestamp 标头
时间戳可以防止重放攻击。client.webhooks.unwrap() 会自动验证时间戳;但如果你手动实现验证,请检查时间戳是否足够新(在几分钟以内)。
8. 在生产环境中使用 unsafeUnwrap()
unsafeUnwrap() 会跳过签名验证。仅将其用于来自 dodo wh trigger 的未签名测试载荷。切勿将其用于生产环境的 Webhook。