usage-based-billing

Dodo Payments Usage-Based Billing

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "usage-based-billing" with this command: npx skills add dodopayments/skills/dodopayments-skills-usage-based-billing

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

  1. Create a Meter

In Dashboard → Meters → Create Meter:

  • Name: "API Requests"

  • Event Name: api.call (exact match, case-sensitive)

  • Aggregation: Count

  • Unit: "calls"

  1. 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

  1. 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, &#x26;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

  1. Unique Event IDs

Always generate unique IDs for idempotency:

const eventId = ${eventName}_${Date.now()}_${crypto.randomUUID()};

  1. 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);

  1. 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 } } }

  1. 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, }

  1. 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

webhook-integration

No summary provided by upstream source.

Repository SourceNeeds Review
General

dodo-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

checkout-integration

No summary provided by upstream source.

Repository SourceNeeds Review