usage-based-billing
Guide for implementing usage-based billing with Dodo Payments - meters, events, pricing per unit, and metered subscriptions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Guide for implementing usage-based billing with Dodo Payments - meters, events, pricing per unit, and metered subscriptions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Guide for creating checkout sessions and payment flows with Dodo Payments - one-time, subscriptions, and overlay checkout.
Guide for implementing credit-based billing with Dodo Payments - credit entitlements, balances, ledger management, rollover, overage, and meter-based deduction.
Comprehensive guide for integrating Dodo Payments - the all-in-one payment and billing platform for SaaS and AI products.
Guide for implementing subscription billing with Dodo Payments - trials, upgrades, downgrades, and on-demand billing.
Complete guide for setting up and handling Dodo Payments webhooks for real-time payment event notifications.
| name | usage-based-billing |
| description | Guide for implementing usage-based billing with Dodo Payments - meters, events, pricing per unit, and metered subscriptions. |
Reference: docs.dodopayments.com/features/usage-based-billing
Charge customers for what they actually use—API calls, storage, AI tokens, or any metric you define.
Usage-based billing is perfect for:
Usage actions sent from your application:
{
"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 }
}
Aggregate events into billable quantities:
| Aggregation | Use Case | Example |
|---|---|---|
| Count | Total events | API calls, image generations |
| Sum | Add values | Tokens used, bytes transferred |
| Max | Highest value | Peak concurrent users |
| Last | Most recent | Current storage used |
Billing Example: 2,500 calls - 1,000 free = 1,500 × $0.02 = $30.00
In Dashboard → Meters → Create Meter:
api.call (exact match, case-sensitive)In Dashboard → Products → Create Product:
import DodoPayments from "dodopayments";
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
});
await client.usageEvents.ingest({
events: [
{
event_id: `api_${Date.now()}_${Math.random()}`,
customer_id: "cus_abc123",
event_name: "api.call",
timestamp: new Date().toISOString(),
metadata: {
endpoint: "/v1/users",
method: "GET",
},
},
],
});
import DodoPayments from "dodopayments";
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
});
// Track single event
async function trackUsage(
customerId: string,
eventName: string,
metadata: Record<string, string>
) {
await client.usageEvents.ingest({
events: [
{
event_id: `${eventName}_${Date.now()}_${crypto.randomUUID()}`,
customer_id: customerId,
event_name: eventName,
timestamp: new Date().toISOString(),
metadata,
},
],
});
}
// Track API call
await trackUsage("cus_abc123", "api.call", {
endpoint: "/v1/generate",
method: "POST",
});
// Track token usage (for Sum aggregation)
await trackUsage("cus_abc123", "token.usage", {
tokens: "1500",
model: "gpt-4",
});
Send multiple events efficiently (max 1000 per request):
async function trackBatchUsage(
events: Array<{
customerId: string;
eventName: string;
metadata: Record<string, string>;
timestamp?: string;
}>
) {
const formattedEvents = events.map((e, i) => ({
event_id: `batch_${Date.now()}_${i}_${crypto.randomUUID()}`,
customer_id: e.customerId,
event_name: e.eventName,
timestamp: e.timestamp || new Date().toISOString(),
metadata: e.metadata,
}));
await client.usageEvents.ingest({ events: formattedEvents });
}
// Batch track multiple API calls
await trackBatchUsage([
{
customerId: "cus_abc",
eventName: "api.call",
metadata: { endpoint: "/v1/users" },
},
{
customerId: "cus_abc",
eventName: "api.call",
metadata: { endpoint: "/v1/orders" },
},
{
customerId: "cus_xyz",
eventName: "api.call",
metadata: { endpoint: "/v1/products" },
},
]);
from dodopayments import DodoPayments
import uuid
from datetime import datetime
client = DodoPayments(bearer_token=os.environ["DODO_PAYMENTS_API_KEY"])
def track_usage(customer_id: str, event_name: str, metadata: dict):
client.usage_events.ingest(events=[{
"event_id": f"{event_name}_{datetime.now().timestamp()}_{uuid.uuid4()}",
"customer_id": customer_id,
"event_name": event_name,
"timestamp": datetime.now().isoformat(),
"metadata": metadata
}])
# Track AI token usage
track_usage("cus_abc123", "ai.tokens", {
"tokens": "2500",
"model": "claude-3",
"operation": "completion"
})
# Track image generation
track_usage("cus_abc123", "image.generated", {
"size": "1024x1024",
"model": "dall-e-3"
})
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/dodopayments/dodopayments-go"
"github.com/google/uuid"
)
func main() {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
ctx := context.Background()
_, err := client.UsageEvents.Ingest(ctx, &dodopayments.UsageEventIngestParams{
Events: []dodopayments.UsageEvent{{
EventID: fmt.Sprintf("api_%d_%s", time.Now().Unix(), uuid.New().String()),
CustomerID: "cus_abc123",
EventName: "api.call",
Timestamp: time.Now().Format(time.RFC3339),
Metadata: map[string]string{
"endpoint": "/v1/users",
"method": "GET",
},
}},
})
if err != nil {
panic(err)
}
}
Meter: API Requests
Event Name: api.call
Aggregation: Count
Unit: calls
Meter: Token Usage
Event Name: token.usage
Aggregation: Sum
Over Property: tokens
Unit: tokens
Events must include the property in metadata:
await client.usageEvents.ingest({
events: [
{
event_id: "token_123",
customer_id: "cus_abc",
event_name: "token.usage",
metadata: { tokens: "1500" }, // This value gets summed
},
],
});
Meter: Peak Users
Event Name: concurrent.users
Aggregation: Max
Over Property: count
Unit: users
Meter: Storage Used
Event Name: storage.snapshot
Aggregation: Last
Over Property: bytes
Unit: GB
Filter which events count toward the meter:
Filter Logic: AND
Conditions:
- Property: tier, Equals: "premium"
- Property: status, Equals: "success"
Only events matching ALL conditions are counted.
// Meter: AI Tokens (Sum aggregation over "tokens" property)
async function trackAIUsage(
customerId: string,
promptTokens: number,
completionTokens: number,
model: string
) {
const totalTokens = promptTokens + completionTokens;
await client.usageEvents.ingest({
events: [
{
event_id: `ai_${Date.now()}_${crypto.randomUUID()}`,
customer_id: customerId,
event_name: "ai.tokens",
timestamp: new Date().toISOString(),
metadata: {
tokens: totalTokens.toString(),
prompt_tokens: promptTokens.toString(),
completion_tokens: completionTokens.toString(),
model,
},
},
],
});
}
// After AI completion
await trackAIUsage("cus_abc", 500, 1200, "gpt-4");
// Meter: Images Generated (Count aggregation)
async function trackImageGeneration(
customerId: string,
imageSize: string,
model: string
) {
await client.usageEvents.ingest({
events: [
{
event_id: `img_${Date.now()}_${crypto.randomUUID()}`,
customer_id: customerId,
event_name: "image.generated",
timestamp: new Date().toISOString(),
metadata: {
size: imageSize,
model,
},
},
],
});
}
// Middleware for Express/Next.js
async function trackAPIUsage(req: Request, customerId: string) {
await client.usageEvents.ingest({
events: [
{
event_id: `api_${Date.now()}_${crypto.randomUUID()}`,
customer_id: customerId,
event_name: "api.call",
timestamp: new Date().toISOString(),
metadata: {
endpoint: req.url,
method: req.method,
user_agent: req.headers["user-agent"] || "unknown",
},
},
],
});
}
// Meter: Storage (Last aggregation - snapshot of current usage)
async function updateStorageUsage(customerId: string, bytesUsed: number) {
await client.usageEvents.ingest({
events: [
{
event_id: `storage_${Date.now()}_${customerId}`,
customer_id: customerId,
event_name: "storage.snapshot",
timestamp: new Date().toISOString(),
metadata: {
bytes: bytesUsed.toString(),
gb: (bytesUsed / 1024 / 1024 / 1024).toFixed(2),
},
},
],
});
}
// Call periodically or after storage changes
await updateStorageUsage("cus_abc", 5368709120); // 5GB
// app/api/track-usage/route.ts
import { NextRequest, NextResponse } from "next/server";
import DodoPayments from "dodopayments";
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
});
export async function POST(req: NextRequest) {
const { customerId, eventName, metadata } = await req.json();
try {
await client.usageEvents.ingest({
events: [
{
event_id: `${eventName}_${Date.now()}_${crypto.randomUUID()}`,
customer_id: customerId,
event_name: eventName,
timestamp: new Date().toISOString(),
metadata,
},
],
});
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
// hooks/useUsageTracking.ts
import { useCallback } from "react";
export function useUsageTracking(customerId: string) {
const trackUsage = useCallback(
async (eventName: string, metadata: Record<string, string>) => {
await fetch("/api/track-usage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ customerId, eventName, metadata }),
});
},
[customerId]
);
return { trackUsage };
}
// Usage in component
function AIChat() {
const { trackUsage } = useUsageTracking("cus_abc123");
const handleGenerate = async () => {
const result = await generateAIResponse(prompt);
// Track token usage
await trackUsage("ai.tokens", {
tokens: result.totalTokens.toString(),
model: "gpt-4",
});
};
}
Combine usage-based with subscriptions:
// 1. Customer subscribes to plan with included usage
// Product: Pro Plan - $49/month + $0.01/call after 10,000 free
// 2. Track all usage events
await trackUsage("cus_abc", "api.call", { endpoint: "/v1/generate" });
// 3. Dodo automatically:
// - Applies free threshold (10,000 calls)
// - Charges overage at $0.01/call
// - Combines with subscription fee
Attach up to 10 meters to a single product:
Product: AI Platform
├── Meter: API Calls ($0.001/call, 1000 free)
├── Meter: Token Usage ($0.01/1000 tokens)
├── Meter: Image Generations ($0.05/image, 10 free)
└── Meter: Storage ($0.10/GB)
Link meters to credit entitlements so usage events deduct from a customer's credit balance instead of charging per-unit:
// Meter: AI Tokens (Sum aggregation over "tokens")
// Credit: "AI Credits" with 10,000 credits/cycle
// Meter units per credit: 1000 (1,000 tokens = 1 credit)
// Usage events deduct credits automatically
await client.usageEvents.ingest({
events: [
{
event_id: `ai_${Date.now()}_${crypto.randomUUID()}`,
customer_id: "cus_abc123",
event_name: "ai.tokens",
timestamp: new Date().toISOString(),
metadata: { tokens: "1500", model: "gpt-4" },
},
],
});
// Check remaining credit balance
const balance = await client.creditEntitlements.balances.get(
"cent_ai_credits",
"cus_abc123"
);
console.log(`Credits remaining: ${balance.available_balance}`);
Credit deduction runs via a background worker every minute using FIFO ordering (oldest grants consumed first). When credits run out:
Always generate unique IDs for idempotency:
const eventId = `${eventName}_${Date.now()}_${crypto.randomUUID()}`;
For high-volume, batch events (max 1000/request):
// Queue events and send in batches
const eventQueue: Event[] = [];
function queueEvent(event: Event) {
eventQueue.push(event);
if (eventQueue.length >= 100) {
flushEvents();
}
}
async function flushEvents() {
if (eventQueue.length === 0) return;
const batch = eventQueue.splice(0, 1000);
await client.usageEvents.ingest({ events: batch });
}
// Flush periodically
setInterval(flushEvents, 5000);
Implement retry logic for event ingestion:
async function trackWithRetry(event: Event, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await client.usageEvents.ingest({ events: [event] });
return;
} catch (error) {
if (i === retries - 1) throw error;
await sleep(1000 * Math.pow(2, i)); // Exponential backoff
}
}
}
Add context for debugging and filtering:
metadata: {
endpoint: '/v1/generate',
method: 'POST',
model: 'gpt-4',
user_id: 'internal_user_123',
request_id: requestId,
}
Check Meters dashboard for:
Meter: API Calls (Count)
Price: $0.001 per call
Free Threshold: 1,000 calls/month
Customer uses 15,000 calls:
(15,000 - 1,000) × $0.001 = $14.00
Meter: Tokens (Sum over "tokens")
Price: $0.00001 per token ($0.01 per 1,000)
Free Threshold: 10,000 tokens
Customer uses 50,000 tokens:
(50,000 - 10,000) × $0.00001 = $0.40
Meter: Images (Count)
Price: $0.05 per image
Free Threshold: 10 images
Customer generates 100 images:
(100 - 10) × $0.05 = $4.50