usage-based-billing
Guide for implementing usage-based billing with Dodo Payments - meters, events, pricing per unit, and metered subscriptions.
What this skill does
# Dodo Payments Usage-Based Billing
**Reference: [docs.dodopayments.com/features/usage-based-billing](https://docs.dodopayments.com/features/usage-based-billing/introduction)**
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:
```json
{
"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:
1. **Name**: "API Requests"
2. **Event Name**: `api.call` (exact match, case-sensitive)
3. **Aggregation**: Count
4. **Unit**: "calls"
### 2. Create Usage-Based Product
In Dashboard → Products → Create Product:
1. Select **Usage-Based** type
2. Connect your meter
3. Set pricing:
- **Price Per Unit**: $0.001
- **Free Threshold**: 1000
### 3. Send Events
```typescript
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
```typescript
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):
```typescript
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
```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
```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:
```typescript
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
```typescript
// 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
```typescript
// 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()Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.