dodo-best-practices について
dodo-best-practicesは、Dodo Paymentsを統合する開発者向けのスキルです。複数言語でのSDK設定、決済フロー、フレームワークアダプター、Webhook検証を説明します。
主な機能
- 01複数言語でのSDK設定
- 02フレームワーク別アダプター
- 03チェックアウトとサブスクリプションの概念
- 04Webhook署名の検証
- 05APIエラー、ページネーション、リトライ
活用シーン
- 01Dodo Payments統合の開始
- 02Webフレームワーク用アダプターの選択
- 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 をインストールして初期化する必要がある場合
- 標準的な決済フローとウェブフックの検証について理解したい場合
- フレームワークアダプターや決済方法を選ぶ場合
- Dodo が処理することと、自分で構築する必要があることを把握したい場合
Dodo Payments とは
Dodo Payments は Merchant of Record(MoR)です。つまり、Dodo はすべての取引における法的な販売者となり、あらゆる管轄区域での売上税の登録と計算、税金の当局への納付、カードネットワークに関する異議申し立てやチャージバックの管理を行います。開発者は、売上税の計算エンジンや異議申し立て処理システムを構築する必要はありません。チェックアウトセッションを作成し、ウェブフックをリッスンして、決済が成功したらアクセス権を付与します。
環境のセットアップ
ベース URL
実際に使用できるベース URL は 2 つだけです。
- 本番環境:
https://live.dodopayments.com - テスト環境:
https://test.dodopayments.com
api.dodopayments.com は使用しないでください。DNS レコードがなく、接続できません。
API キー
キーは 2 つの形式があります。
- テスト:
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 のローダー/アクションを使用する場合 |
| TanStack Start | @dodopayments/tanstack | TanStack Start を使用する場合 |
| Better Auth | @dodopayments/better-auth | Better Auth と統合する場合 |
| Convex | @dodopayments/convex | Convex バックエンドを使用する場合 |
| Bun | @dodopayments/bun | Bun.serve() を使用する場合 |
各アダプターは、フレームワークの規約に合わせたチェックアウト、顧客ポータル、ウェブフックのハンドラーを提供します。
基本概念
商品
販売するアイテムです。ダッシュボードまたは API で作成します。種類は次のとおりです。
- 買い切り:1 回限りの購入
- サブスクリプション:定期請求
- 従量課金:使用量に応じた課金
顧客
購入者を表します。複数の決済方法、サブスクリプション、クレジット残高を持つことができます。明示的に作成することも、チェックアウト中に暗黙的に作成することもできます。
チェックアウトセッション
主要な決済手段です。サーバー側でセッションを作成し、ホスト型チェックアウト URL に顧客をリダイレクトして、決済を確認するためにウェブフックをリッスンします。
チェックアウト設定の詳細については checkout-integration スキルを、定期請求のライフサイクル管理については subscription-integration スキルを参照してください。
サブスクリプション
スケジュールに従って繰り返し発生する料金です。チェックアウトセッションまたはサブスクリプション API で管理します。ライフサイクル、トライアル、プラン変更、オンデマンド課金については、subscription-integration スキルをご覧ください。
Webhook
リアルタイムのイベント通知です。Dodo は payment.succeeded、subscription.active、refund.succeeded、credit.deducted などのイベントを送信します。Webhook の署名検証については、webhook-integration スキルで説明しています。
クレジットの利用権
製品に紐付けられた仮想残高(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 レスポンスに対し、短い指数バックオフを使って 2 回再試行します。条件を設けない独自の再試行ループを追加しないでください。
クライアントの作成時に、すべてのリクエストでデフォルト設定を上書きできます。
const clientWithoutRetries = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
maxRetries: 0,
});
または、1 つのリクエストに対して上書きできます。
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には決して使用しないでください。