UsageTap API Reference
Complete reference for the UsageTap REST API and SDK.
Table of Contents
- Authentication
- Request Headers
- Response Format
- Idempotency
- REST API Endpoints
- SDK API
- Type Definitions
- Error Codes
Authentication
All API requests require authentication via Bearer token or API key header.
Option 1: Bearer Token (Recommended)
Authorization: Bearer YOUR_USAGETAP_API_KEY
Option 2: API Key Header (Legacy)
x-api-key: YOUR_USAGETAP_API_KEY
If both headers are present, Authorization takes precedence.
Request Headers
Required Headers
Accept: application/vnd.usagetap.v1+json
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
Optional Headers
| Header | Description | Example |
|---|---|---|
Idempotency-Key |
Unique key for safe retries | 550e8400-e29b-41d4-a716-446655440000 |
x-usage-correlation-id |
Track related requests | corr_abc123 |
x-usage-sdk |
SDK identifier | js/0.7.1 |
Important: The Accept header must be application/vnd.usagetap.v1+json. Requests without this header will receive 406 Not Acceptable.
Response Format
All responses follow the canonical envelope format:
Success Response
{
result: {
status: "ACCEPTED",
code: string, // Status code (e.g., "CALL_BEGIN_SUCCESS")
message?: string, // Optional human-readable message
timestamp: string // ISO 8601 timestamp
},
data: { // Endpoint-specific data
// ...
},
correlationId: string // For request tracing
}
Error Response
{
result: {
status: "ERROR",
code: string, // Error code
message: string, // Error description
timestamp: string
},
error: {
code: string, // Detailed error code
message: string, // Error details
details?: object // Additional context
},
correlationId: string
}
Idempotency
UsageTap supports three methods for providing idempotency keys:
1. Request Header (Preferred)
POST /call_begin
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"customerId": "cust_123",
"requested": { "standard": true }
}
2. Request Body (Recommended Field: idempotencyKey)
{
"customerId": "cust_123",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"requested": { "standard": true }
}
3. Request Body (Deprecated: idempotency)
{
"customerId": "cust_123",
"idempotency": "550e8400-e29b-41d4-a716-446655440000",
"requested": { "standard": true }
}
Priority: Header > idempotencyKey > idempotency
Auto-Generated Keys
If no idempotency key is provided, UsageTap generates one deterministically by hashing:
- Organization ID
- Customer ID
- Feature name
- Requested entitlements
The resolved key is returned in data.idempotency.key.
Idempotency Behavior
- Same key + same payload → Returns cached response (same
callId) - Same key + different payload → Returns error
- Different key + same payload → Creates new call
REST API Endpoints
POST /call_begin
Start a usage tracking session and retrieve customer entitlements.
Endpoint: POST {baseUrl}/call_begin
Request Body:
{
customerId: string; // Required: Your customer identifier
feature?: string; // Optional: Feature being accessed
requested?: { // Optional: Requested capabilities
standard?: boolean; // Standard tier models
premium?: boolean; // Premium tier models
audio?: boolean; // Audio processing
image?: boolean; // Image generation
search?: boolean; // Web search
reasoningLevel?: "NONE" | "LOW" | "MEDIUM" | "HIGH";
};
holdUsd?: number; // Optional: PAYG hold hint (USD) for this call
idempotencyKey?: string; // Optional: Unique key for retries
tags?: string[]; // Optional: Tags for analytics
customerName?: string; // HIGHLY IMPORTANT BUT OPTIONAL: Display name (for new customers)
customerEmail?: string; // HIGHLY IMPORTANT BUT OPTIONAL: Email (for new customers)
stripeCustomerId?: string; // Optional: Link to Stripe customer
}
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "CALL_BEGIN_SUCCESS",
timestamp: "2025-11-05T12:00:00.000Z"
},
data: {
callId: string; // Use in call_end
startTime: string; // ISO 8601 timestamp
feature: string; // Echo of feature
tags?: string[]; // Echo of tags
newCustomer: boolean; // True if customer was just created
canceled: boolean; // True if subscription is canceled
policy: "NONE" | "BLOCK" | "DOWNGRADE";
// What customer can actually use
allowed: {
standard: boolean;
premium: boolean;
audio: boolean;
image: boolean;
search: boolean;
reasoningLevel: "NONE" | "LOW" | "MEDIUM" | "HIGH";
};
// Guidance for selecting models
entitlementHints: {
suggestedModelTier: "premium" | "standard" | "none";
reasoningLevel: "NONE" | "LOW" | "MEDIUM" | "HIGH";
policy: "NONE" | "BLOCK" | "DOWNGRADE";
downgrade?: {
reason: string; // Why downgraded (e.g., "PREMIUM_QUOTA_EXHAUSTED")
fallbackTier?: string; // Suggested fallback
};
};
// Current usage levels per meter
meters: {
[meterName: string]: {
remaining: number | null;
limit: number | null;
used: number;
unlimited: boolean;
ratio: number | null; // remaining/limit (0-1)
};
};
// Quick lookup for remaining ratios
remainingRatios: {
[meterName: string]: number | null;
};
// Subscription details
subscription: {
id: string;
usagePlanVersionId: string;
planName: string;
planVersion: string;
limitType: "NONE" | "BLOCK" | "DOWNGRADE";
reasoningLevel: "NONE" | "LOW" | "MEDIUM" | "HIGH";
lastReplenishedAt: string;
nextReplenishAt: string;
subscriptionVersion: number;
customerFriendlyName?: string;
customerEmail?: string;
stripeCustomerId?: string;
pending?: { // Scheduled plan change
usagePlanVersionId: string;
strategy: string;
effectiveAt: string;
};
};
// Organization-specific model mappings
models?: {
standard?: string[]; // Standard tier models
premium?: string[]; // Premium tier models
};
// Resolved idempotency key
idempotency: {
key: string; // Always matches callId
source: "explicit" | "derived";
};
// Vendor-specific hints (optional)
vendorHints?: {
preferredModel?: string;
maxInputTokens?: number;
maxResponseTokens?: number;
};
// Legacy fields (backward compatibility)
plan?: {
id: string;
name: string;
version: string;
};
balances?: {
tokensRemaining?: number;
searchesRemaining?: number;
};
},
correlationId: string
}
Example:
curl -X POST https://api.usagetap.com/call_begin \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customerId": "cust_123",
"feature": "chat.completions",
"requested": {
"standard": true,
"premium": true,
"search": true,
"reasoningLevel": "HIGH"
},
"tags": ["production", "web-app"]
}'
POST /call_end
Report actual usage for a tracked call.
Endpoint: POST {baseUrl}/call_end
Request Body:
{
callId: string; // Required: From call_begin response
modelUsed?: string; // Model identifier (e.g., "gpt-4o")
inputTokens?: number; // Prompt tokens
responseTokens?: number; // Completion tokens
cachedTokens?: number; // Cached/reused tokens
reasoningTokens?: number; // Reasoning tokens (o1 models)
searches?: number; // Number of web searches
audio?: number; // Audio processing units
audioSeconds?: number; // Audio duration in seconds
isPremium?: boolean; // Override premium detection
error?: { // Report error if call failed
code: string;
message: string;
};
stripeCustomerId?: string; // Override Stripe customer ID
}
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "CALL_END_SUCCESS",
timestamp: "2025-11-05T12:00:05.000Z"
},
data: {
callId: string;
costUSD: number; // Calculated cost in USD
metered: { // Usage that was counted
calls?: number;
tokens?: number;
reasoningTokens?: number;
searches?: number;
audio?: number;
audioSeconds?: number;
};
balances?: { // Remaining quotas
tokensRemaining?: number;
searchesRemaining?: number;
audioSecondsRemaining?: number;
};
stripeCustomerId?: string;
},
correlationId: string
}
Premium Detection:
UsageTap automatically classifies calls as premium based on model pricing:
- Premium: Output token price > $4.00 per million tokens
- Standard: Output token price ≤ $4.00 per million tokens
Override with isPremium: true or isPremium: false for custom models.
Example:
curl -X POST https://api.usagetap.com/call_end \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "x-usage-correlation-id: corr_abc123" \
-d '{
"callId": "call_xyz789",
"modelUsed": "gpt-4o",
"inputTokens": 512,
"responseTokens": 256,
"reasoningTokens": 0,
"searches": 1
}'
POST /call
Unified endpoint that combines begin, vendor call, and end in one request.
Endpoint: POST {baseUrl}/call
Request Body:
{
// Standard call_begin fields
customerId: string;
feature?: string;
requested?: { /* entitlements */ };
idempotencyKey?: string;
holdUsd?: number; // Optional: PAYG hold hint (USD) for this call
tags?: string[];
// Optional vendor call configuration
vendor?: {
url: string; // Vendor API endpoint
method: "POST" | "GET"; // HTTP method
headers: { // Headers to send
[key: string]: string;
};
body?: object; // Request body
timeoutMs?: number; // Request timeout (default: 30000)
responseType?: "json" | "text" | "stream";
};
// Usage overrides (if vendor block is omitted)
usage?: {
modelUsed?: string;
inputTokens?: number;
responseTokens?: number;
// ... other usage fields
};
// Optional error details
end?: {
error?: {
code: string;
message: string;
};
};
}
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "CALL_SUCCESS" | "CALL_VENDOR_WARNING",
timestamp: string
},
data: {
begin: { // call_begin response
callId: string;
allowed: { /* ... */ };
// ... other begin fields
};
end: { // call_end response
metered: { /* ... */ };
balances: { /* ... */ };
};
vendor?: { // Vendor response (if vendor block provided)
ok: boolean;
status: number;
modelUsed?: string;
error?: {
message: string;
details?: object;
};
};
endUsage: { // Final reported usage
tokens: number;
modelUsed: string;
// ... other usage fields
};
},
correlationId: string
}
Behavior:
- If
vendoris provided, UsageTap makes the HTTP call and extracts usage - If
vendoris omitted, onlyusageoverrides are recorded - Non-2xx vendor responses still trigger
call_endwith error metadata
Example:
curl -X POST https://api.usagetap.com/call \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-d '{
"customerId": "cust_123",
"feature": "chat.completions",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"requested": {
"standard": true,
"premium": true
},
"vendor": {
"url": "https://api.openai.com/v1/chat/completions",
"method": "POST",
"headers": {
"Authorization": "Bearer sk-...",
"Content-Type": "application/json"
},
"body": {
"model": "gpt-4o-mini",
"messages": [
{ "role": "user", "content": "Hello" }
]
},
"responseType": "json"
},
"usage": {
"modelUsed": "gpt-4o-mini"
}
}'
POST /customers
Create or retrieve a customer subscription idempotently.
Endpoint: POST {baseUrl}/customers
Request Body:
{
customerId: string; // Required: Your customer identifier
customerFriendlyName?: string; // HIGHLY IMPORTANT BUT OPTIONAL: Display name (preferred field; alias: customerName)
customerName?: string; // HIGHLY IMPORTANT BUT OPTIONAL: Alias for customerFriendlyName
customerEmail?: string; // HIGHLY IMPORTANT BUT OPTIONAL: Email for notifications
stripeCustomerId?: string; // Link to Stripe customer
}
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "CUSTOMER_READY",
timestamp: string
},
data: {
customerId: string;
newCustomer: boolean; // True if just created, false if existing
canceled: boolean;
policy: "NONE" | "BLOCK" | "DOWNGRADE";
allowed: { /* ... */ }; // Current entitlements
entitlementHints: { /* ... */ };
meters: { /* ... */ }; // Current usage
remainingRatios: { /* ... */ };
subscription: { /* ... */ }; // Full subscription details
models?: { /* ... */ };
plan?: { /* ... */ };
balances?: { /* ... */ };
stripeCustomerId?: string;
},
correlationId: string
}
Idempotent: Calling multiple times returns the same customer. Only newCustomer: true on first call.
Example:
curl -X POST https://api.usagetap.com/customers \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customerId": "cust_123",
"customerFriendlyName": "Acme Corp",
"customerEmail": "billing@acme.com",
"stripeCustomerId": "cus_stripe123"
}'
GET /customers/{customerId}/usage
Check current usage status without creating a call.
Endpoint: GET {baseUrl}/customers/{customerId}/usage
Path Parameters:
customerId(string, required): Customer identifier
Response (200 OK):
Same structure as POST /customers but without newCustomer field.
Example:
curl -X GET https://api.usagetap.com/customers/cust_123/usage \
-H "Authorization: Bearer sk_test_..." \
-H "Accept: application/vnd.usagetap.v1+json"
POST /customers/{customerId}/change_plan
Change customer's usage plan.
Endpoint: POST {baseUrl}/customers/{customerId}/change_plan
Path Parameters:
customerId(string, required): Customer identifier
Request Body:
{
planId: string; // Required: Target plan ID
strategy?: "IMMEDIATE_RESET" | "IMMEDIATE_PRORATED" | "AT_NEXT_REPLENISH";
}
Strategy Options:
IMMEDIATE_RESET(default): Switch immediately, reset all usage to zeroIMMEDIATE_PRORATED: Switch immediately, prorate existing usage to new limitsAT_NEXT_REPLENISH: Schedule change for next replenishment cycle
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "PLAN_CHANGED",
timestamp: string
},
data: {
success: boolean;
subscription: { // Updated subscription
id: string;
usagePlanVersionId: string;
planName: string;
planVersion: string;
// ... full subscription details
pending?: { // If strategy = AT_NEXT_REPLENISH
usagePlanVersionId: string;
strategy: string;
effectiveAt: string;
};
};
},
correlationId: string
}
Example:
curl -X POST https://api.usagetap.com/customers/cust_123/change_plan \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"planId": "plan_premium_v2",
"strategy": "IMMEDIATE_RESET"
}'
POST /custom_meter
Update (increment) a custom meter for a customer by recording a new usage event. Custom meters allow you to track any usage metric beyond the standard LLM usage counters.
Endpoint: POST {baseUrl}/custom_meter
Headers:
Accept: application/vnd.usagetap.v1+jsonContent-Type: application/jsonIdempotency-Key(recommended) or rely on server-side derivation
Request Body:
{
customerId: string; // Required: Customer identifier
meterSlot: "CUSTOM1" | "CUSTOM2"; // Required: Which custom meter to increment
amount: number; // Required: Positive number to decrement from quota
feature?: string; // Optional: Feature identifier for tracking
tags?: string[]; // Optional: Tags for categorization
metadata?: object; // Optional: Additional metadata
}
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "CUSTOM_METER_SUCCESS",
timestamp: string
},
data: {
success: boolean; // true
eventId: string; // Unique event identifier
meterSlot: "CUSTOM1" | "CUSTOM2";
amount: number; // Amount that was recorded
meter: { // Updated meter snapshot
remaining: number | null;
limit: number | null;
used: number | null;
unlimited: boolean;
label: string;
};
blocked: boolean; // true if quota exceeded and policy is BLOCK
},
correlationId: string
}
Error Responses:
400 BAD_REQUEST: Invalid meterSlot, missing customerId, or invalid amount403 QUOTA_EXCEEDED: Insufficient quota (when limitType is BLOCK)404 SUBSCRIPTION_NOT_FOUND: No active subscription found400 METER_NOT_ENABLED: Custom meter not enabled on customer's plan
Example:
curl -X POST https://api.usagetap.com/custom_meter \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customerId": "cust_123",
"meterSlot": "CUSTOM1",
"amount": 5,
"feature": "agent_actions",
"tags": ["workflow_automation"],
"metadata": {
"workflowId": "wf_abc123",
"actionType": "email_send"
}
}'
Use Cases:
Custom meters are ideal for tracking:
- Agent actions or tool invocations
- Document processing units
- API calls to non-LLM services
- Custom workflow steps
- Any usage that doesn't fit standard LLM metrics
Important Notes:
- Custom meters must be enabled in the customer's usage plan
- The
amountdecrements theremainingquota (similar to how tokens work) - With
BLOCKpolicy, requests exceeding quota return403 QUOTA_EXCEEDED - With
DOWNGRADEpolicy, usage continues but quota can go negative - Unlimited meters don't track usage but still record events
SDK API
The TypeScript/JavaScript SDK provides a high-level interface to the REST API.
Installation
npm install @usagetap/sdk openai
Initialization
import { UsageTapClient } from "@usagetap/sdk";
const client = new UsageTapClient({
apiKey: process.env.USAGETAP_API_KEY!,
baseUrl: process.env.USAGETAP_BASE_URL!,
// Optional configuration
defaultFeature?: string; // Default feature name
defaultTags?: string[]; // Default tags
fetchImpl?: typeof fetch; // Custom fetch implementation
headers?: Record<string, string>; // Additional headers
retries?: {
maxAttempts?: number; // Default: 3
baseDelayMs?: number; // Default: 250
maxDelayMs?: number; // Default: 5000
jitterRatio?: number; // Default: 0.2
};
idempotencyGenerator?: () => string; // Custom ID generator
autoIdempotency?: boolean; // Default: true
onLog?: (entry) => void; // Logging callback
useApiKeyHeader?: boolean; // Use x-api-key header
allowBrowser?: boolean; // Allow browser usage (testing only)
});
Core Methods
beginCall(request, options?)
const response = await client.beginCall({
customerId: "cust_123",
feature: "chat.send",
idempotencyKey: crypto.randomUUID(),
requested: {
standard: true,
premium: true,
search: true,
reasoningLevel: "HIGH",
},
tags: ["production"],
});
console.log("Call ID:", response.data.callId);
console.log("Allowed:", response.data.allowed);
endCall(request, options?)
const response = await client.endCall({
callId: "call_xyz789",
modelUsed: "gpt-4o",
inputTokens: 512,
responseTokens: 256,
reasoningTokens: 0,
searches: 1,
});
console.log("Cost:", response.data.costUSD);
console.log("Metered:", response.data.metered);
withUsage(request, handler, options?)
const result = await client.withUsage(
{
customerId: "cust_123",
feature: "chat.send",
idempotencyKey: crypto.randomUUID(),
requested: { standard: true, premium: true },
},
async ({ begin, setUsage, setError }) => {
try {
const model = begin.data.allowed.premium ? "gpt-4o" : "gpt-4o-mini";
const response = await openai.chat.completions.create({
model,
messages: [{ role: "user", content: "Hello" }],
});
setUsage({
modelUsed: model,
inputTokens: response.usage?.prompt_tokens ?? 0,
responseTokens: response.usage?.completion_tokens ?? 0,
});
return response.choices[0].message.content;
} catch (error) {
setError({
code: "VENDOR_ERROR",
message: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
);
createCustomer(request, options?)
const response = await client.createCustomer({
customerId: "cust_123",
customerFriendlyName: "Acme Corp",
customerEmail: "billing@acme.com",
stripeCustomerId: "cus_stripe123",
});
console.log("New customer?", response.data.newCustomer);
console.log("Plan:", response.data.subscription.planName);
checkUsage(request, options?)
const response = await client.checkUsage({
customerId: "cust_123",
});
console.log("Meters:", response.data.meters);
console.log("Allowed:", response.data.allowed);
changePlan(request, options?)
const response = await client.changePlan({
customerId: "cust_123",
planId: "plan_premium_v2",
strategy: "IMMEDIATE_RESET",
});
console.log("Success:", response.data.success);
console.log("New plan:", response.data.subscription.planName);
incrementCustomMeter(request, options?)
Track custom usage metrics beyond standard LLM counters.
const response = await client.incrementCustomMeter({
customerId: "cust_123",
meterSlot: "CUSTOM1",
amount: 5,
feature: "agent_actions",
tags: ["workflow_automation"],
metadata: {
workflowId: "wf_abc123",
actionType: "email_send",
},
});
console.log("Event ID:", response.data.eventId);
console.log("Remaining:", response.data.meter.remaining);
console.log("Blocked:", response.data.blocked);
Parameters:
customerId(string, required): Customer identifiermeterSlot("CUSTOM1" | "CUSTOM2", required): Which custom meter to incrementamount(number, required): Positive number to decrement from quotafeature(string, optional): Feature identifier for trackingtags(string[], optional): Tags for categorizationmetadata(object, optional): Additional metadata
Returns: Updated meter snapshot with remaining, limit, used, unlimited, and label fields.
Throws:
UsageTapErrorwith codeUSAGETAP_BAD_REQUESTfor invalid parametersUsageTapErrorwith codeUSAGETAP_AUTH_ERRORfor quota exceeded (BLOCK policy)
Use Cases:
// Track agent tool invocations
await client.incrementCustomMeter({
customerId: "cust_123",
meterSlot: "CUSTOM1",
amount: 1,
feature: "agent.tool_call",
tags: ["web_search"],
});
// Track document processing
await client.incrementCustomMeter({
customerId: "cust_456",
meterSlot: "CUSTOM2",
amount: 10, // 10 pages processed
feature: "document.ocr",
metadata: { documentId: "doc_789", pages: 10 },
});
// Track custom API calls
await client.incrementCustomMeter({
customerId: "cust_789",
meterSlot: "CUSTOM1",
amount: 1,
feature: "external_api.maps",
tags: ["geocoding"],
});
OpenAI Integration
wrapOpenAI(client, usageTap, options)
import OpenAI from "openai";
import { wrapOpenAI } from "@usagetap/sdk/openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const ai = wrapOpenAI(openai, client, {
defaultContext: {
customerId: "cust_123",
feature: "chat.completions",
requested: {
standard: true,
premium: true,
search: true,
reasoningLevel: "HIGH",
},
},
});
// Now use `ai` instead of `openai`
const completion = await ai.chat.completions.create(
{
messages: [{ role: "user", content: "Hello" }],
// model is optional - selected based on entitlements
},
{
usageTap: {
idempotencyKey: crypto.randomUUID(),
},
}
);
Streaming Support
const stream = await ai.chat.completions.create(
{
messages: [{ role: "user", content: "Hello" }],
stream: true,
},
{
usageTap: {
customerId: "cust_123",
idempotencyKey: crypto.randomUUID(),
},
}
);
// Next.js
import { toNextResponse } from "@usagetap/sdk/openai";
return toNextResponse(stream, { mode: "text" });
// Express
import { pipeToResponse } from "@usagetap/sdk/openai";
pipeToResponse(stream, res);
Express Middleware
import { withUsage } from "@usagetap/sdk/express";
app.use(
withUsage(client, (req) => req.user?.id || "anonymous")
);
app.post("/api/chat", async (req, res) => {
const ai = req.usageTap!.openai(openai, {
feature: "chat.assistant",
requested: { standard: true, premium: true },
});
const stream = await ai.chat.completions.create({
messages: req.body.messages,
stream: true,
});
req.usageTap!.pipeToResponse(stream, res);
});
React Hook
import { useChatWithUsage } from "@usagetap/sdk/react";
function Chat({ userId }: { userId: string }) {
const { messages, input, setInput, handleSubmit, isLoading } =
useChatWithUsage({
api: "/api/chat",
customerId: userId,
feature: "chat.assistant",
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>{m.content}</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}
Type Definitions
ReasoningLevel
type ReasoningLevel = "NONE" | "LOW" | "MEDIUM" | "HIGH";
LimitType
type LimitType = "NONE" | "BLOCK" | "DOWNGRADE";
RequestedEntitlements
interface RequestedEntitlements {
standard?: boolean;
premium?: boolean;
audio?: boolean;
image?: boolean;
search?: boolean;
reasoningLevel?: ReasoningLevel;
}
AllowedEntitlements
type AllowedEntitlements = Required<RequestedEntitlements>;
MeterSummary
interface MeterSummary {
remaining: number | null; // Null if unlimited
limit: number | null; // Null if unlimited
used: number;
unlimited: boolean;
ratio: number | null; // remaining/limit (0-1), null if unlimited
}
SubscriptionSnapshot
interface SubscriptionSnapshot {
id: string;
usagePlanVersionId: string;
planName: string;
planVersion: string;
limitType: LimitType;
reasoningLevel: ReasoningLevel;
lastReplenishedAt: string; // ISO 8601
nextReplenishAt: string; // ISO 8601
subscriptionVersion: number;
customerFriendlyName?: string;
customerEmail?: string;
stripeCustomerId?: string;
pending?: {
usagePlanVersionId: string;
strategy: string;
effectiveAt: string;
};
}
EntitlementHints
interface EntitlementHints {
suggestedModelTier: "premium" | "standard" | "none";
reasoningLevel: ReasoningLevel;
policy: LimitType;
downgrade?: {
reason: string;
fallbackTier?: "premium" | "standard" | "none";
};
}
IdempotencyMetadata
interface IdempotencyMetadata {
key: string; // Always matches callId
source: "explicit" | "derived";
}
Error Codes
SDK Error Codes
| Code | Description | Retryable |
|---|---|---|
USAGETAP_AUTH_ERROR |
Invalid API key or unauthorized | No |
USAGETAP_BAD_REQUEST |
Invalid request parameters | No |
USAGETAP_RATE_LIMITED |
Too many requests | Yes |
USAGETAP_SERVER_ERROR |
Server-side error | Yes |
USAGETAP_NETWORK_ERROR |
Network failure | Yes |
USAGETAP_INVALID_RESPONSE |
Unexpected response format | No |
USAGETAP_END_CALL_ERROR |
Failed to finalize call | No |
USAGETAP_BROWSER_RUNTIME |
SDK used in browser without allowBrowser | No |
API Result Codes
| Code | Status | Description |
|---|---|---|
CALL_BEGIN_SUCCESS |
ACCEPTED |
Call started successfully |
CALL_END_SUCCESS |
ACCEPTED |
Call ended successfully |
CALL_SUCCESS |
ACCEPTED |
Unified /call succeeded |
CALL_VENDOR_WARNING |
ACCEPTED |
/call succeeded but vendor returned error |
CUSTOMER_READY |
ACCEPTED |
Customer subscription ready |
PLAN_CHANGED |
ACCEPTED |
Plan change successful |
HTTP Status Codes
| Status | Meaning | Retryable |
|---|---|---|
200 |
Success | N/A |
400 |
Bad Request (invalid parameters) | No |
401 |
Unauthorized (invalid API key) | No |
403 |
Forbidden | No |
404 |
Not Found | No |
406 |
Not Acceptable (missing Accept header) | No |
409 |
Conflict (idempotency key mismatch) | No |
429 |
Rate Limited | Yes |
500 |
Internal Server Error | Yes |
502 |
Bad Gateway | Yes |
503 |
Service Unavailable | Yes |
504 |
Gateway Timeout | Yes |
Examples
Complete Node.js Script
import OpenAI from "openai";
import { UsageTapClient } from "@usagetap/sdk";
import crypto from "crypto";
const usageTap = new UsageTapClient({
apiKey: process.env.USAGETAP_API_KEY!,
baseUrl: process.env.USAGETAP_BASE_URL!,
});
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
});
async function chat(userId: string, message: string) {
return usageTap.withUsage(
{
customerId: userId,
feature: "chat.send",
idempotencyKey: crypto.randomUUID(),
requested: {
standard: true,
premium: true,
search: true,
reasoningLevel: "MEDIUM",
},
},
async ({ begin, setUsage }) => {
// Select model based on entitlements
const model = begin.data.allowed.premium ? "gpt-4o" : "gpt-4o-mini";
// Add search tool if allowed
const tools = begin.data.allowed.search
? [{ type: "web_search" as const }]
: undefined;
const response = await openai.chat.completions.create({
model,
messages: [{ role: "user", content: message }],
tools,
});
setUsage({
modelUsed: model,
inputTokens: response.usage?.prompt_tokens ?? 0,
responseTokens: response.usage?.completion_tokens ?? 0,
searches: tools ? response.usage?.web_search_queries ?? 0 : 0,
});
return response.choices[0].message.content;
}
);
}
chat("user_123", "What's new in AI today?")
.then((response) => console.log("Response:", response))
.catch((error) => console.error("Error:", error));
Raw HTTP Request
const response = await fetch("https://api.usagetap.com/call", {
method: "POST",
headers: {
"Authorization": "Bearer sk_test_...",
"Content-Type": "application/json",
"Accept": "application/vnd.usagetap.v1+json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
customerId: "cust_123",
feature: "chat.completions",
requested: {
standard: true,
premium: true,
},
vendor: {
url: "https://api.openai.com/v1/chat/completions",
method: "POST",
headers: {
"Authorization": "Bearer sk-...",
"Content-Type": "application/json",
},
body: {
model: "gpt-4o-mini",
messages: [
{ role: "user", content: "Hello" }
],
},
responseType: "json",
},
usage: {
modelUsed: "gpt-4o-mini",
},
}),
});
const result = await response.json();
console.log("Result:", result.data);
Support
- Documentation: https://usagetap.com/docs
- API Status: https://status.usagetap.com
- GitHub: https://github.com/usagetap/sdk
- Email: support@usagetap.com
Last Updated: November 5, 2025 API Version: v1 SDK Version: 0.7.1