UsageTap API Reference

Complete reference for the UsageTap REST API and SDK.

Table of Contents


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.

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 }
}
{
  "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.requiredScope names 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_begin behaves 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.5 for batch or 1 for 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 vendor is provided, UsageTap makes the HTTP call and extracts usage
  • If vendor is omitted, only usage overrides are recorded
  • Non-2xx vendor responses still trigger call_end with 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 zero
  • IMMEDIATE_PRORATED: Switch immediately, prorate existing usage to new limits
  • AT_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+json
  • Content-Type: application/json
  • Idempotency-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 amount
  • 403 QUOTA_EXCEEDED: Insufficient quota (when limitType is BLOCK)
  • 404 SUBSCRIPTION_NOT_FOUND: No active subscription found
  • 400 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:

  1. Custom meters must be enabled in the customer's usage plan
  2. The amount decrements the remaining quota (similar to how tokens work)
  3. Idempotency-Key is honored atomically with the subscription update. Reuse it only when retrying the same logical increment.
  4. With BLOCK policy, requests exceeding quota return 403 QUOTA_EXCEEDED
  5. With DOWNGRADE policy, usage continues but quota can go negative
  6. Unlimited meters don't track usage but still record events
  7. 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 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

Returns: Updated meter snapshot with remaining, limit, used, unlimited, and label fields.

Throws:

  • UsageTapError with code USAGETAP_BAD_REQUEST for invalid parameters
  • UsageTapError with code USAGETAP_AUTH_ERROR for 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): remaining is now always a number. Previously null meant "unlimited"; that semantic has moved to the explicit unlimited boolean flag. Consumers that tested remaining === null to detect unlimited meters must switch to checking unlimited === 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


Last Updated: July 29, 2026 API Version: v1 SDK Version: 1.4.0