mcpField
D
dodo-best-practices

dodo-best-practices

github.com/dodopayments开发13995 次安装3 周前更新
代码仓库

关于 dodo-best-practices

dodo-best-practices 面向集成 Dodo Payments 的开发者,介绍 SDK 配置、支付流程、框架适配器和 Webhook 验签,涵盖多种语言。

主要功能

  1. 01多种语言的 SDK 配置
  2. 02针对框架的适配器
  3. 03结账与订阅概念
  4. 04Webhook 签名验证
  5. 05API 错误、分页与重试

适用场景

  1. 01启动 Dodo Payments 集成
  2. 02为 Web 框架选择适配器
  3. 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'
});

构造函数选项:

选项类型默认值备注
bearerTokenstring环境变量 DODO_PAYMENTS_API_KEYAPI 调用必需
environment'test_mode' | 'live_mode''live_mode'开发时显式指定
webhookKeystring环境变量 DODO_PAYMENTS_WEBHOOK_KEY用于 webhooks.unwrap()
baseURLstring—覆盖项;不能与 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 技能。


标准支付流程

  1. 在服务器上创建结账会话,并提供产品 ID 和客户邮箱。
  2. 将客户重定向到返回的 checkout_url。
  3. 客户在托管结账页面付款。
  4. Dodo 向你的端点发送 Webhook(例如 payment.succeeded)。
  5. 使用 client.webhooks.unwrap() 验证 Webhook 签名。
  6. 仅在 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。


资源

评论

支持 Markdown