Dodo Payments Usage-Based Billing
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.
Overview
Usage-based billing is perfect for:
-
APIs: Charge per request or operation
-
AI Services: Bill per token, generation, or inference
-
Infrastructure: Charge for compute, storage, bandwidth
-
SaaS: Metered features alongside subscriptions
Core Concepts
Events
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 } }
Meters
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
Products with Usage Pricing
-
Price per unit (e.g., $0.001 per API call)
-
Free threshold (e.g., 1,000 free calls)
-
Automatic billing each cycle
Billing Example: 2,500 calls - 1,000 free = 1,500 × $0.02 = $30.00
Quick Start
- Create a Meter
In Dashboard → Meters → Create Meter:
-
Name: "API Requests"
-
Event Name: api.call (exact match, case-sensitive)
-
Aggregation: Count
-
Unit: "calls"
- Create Usage-Based Product
In Dashboard → Products → Create Product:
-
Select Usage-Based type
-
Connect your meter
-
Set pricing:
-
Price Per Unit: $0.001
-
Free Threshold: 1000
- Send Events
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',
}
}]
});
Implementation Examples
TypeScript/Node.js
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', });
Batch Event Ingestion
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' } }, ]);
Python
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" })
Go
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 Configuration
Aggregation Types
Count (API Calls, Requests)
Meter: API Requests Event Name: api.call Aggregation: Count Unit: calls
Sum (Tokens, Bytes)
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 }] });
Max (Peak Concurrent Users)
Meter: Peak Users Event Name: concurrent.users Aggregation: Max Over Property: count Unit: users
Last (Current Storage)
Meter: Storage Used Event Name: storage.snapshot Aggregation: Last Over Property: bytes Unit: GB
Event Filtering
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.
Common Use Cases
AI Token Billing
// 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');
Image Generation
// 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,
}
}]
});
}
API Rate Tracking
// 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',
}
}]
});
}
Storage Billing
// 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
Next.js Integration
API Route for Usage Tracking
// 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 } ); } }
Usage Tracking Hook
// 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',
});
}; }
Hybrid Billing
Combine usage-based with subscriptions:
Subscription + Usage Overage
// 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
Multiple Meters per Product
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)
Credit-Based Meter Billing
Link meters to credit entitlements so usage events deduct from a customer's credit balance instead of charging per-unit:
-
Create a credit entitlement (Dashboard → Products → Credits)
-
Create a usage-based product with a meter
-
On the meter, toggle Bill usage in Credits
-
Select the credit entitlement and set Meter units per credit
// 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:
-
Overage disabled: Usage is blocked
-
Overage enabled: Usage continues and overage is tracked per your configured behavior (forgive, bill, or carry deficit)
Best Practices
- Unique Event IDs
Always generate unique IDs for idempotency:
const eventId = ${eventName}_${Date.now()}_${crypto.randomUUID()};
- Batch Events
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);
- Handle Failures Gracefully
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 } } }
- Include Relevant Metadata
Add context for debugging and filtering:
metadata: { endpoint: '/v1/generate', method: 'POST', model: 'gpt-4', user_id: 'internal_user_123', request_id: requestId, }
- Monitor in Dashboard
Check Meters dashboard for:
-
Event volume and trends
-
Usage per customer
-
Aggregated quantities
Pricing Examples
API Service
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
AI Token Service
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
Image Generation
Meter: Images (Count) Price: $0.05 per image Free Threshold: 10 images
Customer generates 100 images: (100 - 10) × $0.05 = $4.50
Resources
-
Usage-Based Billing Guide
-
Meters Documentation
-
Event Ingestion
-
AI Chat App Tutorial
-
Hybrid Billing Models
-
Credit-Based Billing