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.
New server credentials use the utk- prefix and carry one or more OAuth-style
scopes. The prefix identifies the credential format; scopes determine what the
credential may do.
| Scope | Access |
|---|---|
usage:read |
Customer usage, organization summaries, and anomalies |
usage:write |
Call lifecycle and custom meter events |
customers:read |
Customer and usage-plan listings |
customers:write |
Customer provisioning, plan changes, and replenishment |
gateway:invoke |
UsageTap Gateway requests |
compression:invoke |
Compression, sampling, and recipe APIs |
embeds:read |
Server-created embed sessions; must use a dedicated key |
Scopes can be combined on one utk- key except embeds:read, which is kept
separate because embed credentials may be exposed to a browser. Existing
ck-, ak-, cmp-, gk-, and ek- keys remain valid with their historical
permissions.
Permissions on an active server key can be edited without rotating the secret.
The key ID, value, and prefix stay the same, and the new permissions take
effect immediately. Editing a legacy server key migrates it to explicit stored
scopes. Dedicated ek- browser keys cannot be converted to or from server
permissions; create a separate key for that trust boundary.
Keys are server credentials. Do not place them in browser code.
Required scopes by operation
Scopes are independent and do not imply one another. Give a key every scope needed by its workflow.
| Operations | Required scope |
|---|---|
POST /call, /call_begin, /call_end, /custom_meter |
usage:write |
GET /calls/{callId}, /customers/{customerId}/usage, /usage/summary, /usage/anomalies |
usage:read |
GET /customers, /usage-plans |
customers:read |
POST /customers, /customers/{customerId}/change_plan, /customers/{customerId}/replenish |
customers:write |
GET /sampling/settings, POST /sampling/decide, /samples, /compress_prompt |
compression:invoke |
/v1/compression/authorize and published Compression profile, summary, and job routes |
compression:invoke |
Gateway /v1/* routes |
gateway:invoke |
POST /v1/client/sessions |
gateway:invoke and/or compression:invoke; returns a five-minute client session with the parent key's client-safe capabilities |
POST /v1/embed-sessions |
embeds:read on a dedicated ek- key |
Common combinations:
- Direct metering plus Compression:
usage:write,compression:invoke - Gateway with hosted Compression:
gateway:invoke,compression:invoke - Customer administration with usage reporting:
customers:read,customers:write,usage:read
Widget-data requests use the short-lived embed session token created by
POST /v1/embed-sessions; they do not receive the long-lived API key.
Client applications use the short-lived session token created by
POST /v1/client/sessions. The customer's authenticated backend calls this
endpoint with its server-only key and server-derived customer_id and optional
customer_user_id. The token inherits only client-safe capabilities:
gateway:invoke becomes gateway:chat, and compression:invoke becomes
compression:run. A combined parent receives both. The browser keeps the
returned token in memory and renews it by calling that backend again before the
five-minute expiry. Gateway access is restricted to POST /v1/chat/completions and POST /v1/responses; it cannot list models or use
batch routes. The earlier /v1/gateway/sessions path remains an alias.
Option 1: Bearer Token (Recommended)
Authorization: Bearer YOUR_USAGETAP_API_KEY
Option 2: API Key Header
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/1.4.0 |
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: object, // Error-specific machine-readable fields
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
- Call type
- Pricing mode
The resolved key is returned in data.idempotency.key.
This fallback is for compatibility, not normal call creation. Identical inputs can replay an earlier call. Direct integrations should send a unique key for each new logical call and reuse that key only when retrying that call. The JavaScript SDK generates a unique key by default.
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
customerUserId?: string; // Optional: Stable end-user identifier
customerUserName?: string; // Optional: End-user display name
customerUserEmail?: string; // Optional: End-user display email
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";
};
idempotencyKey?: string; // Direct callers should send one per logical call
tags?: string[]; // Optional: Tags for analytics
customerName?: string; // Optional display name for first provisioning
customerEmail?: string; // Optional email for first provisioning
stripeCustomerId?: string; // Optional: Link to Stripe customer
batch?: boolean; // Optional: Whether this call uses batch pricing (default: false)
pricingMode?: "batch" | "standard"; // Optional: Explicit pricing mode (default: "standard")
}
call_begin reports entitlements, including the allowed reasoning ceiling.
Execution identity belongs to call_end. Do not infer reasoningEffort from
reasoningTokens; omit unknown values and include provenance whenever effort is
known. The UsageTap gateway treats effort as a normalized cross-provider ladder.
When a selected Anthropic or Google model does not support the requested value,
the gateway maps downward to the nearest supported level (with minimal mapping
to the lowest non-zero level) and reports that effective value at call_end.
For models that support disabling reasoning, none is preserved; otherwise it
maps to the lowest supported level.
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "CALL_BEGIN_SUCCESS",
timestamp: "2025-11-05T12:00:00.000Z"
},
data: {
callId: string; // Use in call_end
callType?: "standard" | "API";
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
};
};
rateLimits?: { // Optional rolling-call limit snapshots
rollingCalls: object | { [scope: string]: object };
};
// Current usage levels per meter
meters: {
[meterName: string]: {
remaining: number; // Always numeric; check `unlimited` flag instead of null
limit: number | null;
used: number;
unlimited: boolean; // true = unbounded; remaining is informational
ratio: number | null; // remaining/limit (0-1), null when unlimited
};
};
// 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";
};
// Batch pricing
batch: boolean; // Whether batch pricing is applied
pricingMode: "batch" | "standard"; // Pricing mode for this call
// 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 ck-..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customerId": "cust_123",
"customerUserId": "user_123",
"feature": "chat.completions",
"requested": {
"standard": true,
"premium": true,
"search": true,
"reasoningLevel": "HIGH"
},
"tags": ["production", "web-app"]
}'
call_begin returns an accepted HTTP 200 envelope even when the requested
entitlement is denied. Inspect data.allowed before invoking the provider. A
denied call is stored with status NOT_ALLOWED and is already finalized; do
not invoke the provider or call /call_end for that denial. Under
DOWNGRADE, select only an application-approved fallback model.
GET /sampling/settings
Load the organization's canonical sampling policy. This is the best option for high-throughput, non-SDK integrations that want to cache the policy and evaluate it locally.
For an end-to-end Gateway, SDK, and direct-API guide that finishes with a Production Sample model benchmark, see Benchmark Models on Production Tasks.
Endpoint: GET {baseUrl}/sampling/settings
The response includes rate, minInputTokens, feature/customer filters, retentionDays, a content-derived version, and cache metadata. Honor the Cache-Control header (currently five minutes); the response also includes an ETag containing the policy version.
curl https://api.usagetap.com/sampling/settings \
-H "Authorization: Bearer cmp-..." \
-H "Accept: application/vnd.usagetap.v1+json"
{
"data": {
"version": "a1b2c3d4e5f60718",
"rate": 0.1,
"minInputTokens": 2000,
"features": { "include": [], "exclude": [] },
"customers": { "exclude": [] },
"retentionDays": 7,
"cacheSeconds": 300,
"settingsExpiresAt": "2026-07-23T00:05:00.000Z"
}
}
customerId identifies the customer account. End-user fields are optional.
Prefer a stable, non-PII customerUserId; name and email are display metadata
and may be omitted. CamelCase, snake_case, and the legacy nested customer
object are accepted.
Authentication and permission failures are intentionally distinct:
401 UNAUTHORIZED: the credential is missing, malformed, unknown, revoked, or otherwise invalid. The response does not claim that adding a scope will repair an invalid credential.403 API_KEY_SCOPE_REQUIRED: the credential is valid but lacks the required permission.error.requiredScopenames the exact scope to add.
{
"result": {
"status": "ERROR",
"code": "API_KEY_SCOPE_REQUIRED",
"message": "This operation requires the \"usage:read\" API key scope.",
"timestamp": "2026-08-04T12:00:00.000Z"
},
"error": {
"reason": "API_KEY_SCOPE_REQUIRED",
"requiredScope": "usage:read"
},
"correlationId": "corr_abc123"
}
Gateway errors use the OpenAI-compatible error envelope and expose the same
machine-readable values as error.code and error.requiredScope.
Evaluate filters before the rate: customer exclusion, feature exclusion, feature inclusion, minimum input tokens, then the sampling rate. Only selected calls should be sent to /samples.
POST /sampling/decide
Ask UsageTap for a centralized sampling decision without sending prompt or response content. This is the simplest direct-API integration and can run in parallel with the model request.
Endpoint: POST {baseUrl}/sampling/decide
{
samplingKey?: string; // Stable provider/request ID; generated if omitted
customerId?: string;
feature?: string;
inputTokens?: number; // Preferred when known
inputCharacters?: number; // Accepted instead; approximated at 4 chars/token
}
Supplying a stable samplingKey makes retries return the same decision for the same policy version. The response contains sample, a machine-readable reason, decisionId, samplingKey, policyVersion, and settingsExpiresAt.
curl -X POST https://api.usagetap.com/sampling/decide \
-H "Authorization: Bearer cmp-..." \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "Content-Type: application/json" \
-d '{"samplingKey":"req_123","customerId":"cust_123","feature":"assistant.answer","inputTokens":4200}'
POST /samples
Store a call selected by the SDK, a local policy evaluation, or /sampling/decide. The sample automatically expires according to the retention period configured on the Sampling page.
Endpoint: POST {baseUrl}/samples
{
sampleId?: string; // Stable ID recommended for safe retries
decisionId?: string; // From /sampling/decide
policyVersion?: string; // From settings or decision response
customerId?: string;
feature?: string;
environment?: string;
tags?: string[];
provider: string;
model?: string;
input: unknown; // Required; retained raw call data
output?: unknown;
usage?: unknown;
latencyMs?: number;
error?: unknown;
}
The maximum request body is 300 KiB. A successful response includes sampleId, receivedAt, expiresAt, retentionDays, and policyVersion. Because this endpoint stores raw application data, call it only after a positive sampling decision.
POST /compress_prompt
Record prompt compression savings for an existing call. This endpoint is optional and should only be used after call_begin when your application has already compressed the prompt locally. Do not send raw prompt content, compressed prompt content, messages, tool schemas, or other user data to this endpoint.
Endpoint: POST {baseUrl}/compress_prompt
Request Body:
{
callId: string; // Required: From call_begin response
promptCompression: {
provider: "heuristic" | "toon" | "thetokencompany" | "usagetap";
originalTokens: number;
compressedTokens: number;
savedTokens?: number; // Re-derived by UsageTap
tokenSavingsRatio?: number; // Re-derived by UsageTap
techniques: string[]; // e.g. ["json-minify", "embedded-json-toon"]
};
}
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "PROMPT_COMPRESSION_RECORDED",
timestamp: string
},
data: {
callId: string;
updated: true;
provider: "heuristic" | "toon" | "thetokencompany" | "usagetap";
originalTokens: number;
compressedTokens: number;
savedTokens: number;
tokenSavingsRatio: number;
techniques: string[];
},
correlationId: string
}
Behavior:
- Compression is opt-in.
call_beginbehaves the same whether you use this endpoint or not. - UsageTap stores only counts, ratios, provider, techniques, and timestamp. Raw prompt content is never stored by this endpoint.
- Token savings use lightweight regex estimation, not a provider-specific BPE tokenizer.
- The JavaScript SDK's
promptCompress()handles local compression and calls this endpoint for you. If compression or telemetry fails, the SDK returns the original prompt so the vendor call can continue.
Example:
curl -X POST https://api.usagetap.com/compress_prompt \
-H "Authorization: Bearer cmp-..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "x-usage-correlation-id: corr_abc123" \
-d '{
"callId": "call_xyz789",
"promptCompression": {
"provider": "heuristic",
"originalTokens": 260,
"compressedTokens": 180,
"techniques": ["json-minify", "embedded-json-toon"]
}
}'
POST /call_end
Report actual usage for a tracked call.
Endpoint: POST {baseUrl}/call_end
Request Body:
{
callId: string; // Required: From call_begin response
providerUsed?: string; // Executing vendor, e.g. "openai"
modelUsed?: string; // Model identifier (e.g., "gpt-5.6-luna")
reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
reasoningEffortSource?: "provider_response" | "provider_request" | "gateway_config" | "model_default";
reasoningMode?: string; // Optional provider-specific thinking mode
reasoningBudgetTokens?: number; // Optional explicit thinking budget
inputTokens?: number; // Total prompt tokens, including cache reads/writes
responseTokens?: number; // Completion tokens
cachedInputTokens?: number; // Subset served from prompt cache
cacheWriteInputTokens?: number; // Subset written to prompt cache
reasoningTokens?: number; // Reasoning tokens (o1 models)
searches?: number; // Number of web searches
audio?: number; // Audio processing units
audioSeconds?: number; // Audio duration in seconds
promptCompression?: { // Optional before/after savings metadata
provider: "heuristic" | "toon" | "thetokencompany" | "usagetap";
originalTokens: number;
compressedTokens: number;
savedTokens?: number; // Re-derived by UsageTap for consistency
tokenSavingsRatio?: number; // Re-derived by UsageTap for consistency
techniques?: string[];
};
isPremium?: boolean; // Override premium detection
error?: { // Report error if call failed
code: string;
message: string;
};
stripeCustomerId?: string; // Override Stripe customer ID
batch?: boolean; // Optional: Whether this call uses batch pricing (default: false)
pricingMode?: "batch" | "standard"; // Optional: Explicit pricing mode (default: "standard")
}
Response (200 OK):
{
result: {
status: "ACCEPTED",
code: "CALL_END_SUCCESS",
timestamp: "2025-11-05T12:00:05.000Z"
},
data: {
callId: string;
status: "COMPLETED" | "FAILED";
error?: {
code: string;
message: string;
};
providerUsed?: string;
modelUsed?: string;
reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
reasoningEffortSource?: "provider_response" | "provider_request" | "gateway_config" | "model_default";
reasoningMode?: string;
reasoningBudgetTokens?: number;
costUSD: number; // Final cost after pricing mode
costUsdNano?: string; // High-precision decimal integer representation
costBreakdown?: object; // Provider/model pricing components
standardCostUSD: number; // Cost before batch reduction
pricingMultiplier: 0.5 | 1; // Applied pricing multiplier
batch: boolean; // Whether batch pricing was applied
pricingMode: "batch" | "standard"; // Pricing mode used
usage: {
inputTokens: number;
cachedInputTokens: number;
cacheWriteInputTokens: number;
billableInputTokens: number;
responseTokens: number;
reasoningTokens: number;
};
metered: { // Usage that was counted
calls?: number;
tokens?: number;
reasoningTokens?: number;
searches?: number;
audio?: number;
audioSeconds?: number;
};
spendVelocity?: { // Aggregate-backed customer spend telemetry
currency: "USD";
source: "usage_aggregate";
generatedAt: string;
customerId: string;
currentCallCostUsd: number;
windows: {
hour: {
bucket: string; // UTC bucket, e.g. "2026-05-01T17"
windowMinutes: 60;
startedAt: string;
endedAt: string;
completedCostUsd: number;
completedCalls: number;
};
day: {
bucket: string; // UTC bucket, e.g. "2026-05-01"
windowMinutes: 1440;
startedAt: string;
endedAt: string;
completedCostUsd: number;
completedCalls: number;
};
};
};
balances?: { // Remaining quotas
tokensRemaining?: number;
searchesRemaining?: number;
audioSecondsRemaining?: number;
};
stripeCustomerId?: string;
},
correlationId: string
}
When error is supplied with non-empty code and message, UsageTap accepts
the finalization, stores the call as FAILED, and returns the normalized error
metadata in data.error. Invalid error objects return 400 BAD_REQUEST.
Sanitize the message; do not send prompts, credentials, or raw provider bodies.
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.
Batch Pricing:
Batch mode applies a 50% discount to standard pricing rates. Prefer
pricingMode: "batch"; batch: true is retained as a compatibility flag.
When both are supplied, pricingMode is authoritative. A mode set on
call_begin carries through to call_end; either field on call_end can
override the stored mode.
UsageTap accepts the execution mode asserted by the authenticated application. Provider response usage supplies token quantities but is not treated as proof of batch execution. OpenAI and Anthropic per-response usage objects do not expose a dependable batch-priced signal; applications using their native asynchronous batch APIs must retain that workflow context and report it.
For auditability, call_end returns and persists:
standardCostUSD: cost before the batch rate.pricingMultiplier:0.5for batch or1for standard.costUSD: final cost after the multiplier.
PAYG markup and credit settlement use costUSD, so the batch rate is applied
before markup and credit deduction. Token and call allowances still decrement
using actual usage.
When promptCompression is present, UsageTap stores the before/after token
counts, derives saved tokens and percentage decrease, and estimates prompt
input cost avoided from the resolved model pricing. Raw prompt content is
never included in this telemetry.
Example:
curl -X POST https://api.usagetap.com/call_end \
-H "Authorization: Bearer ck-..." \
-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-5.6-sol",
"inputTokens": 512,
"responseTokens": 256,
"reasoningTokens": 0,
"searches": 1,
"batch": true,
"pricingMode": "batch"
}'
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;
customerUserId?: string;
customerUserName?: string;
customerUserEmail?: string;
feature?: string;
requested?: { /* entitlements */ };
idempotencyKey?: string;
tags?: string[];
// Optional vendor call configuration
vendor?: {
url: string; // Vendor API endpoint
provider?: string; // Vendor label recorded at call_end
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
costUSD: number; // Final provider/model cost
standardCostUSD: number; // Cost before batch reduction
pricingMultiplier: number;
costUsdNano?: string;
costBreakdown?: object;
metered: { /* ... */ };
balances: { /* ... */ };
payg?: {
chargedUsd: number; // Final customer PAYG charge
markupPercent: number;
};
};
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 ck-..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-d '{
"customerId": "cust_123",
"customerUserId": "user_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-5.6-luna",
"messages": [
{ "role": "user", "content": "Hello" }
]
},
"responseType": "json"
},
"usage": {
"modelUsed": "gpt-5.6-luna"
}
}'
GET /calls/{callId}
Retrieves pricing for an existing call without changing usage, balances, or
settlement state. This endpoint requires an API key with the usage:read
scope. A missing call and a call owned by another organization both return
404 CALL_NOT_FOUND.
Endpoint: GET {baseUrl}/calls/{callId}
Response (200 OK):
{
result: {
status: "ACCEPTED";
code: "CALL_PRICING_RETRIEVED";
timestamp: string;
};
data: {
callId: string;
customerId: string | null; // Customer account identity recorded on the call
customerUserId: string | null; // End-user identity, when recorded on the call
status: "IN_PROGRESS" | "COMPLETED" | "FAILED" | "TIMEOUT" | "NOT_ALLOWED";
pricingStatus: "PENDING" | "FINAL" | "UNAVAILABLE";
currency: "USD";
costUSD: number | null; // Final provider/model cost
costUsdNano: string | null;
standardCostUSD: number | null; // Cost before batch reduction
pricingMultiplier: number;
pricingMode: "batch" | "standard";
batch: boolean;
costBreakdown: object | null;
modelPricingSnapshotId: string | null;
pricingTier: string | null;
pricingTierReason: string | null;
providerUsed: string | null;
modelUsed: string | null;
startTime: string | null;
endTime: string | null;
payg: {
usingCredit: boolean;
chargedUsd: number | null; // Final customer PAYG charge
chargeNanoUsd: string | null;
markupPercent: number | null;
settlementStatus: "PENDING" | "POSTED" | "FAILED" | null;
settlementFailureReason: string | null;
};
pricingUnavailableReason?: string;
};
correlationId: string;
}
pricingStatus must be FINAL before treating costUSD as finalized. A
completed call can report UNAVAILABLE when the model or pricing snapshot
could not be resolved; this prevents an unresolved price from being mistaken
for a free call.
customerId and customerUserId are the only customer identity fields
returned by this endpoint. Use stable, opaque, non-PII values for both fields.
customerUserId is null when it was not recorded on the call. Customer and
end-user names or emails, billing-provider identifiers, organizationId, and
the internal orgIdCustomerId composite key are deliberately not returned.
curl https://api.usagetap.com/calls/call_123 \
-H "Authorization: Bearer utk-..." \
-H "Accept: application/vnd.usagetap.v1+json"
POST /customers
Create or retrieve a customer subscription idempotently.
Endpoint: POST {baseUrl}/customers
Authentication: A Usage key with Meter calls access (ck-…) or an Admin
key (ak-…) from the provider organization.
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 ck-..." \
-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 ck-..." \
-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 ak-..." \
-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
customerUserId?: string; // Optional: Stable end-user identifier
customerUserName?: string; // Optional: End-user display name
customerUserEmail?: string; // Optional: End-user display email
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; // Always numeric; check `unlimited` for unbounded
limit: number | null;
used: number; // 0 for unlimited meters
unlimited: boolean;
ratio: number | null; // remaining/limit, null when unlimited
label: string;
periodUsageBefore: string; // Exact interval volume before this event
periodUsageAfter: string; // Exact interval volume after this event
overagePricingModel: "SINGLE" | "GRADUATED";
overagePricingComponents: Array<{
tierIndex: number;
upTo: string | null;
unitsExact: string;
priceNanoUsd: string;
priceQuantity: 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 ck-..." \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.usagetap.v1+json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customerId": "cust_123",
"customerUserId": "user_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) Idempotency-Keyis honored atomically with the subscription update. Reuse it only when retrying the same logical increment.- 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
- Graduated rates are resolved and snapshotted when the event is recorded. Tier progress resets with the plan's replenishment interval; UsageTap's own meter-event pricing should use a monthly plan.
SDK API
The TypeScript/JavaScript SDK provides a high-level interface to the REST API.
Installation
# OpenAI/OpenRouter
npm install @usagetap/sdk openai
# Anthropic
npm install @usagetap/sdk @anthropic-ai/sdk
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)
tokenCompanyApiKey?: string; // Optional remote prompt compression provider
tokenCompanyEndpoint?: string; // Optional compatible compression endpoint
tokenCompanyModel?: string; // Optional TTC model, default "bear-2"
aggressiveness?: number; // Optional compression aggressiveness, 0.0-1.0
tokenCompanyAppId?: string; // Optional TTC app identifier
usageTapCompressionEndpoint?: string; // Optional single-text compression endpoint
usageTapCompressionMessagesEndpoint?: string; // Optional message/request compression endpoint
});
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);
promptCompress(request, options?)
Optional local prompt compression for manual flows. Call it after beginCall and before your vendor request. beginCall does not compress prompts by itself.
const begin = await client.beginCall({
customerId: "cust_123",
feature: "chat.send",
idempotencyKey: crypto.randomUUID(),
});
const compressed = await client.promptCompress({
callId: begin.data.callId,
text: "Your text here",
provider: "usagetap",
model: "bear-2",
aggressiveness: 0.5,
});
const response = await openai.responses.create({
model: "gpt-5.6-luna",
input: compressed.compressedInput,
});
console.log("Approx tokens saved:", compressed.savedTokens);
console.log("Savings ratio:", compressed.tokenSavingsRatio);
promptCompress() compresses locally, sends only savings metadata to /compress_prompt, and returns compressedInput for your vendor request. It is fail-open: if local compression or metadata reporting fails, it returns the original input with zero savings so the vendor call can continue.
With provider: "usagetap", manual compression uses the single-text compatible endpoint and accepts scalar aggressiveness. OpenAI and Anthropic wrappers use the message/request endpoint and can pass per-role aggressiveness such as { user: 0.5, system: 0.5, tool: 0.5 }.
For OpenAI and Anthropic clients, wrapOpenAI() / wrapAnthropic() can compress automatically when enabled:
const ai = wrapOpenAI(openai, client, {
defaultContext: { customerId: "cust_123", feature: "chat.send" },
promptCompression: {
provider: "heuristic",
roles: { user: true, tool: true },
minTokens: 500,
},
});
Assistant messages are skipped by default. The wrappers record aggregate compression telemetry once per UsageTap call and expose totals on ai.promptCompression.
For custom pipelines, use client.compressPromptInput(input, options?) to get a PromptCompressionResult without recording telemetry, then call client.recordPromptCompression({ callId, promptCompression }) to persist precomputed savings metadata. Use protectPromptText(text) from @usagetap/sdk for exact spans that compatible compressors should not rewrite.
endCall(request, options?)
const response = await client.endCall({
callId: "call_xyz789",
modelUsed: "gpt-5.6-sol",
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-5.6-sol" : "gpt-5.6-luna";
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);
Anthropic Integration
wrapAnthropic(client, usageTap, options)
import Anthropic from "@anthropic-ai/sdk";
import { wrapAnthropic } from "@usagetap/sdk/anthropic";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const claude = wrapAnthropic(anthropic, client, {
defaultContext: {
customerId: "cust_123",
feature: "chat.anthropic",
requested: { standard: true, premium: true },
},
promptCompression: {
roles: { system: true, user: true, tool: true },
},
});
const message = await claude.messages.create({
model: "claude-3-5-haiku-latest",
max_tokens: 512,
system: "Long system prompt",
messages: [{ role: "user", content: "Long user prompt" }],
});
wrapAnthropic() instruments messages.create(), applies UsageTap model hints when model is omitted, extracts Anthropic token usage, and can compress system, user text blocks, and tool_result content before the vendor request.
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; // Always numeric (0 when exhausted; the `unlimited` flag is authoritative)
limit: number | null; // Null when plan does not specify a limit for this meter
used: number;
unlimited: boolean; // true = this meter is unbounded; remaining is informational only
ratio: number | null; // remaining/limit (0-1), null when unlimited or limit is null/0
}
Breaking change (v2026-02):
remainingis now always a number. Previouslynullmeant "unlimited"; that semantic has moved to the explicitunlimitedboolean flag. Consumers that testedremaining === nullto detect unlimited meters must switch to checkingunlimited === true.
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-5.6-sol" : "gpt-5.6-luna";
// 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 ck-...",
"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-5.6-luna",
messages: [{ role: "user", content: "Hello" }],
},
responseType: "json",
},
usage: {
modelUsed: "gpt-5.6-luna",
},
}),
});
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: July 29, 2026 API Version: v1 SDK Version: 1.4.0