track()
Record usage for metered billing
track()
Record usage events for metered billing. Use this to track API calls, storage, messages, or any countable resource.
Signature
await billwave.track({
customer: string, // Your internal user ID
feature: string, // Feature key being used
value?: number, // Amount to track (default: 1)
entity?: string, // Optional entity scope (seat, workspace, org)
customerData?: { // Optional: auto-create customer
email: string,
name?: string,
metadata?: Record<string, unknown>,
},
metadata?: object, // Optional event metadata
}): Promise<TrackResult>Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
customer | string | ✅ | Your identifier for the customer (e.g. your user or workspace ID). Also accepts the customer's email or internal ID. |
feature | string | ✅ | Feature slug (or ID) to record usage against |
value | number | - | Amount to add. Default 1. Must be >= 0. |
entity | string | - | Scope usage to an entity (seat, workspace…). The entity must already exist. |
customerData | object | - | Creates the customer on first use if it doesn't exist. See below. |
metadata | object | - | Free-form event metadata stored with the usage record (auditing, debugging) |
customerData — create on first use
Customers don't need to be created up front. Pass customerData and Billwave will create the customer the first time it sees the customer id. On later calls customerData only backfills what is missing (a name if the customer has none) — it never overwrites existing values; use billwave.customer() to update a customer.
await billwave.track({
customer: "workspace_alpha", // becomes the customer's externalId
feature: "agent_turns",
customerData: {
email: "owner@acme.com", // required — used for receipts/checkout
name: "Acme (Alpha workspace)", // optional
metadata: { plan_hint: "team" }, // optional, free-form
},
});| Field | Type | Required | Notes |
|---|---|---|---|
email | string | ✅ | Currently required for every customer; if your billing entity is a workspace, pass the owner's or a billing address |
name | string | - | Display name |
metadata | object | - | Stored on the customer |
Without customerData, tracking an unknown customer returns code: "customer_not_found" (HTTP 404) and records nothing — unless the customer value itself is an email address, in which case the customer is created from it. If a newly created customer matches a plan with autoEnable: true, that plan is attached immediately so the very first track can succeed.
The same customer id then works for reads: billwave.customer.get("workspace_alpha"), usageHistory, and GET /customers/{id}.
When using entity, the entity must be created with
addEntity() first. Tracking a non-existent entity
returns an entity_not_found error.
Response
A successful track returns the full post-increment entitlement state, so you rarely need a follow-up check:
interface TrackResult {
success: boolean;
allowed: boolean; // Whether the usage was accepted
code: string; // tracked, tracked_overage, addon_credits_used, limit_exceeded, …
environment: "sandbox" | "live"; // Where this was recorded (also X-Billwave-Environment header)
unlimited: boolean; // true when no finite cap applies (limit === null)
usage: number | null; // Usage this period, after this call
limit: number | null; // Plan limit; null = unlimited
balance: number | null; // limit - usage; null = unlimited
resetsAt: string | null; // ISO timestamp of the next reset
resetInterval: string | null; // "monthly", "daily", …
credits:
| {
source: "credit_system";
systemSlug: string;
costPerUnit: number;
bonusBalance: number;
addonBalance: number;
totalBalance: number | null;
plan: { used: number; limit: number | null; balance: number | null; resetsAt: string };
}
| {
source: "prepaid";
bonusBalance: number;
addonBalance: null;
totalBalance: number | null;
plan: { used: number; limit: number | null; balance: number | null; resetsAt: string };
}
| null;
details: {
message: string; // Human-readable explanation
plan?: string; // Slug of the plan granting access (as in your catalog)
planName?: string;
trial?: boolean;
trialEndsAt?: string | null;
creditSystem?: string;
overage?: { … }; // Present when this usage went into billable overage
pricing?: { … }; // Present for chargeable metered features
};
}{
"success": true,
"allowed": true,
"code": "tracked",
"environment": "sandbox",
"unlimited": false,
"usage": 3,
"limit": 100,
"balance": 97,
"resetsAt": "2026-10-01T00:00:00.000Z",
"resetInterval": "monthly",
"credits": null,
"details": { "message": "Tracked 3 agent_turns on Pro.", "plan": "pro", "planName": "Pro" }
}Result codes
code | allowed | Meaning |
|---|---|---|
tracked | true | Recorded within the plan limit |
tracked_overage | true | Recorded beyond the limit; billed as overage |
addon_credits_used | true | Plan credits exhausted; add-on credits covered it |
bonus_credits_used | true | Covered by a manual bonus balance |
limit_exceeded | false | Limit reached and overage is blocked — nothing recorded |
insufficient_credits | false | Not enough credits — nothing recorded |
customer_not_found | false | Unknown customer and no customerData (HTTP 404) |
feature_not_found | false | Unknown feature slug |
no_active_subscription | false | Customer has no active plan |
feature_not_in_plan | false | The active plan doesn't include this feature |
entity_not_found | false | entity given but not created via addEntity() |
Errors that are not entitlement results (invalid body, bad key, wrong environment) come back as { success: false, error: { code, message }, environment } with HTTP 400/401.
Examples
Basic Usage Tracking
import { Billwave } from "@digvijay-x1/billwave";
const billwave = new Billwave({ secretKey: process.env.BILLWAVE_API_KEY });
// Track a single API call
await billwave.track({
customer: "user_123",
feature: "api_calls",
});Track Custom Amounts
// Track storage usage (e.g., 1.5 MB uploaded)
await billwave.track({
customer: "user_123",
feature: "storage_mb",
value: 1.5,
});
// Track multiple items at once
await billwave.track({
customer: "user_123",
feature: "messages_sent",
value: 10,
});With Metadata
await billwave.track({
customer: "user_123",
feature: "api_calls",
value: 1,
metadata: {
endpoint: "/api/generate",
responseTime: 234,
model: "gpt-4",
},
});Check Then Track Pattern
// Common pattern: check quota, then track usage
async function processRequest(userId: string) {
// 1. Check if customer has remaining quota
const check = await billwave.check({
customer: userId,
feature: "api_calls",
});
if (!check.allowed) {
throw new Error(`Quota exceeded. Resets at ${check.resetsAt}`);
}
// 2. Process the request
const result = await expensiveOperation();
// 3. Track usage after successful completion
await billwave.track({
customer: userId,
feature: "api_calls",
});
return result;
}Billing Models
Pay-per-use
Track every unit and invoice at the end of the billing period:
// Track each AI token generated
await billwave.track({
customer: "user_123",
feature: "ai_tokens",
value: response.usage.total_tokens,
});Monthly Quotas
Track against a fixed monthly limit:
const result = await billwave.track({
customer: "user_123",
feature: "exports",
});
console.log(`${result.balance} exports remaining this month`);Credit-based
Consume from a credit-backed feature:
const result = await billwave.track({
customer: "user_123",
feature: "gpt-4",
value: 1,
});
if (result.credits?.source === "credit_system") {
console.log("Plan credits left:", result.credits.plan.balance);
console.log("Add-on credits left:", result.credits.addonBalance);
}Best Practices
- Track after success - Only track after the operation completes successfully
- Non-blocking - Use fire-and-forget for non-critical tracking
- Batch if needed - For high-volume, batch multiple events together
- Include context - Add metadata for debugging and analytics