usage-based-billing について
SaaS、AI、インフラ製品に Dodo Payments の従量課金を実装するためのリファレンススキルです。メーターと料金の設定、イベント取り込み、使用履歴の照会方法を説明します。
主な機能
- 01イベントメーターと集計方法の定義
- 02冪等性を備えた使用イベントの取り込み
- 03従量単価と無料枠の設定
- 04顧客およびサブスクリプション使用量の照会
- 05メーターとクレジット残高の連携
活用シーン
- 01API 呼び出しや AI トークン使用量への課金
- 02課金対象となるストレージや帯域幅の追跡
- 03従量料金とサブスクリプションの併用
- 04請求期間ごとの使用履歴表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dodopayments/ skills --skill usage-based-billingインストール後、Claude Code で/usage-based-billingを入力して読み込まれたか確認します。
同梱ファイルreferences/instrumentation.md
詳細
Dodo Payments の従量課金
参考:docs.dodopayments.com/features/usage-based-billing
API 呼び出し、ストレージ、AI トークン、または定義したあらゆる指標など、顧客が実際に使用した分だけ請求します。
このスキルを使用する場面
- 消費量(API 呼び出し、トークン、ストレージ、帯域幅)に基づいて顧客に請求する必要がある場合。
- 使用量に応じた料金をサブスクリプションや一回限りの購入と組み合わせたい場合。
- イベントを追跡して集計し、課金対象の数量にする必要がある場合。
- 従量制機能を備えた AI サービス、SaaS プラットフォーム、またはインフラ製品を構築している場合。
基本概念
イベント
アプリケーションから Dodo に送信される使用量レコードです。各イベントは顧客に紐付けられ、event_name によってメーターと照合されます。
{
"event_id": "evt_unique_123",
"customer_id": "cus_abc123",
"event_name": "api.call",
"timestamp": "2025-01-21T10:30:00Z",
"metadata": { "endpoint": "/v1/users", "tokens": 150 }
}
メーター
イベントをフィルタリング、集計して、課金対象の数量にします。メーターでは次の項目を指定します。
- イベント名:照合するイベント(大文字と小文字を区別)
- 集計タイプ:イベントをどのようにまとめるか(count、sum、max、last)
- 測定単位:課金単位(呼び出し回数、トークン、GB など)
- 任意のフィルター:カウント対象となるイベントの条件
集計タイプ
| タイプ | 用途 | 例 |
|---|---|---|
| Count | イベントの合計数 | API 呼び出し、画像生成 |
| Sum | プロパティの値を合計 | 使用トークン数、転送バイト数 |
| Max | 期間内の最大値 | ピーク時の同時ユーザー数 |
| Last | 最新の値 | 現在の使用ストレージ量 |
sum、max、last では、集計するメタデータプロパティを指定します。
料金設定
メーターを商品価格に紐付け、単位ごとに課金します。
- 単位あたりの価格:例:API 呼び出し 1 回あたり $0.001
- 無料しきい値:例:月あたり 1,000 回まで無料
- 課金計算式:
(使用量 − しきい値) × 単位あたりの価格
例:2,500 回 − 1,000 回(無料)= 1,500 × $0.02 = $30.00
メーターのライフサイクル
メーターを作成する
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
const meter = await client.meters.create({
name: 'API Requests',
event_name: 'api.call',
aggregation: { type: 'count' },
measurement_unit: 'calls',
description: 'Track API calls per customer',
});
console.log(meter.id); // mtr_...
合計(sum)集計の場合は、集計するプロパティを指定します。
const meter = await client.meters.create({
name: 'Token Usage',
event_name: 'ai.tokens',
aggregation: { type: 'sum', key: 'tokens' },
measurement_unit: 'tokens',
});
メーターの一覧表示と取得
// List all meters
const meters = await client.meters.list();
// Retrieve a specific meter
const meter = await client.meters.retrieve('mtr_abc123');
アーカイブとアーカイブ解除
メーターは削除ではなく、アーカイブします。アーカイブされたメーターは新しいイベントを受け付けなくなりますが、履歴データは保持されます。
// Archive a meter
await client.meters.archive('mtr_abc123');
// Unarchive to resume
await client.meters.unarchive('mtr_abc123');
イベントの取り込み
イベントを送信する
async function recordApiCall(
customerId: string,
requestId: string,
occurredAt: string,
): Promise<number> {
const response = await client.usageEvents.ingest({
events: [{
event_id: `api-call:${requestId}`,
customer_id: customerId,
event_name: 'api.call',
timestamp: occurredAt,
metadata: {
endpoint: '/v1/users',
method: 'GET',
},
}],
});
return response.ingested_count;
}
requestId は元となる API 操作を識別し、再試行時にも変更されないようにする必要があります。取り込みの試行中に生成しないでください。
イベントスキーマ
| フィールド | 必須 | 備考 |
|---|---|---|
event_id | はい | 冪等性を確保するための一意の識別子です。同じリクエスト内で ID が重複すると、リクエスト全体が拒否されます。 |
customer_id | はい | Dodo Payments の顧客 ID です。 |
event_name | はい | メーターのイベント名と完全に一致する必要があります(大文字と小文字を区別)。 |
timestamp | いいえ | ISO-8601 形式の日時です。デフォルトは現在の UTC 時刻です。過去 1 時間以内、または未来 5 分以内である必要があります。 |
metadata | いいえ | 文字列、整数、数値、またはブール値を含むオブジェクトです。最大 50 組で、キーの長さは 100、値の長さは 500 です。ネストされたオブジェクトや配列は使用できません。 |
冪等性と重複排除
- 操作ごとに、グローバルに一意な
event_idを 1 つ割り当てます。 - 変更されないリクエスト、ジョブ、生成、またはスナップショットの ID から ID を導出し、すべての再試行で同じ ID を再利用します。
- 1 回のリクエスト内で ID が重複すると、バッチ全体が拒否されます。
- 以前のリクエストですでに取り込まれた ID は、再試行を安全に行えるよう、通知なく無視されます。
バッチ取り込み
1 回のリクエストで最大 1,000 件のイベントを送信できます。
async function trackBatchUsage(
events: Array<{
operationId: string;
customerId: string;
eventName: string;
occurredAt: string;
metadata: Record<string, string | number | boolean>;
}>
) {
const formattedEvents = events.map((event) => ({
event_id: `usage:${event.operationId}`,
customer_id: event.customerId,
event_name: event.eventName,
timestamp: event.occurredAt,
metadata: event.metadata,
}));
await client.usageEvents.ingest({ events: formattedEvents });
}
// Batch track multiple API calls
await trackBatchUsage([
{ operationId: 'req_101', customerId: 'cus_abc', eventName: 'api.call', occurredAt: '2026-08-01T10:00:00Z', metadata: { endpoint: '/v1/users' } },
{ operationId: 'req_102', customerId: 'cus_abc', eventName: 'api.call', occurredAt: '2026-08-01T10:00:01Z', metadata: { endpoint: '/v1/orders' } },
{ operationId: 'req_103', customerId: 'cus_xyz', eventName: 'api.call', occurredAt: '2026-08-01T10:00:02Z', metadata: { endpoint: '/v1/products' } },
]);
イベントをクエリする
// List events for a customer
const events = await client.usageEvents.list({
customer_id: 'cus_abc123',
});
// Retrieve a specific event
const event = await client.usageEvents.retrieve('evt_abc123');
料金モデル
ユニット単価
現在、文書化され、運用可能な唯一の料金モデルです。メーターの紐付けには、次を使用します。
price_per_unit:小数を表す文字列(整数部は最大5桁、小数部は12桁)free_threshold:任意の整数(この値を下回る使用量には課金されません)
課金計算式:(usage − threshold) × price_per_unit
完全な商品作成リクエストの正規の情報源は、product-catalog-management スキルです。単数形の price オブジェクトと、その中にある type: 'usage_based_price' およびネストされた meters 配列が定義されています。ここでは別の製品スキーマを定義しないでください。
注: 段階制、累進制、従量制、階段型の料金モデルは、現在 Dodo Payments API では文書化されていません。現時点では、無料しきい値を設定したユニット単価を使用してください。
アプリケーションへの計測の組み込み
詳細なガイド:references/instrumentation.md。
以下を扱います。
- API 呼び出しの計測
- AI トークン使用量の計測
- ストレージ使用量の計測
表示用の使用量の照会
使用履歴の取得
// Auto-paginates across ALL billing periods; narrow with start_date/end_date/meter_id.
for await (const period of client.subscriptions.retrieveUsageHistory('sub_abc123', {
start_date: '2025-01-01T00:00:00Z',
page_size: 20,
})) {
console.log(period); // one record per billing period, with per-meter usage
}
これはサブスクリプションの全期間を対象とし、請求期間ごとに整理された、ページ分割された使用履歴です。現在の期間だけではありません。start_date、end_date、meter_id で絞り込み、最初のページだけを読み込むのではなく、ページを反復処理する(または for await を使う)ようにしてください。
クレジットベースの請求との統合
使用イベントは、ユニットごとに課金する代わりに、顧客のクレジット残高から差し引くことができます。クレジットの特典、残高、台帳管理の詳細については、credit-based-billing スキルを参照してください。
メーターをクレジットに紐付けるには、次の手順に従います。
- クレジット特典(例:「AI Credits」)を作成します。
- クレジット特典を同じ商品に紐付けます。
- メーターで クレジットで使用量を請求 を有効にします。
credit_entitlement_idとmeter_units_per_creditを設定します(例:1,000トークン = 1クレジット)。
無料しきい値は、クレジットで請求するメーターには適用されません。すべてのユニットがクレジット控除の対象になります(無料しきい値が適用されるのは、メーターが金銭で請求する場合のみです)。およそ1分ごとにバックグラウンドワーカーが新しい使用量を集計し、メーターとクレジットの比率に基づいて換算したうえで、有効期限が切れていない付与分から差し引きます。有効期限が最も近い付与分から先に消費されます。クレジットがなくなると、設定された超過使用時の動作が適用されます。
Webhook の統合
使用イベントは、監視と照合のために Webhook をトリガーします。Webhook の設定と検証については、webhook-integration スキルを参照してください。
よくある間違い
1. 不安定なイベント ID や再利用したイベント ID を使う
変更されない業務処理をもとに、1つの ID を生成してください。別の処理に同じ ID を再利用すると使用量が破棄され、再試行のたびにタイムスタンプやランダムな ID を生成すると、同じ処理に二重課金される可能性があります。
// WRONG — a retry creates a new billable event
await client.usageEvents.ingest({
events: [{
event_id: `api-call:${Date.now()}:${crypto.randomUUID()}`,
customer_id: 'cus_abc',
event_name: 'api.call',
}],
});
// CORRECT — retry request req_123 with this same ID
await client.usageEvents.ingest({
events: [{
event_id: 'api-call:req_123',
customer_id: 'cus_abc',
event_name: 'api.call',
}],
});
別個のリクエストに api-call:req_123 を再利用しないでください。これはクレジット台帳のガイダンスと同じく、タイムアウトは新しい冪等性キーを生成する理由にはなりません。
2. ファイア・アンド・フォーゲット方式で取り込む
イベントを永続化せず、応答後に取り込み処理を開始しないでください。ユーザーに成功を返した後、使用量が Dodo に届く前にプロセスがクラッシュする可能性があります。
// WRONG — an acknowledged operation can lose its usage event
app.post('/api/generate', async (req, res) => {
const result = await generateAI(req.body);
res.json(result);
void client.usageEvents.ingest({ events: [result.usageEvent] });
});
// CORRECT — durable persistence completes before success is returned
app.post('/api/generate', async (req, res) => {
const result = await generateAI(req.body);
await usageOutbox.persist(result.usageEvent);
res.json(result);
});
再試行するワーカーは、API 呼び出しの計測で示すように、永続化されたペイロードを元の event_id で取り込みます。永続化に失敗した場合は、黙って課金不足にするのではなく、処理を再試行できるようエラーを返してください。
3. タイムスタンプのクロックスキュー
タイムスタンプは、過去1時間以内、または未来5分以内である必要があります。サーバーの時計が同期されていることを確認してください。
// WRONG — timestamp is 2 hours old
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
await client.usageEvents.ingest({
events: [{
event_id: 'evt_123',
customer_id: 'cus_abc',
event_name: 'api.call',
timestamp: oldTime, // Rejected
metadata: {}
}]
});
// CORRECT — use current time
await client.usageEvents.ingest({
events: [{
event_id: 'evt_123',
customer_id: 'cus_abc',
event_name: 'api.call',
timestamp: new Date().toISOString(),
metadata: {}
}]
});
4. クライアント側で取り込む
クライアント側のコードからイベントを送信しないでください。API キーが漏洩しないよう、必ずバックエンドから取り込んでください。
// WRONG — client-side
const trackUsage = async (eventName: string) => {
await fetch('https://test.dodopayments.com/events/ingest', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DODO_PAYMENTS_API_KEY}`, // Exposed!
'Content-Type': 'application/json'
},
body: JSON.stringify({ events: [...] })
});
};
// CORRECT — send the event from a backend worker using its persisted payload
await client.usageEvents.ingest({
events: [{
event_id: 'api-call:req_123',
customer_id: 'cus_abc',
event_name: 'api.call',
timestamp: '2026-08-01T10:30:00Z',
metadata: { endpoint: '/v1/users' },
}],
});
5. イベント名が一致しない
イベント名は大文字と小文字が区別されます。メーターのイベント名と完全に一致している必要があります。
// WRONG — meter expects "api.call", event sends "API.CALL"
const meter = await client.meters.create({
name: 'API Requests',
event_name: 'api.call',
aggregation: { type: 'count' },
measurement_unit: 'calls',
});
await client.usageEvents.ingest({
events: [{
event_id: 'api-call:req_123',
customer_id: 'cus_abc',
event_name: 'API.CALL', // Won't match
}]
});
// CORRECT
await client.usageEvents.ingest({
events: [{
event_id: 'api-call:req_123',
customer_id: 'cus_abc',
event_name: 'api.call', // Matches exactly
}]
});