UsageTap API Reference

Complete reference for the UsageTap REST API and SDK.

Table of Contents


Authentication

API requests require authentication via Bearer token or API key header unless an endpoint is explicitly documented as public. The model-alternatives lookup is public and does not require a UsageTap account or API key.

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/{customerId}/usage-context, /customers/{customerId}/forecast customers:read and 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: Authenticated endpoints require Accept: application/vnd.usagetap.v1+json; requests without it receive 406 Not Acceptable. Public model-alternatives requests use ordinary application/json and do not require an Accept header.


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

GET /v1/model-alternatives/{modelKey}

Resolve a provider model key to source-backed same-provider equivalents and cross-provider candidates. This endpoint is public, CORS-enabled, and does not require an API key or the UsageTap vendor media type.

Use a provider-qualified key when possible. The greedy path also accepts model keys that contain /.

curl https://api.usagetap.com/v1/model-alternatives/openai/gpt-4-turbo
{
  "schemaVersion": 1,
  "catalogVersion": "2026-09-16",
  "requested": {
    "modelKey": "openai/gpt-4-turbo",
    "provider": "openai",
    "model": "gpt-4-turbo"
  },
  "lifecycle": {
    "status": "DEPRECATED",
    "shutdownAt": "2026-10-23",
    "source": {
      "label": "OpenAI API deprecations",
      "url": "https://developers.openai.com/api/docs/deprecations",
      "checkedAt": "2026-09-16"
    }
  },
  "action": "REPLACE",
  "recommendedModelKey": "openai/gpt-5.6-sol",
  "equivalents": [
    {
      "modelKey": "openai/gpt-5.6-sol",
      "provider": "openai",
      "relation": "official_replacement",
      "confidence": "authoritative",
      "rationale": "The provider names this model as the replacement.",
      "requiresEvaluation": false
    }
  ],
  "alternatives": [
    {
      "modelKey": "anthropic/claude-sonnet-4-6",
      "provider": "anthropic",
      "relation": "similar_price_and_strength",
      "confidence": "candidate",
      "rationale": "UsageTap places this model in a similar capability and price class; evaluate it on your workload before switching.",
      "requiresEvaluation": true
    }
  ],
  "automation": {
    "canResolveWithoutUsageTapAccount": true,
    "autoApplyRecommended": true,
    "note": "The recommended key is provider-designated. Opt in to automatic substitution only after validating request parameters and output behavior."
  }
}

equivalents lists provider-designated replacements with confidence: "authoritative" first, followed by any same-provider similarity candidates. alternatives contains cross-provider candidates. Computed entries include their score, price distance, current input/output price, and the mechanical capability evidence used to rank them. They report confidence: "candidate" and must be evaluated on the application's workload.

When available, match explains how a supplied alias or fuzzy model identifier was resolved, while pricing identifies the current pricing snapshot. The recommendationSource value distinguishes provider, admin_review, computed, and none results. An unknown key with no pricing/catalog match returns HTTP 200, lifecycle.status: "UNKNOWN", action: "REVIEW", and empty candidate arrays so application fallback logic remains deterministic.

GET /v1/model-alternatives returns the catalog metadata and all known transitions.

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;          // Provider-reported output tokens
      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
}

The persisted plan limit and Stripe meter keys standardTokens and premiumTokens represent standard-tier and premium-tier output tokens. Input tokens, including cache reads and cache writes, remain recorded and affect provider cost but do not consume these allowances or Stripe token meters. metered.tokens reports the same output-token quantity. Reasoning tokens are a breakdown of provider output and are not added again.

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"

GET /customers/{customerId}/usage-context

Prepare a compact, read-only account usage brief from the same canonical contract used by the UsageTap MCP tool. Requires both customers:read and usage:read.

Endpoint: GET {baseUrl}/customers/{customerId}/usage-context

Query Parameters:

  • timezone (optional): IANA timezone, default UTC
  • forecastMetric (optional): calls, inputTokens, outputTokens, allTokens, dollars, agenticApi, customMeter1, or customMeter2
  • includeForecast (optional): true by default
  • includeSignals (optional): true by default

The response contains contractVersion: "1", customer and plan facts, meters with explicit units, funding context, the latest seven completed UTC days versus the preceding seven, current-period quantities, signals, forecast status and quality, deterministic review reasons, and evidence completeness. All periods use an inclusive start and exclusive end.

Only agenticApi, customMeter1, and customMeter2 have a one-to-one allowance mapping and may return projectedAllowanceExhaustionAt. Aggregate call and token forecasts never fabricate a combined allowance runout. A runout of included allowance does not guarantee service interruption because purchased meter credit, PAYG funding, or another configured policy may allow usage to continue. dollars means recorded provider cost, not revenue or margin.

curl -X GET "https://api.usagetap.com/customers/cust_123/usage-context?forecastMetric=agenticApi" \
  -H "Authorization: Bearer utk-..." \
  -H "Accept: application/vnd.usagetap.v1+json"

Missing and cross-tenant customer IDs return the same non-enumerating 404. Responses use Cache-Control: private, no-store.


GET /customers/{customerId}/forecast

Return the canonical, customer-scoped forecast component without the rest of the account brief. Requires both customers:read and usage:read.

Endpoint: GET {baseUrl}/customers/{customerId}/forecast

Query Parameters:

  • metric (required): one of the supported metrics listed above
  • timezone (optional): IANA timezone, default UTC

The projection horizon is resolved by the server from the customer's next replenishment boundary. The public endpoint does not accept an organization ID, arbitrary horizon, model tuning, exclusions, seasonality test mode, or growth adjustment. Forecast status is one of available, insufficient_history, not_applicable, incomplete, or temporarily_unavailable.

curl -X GET "https://api.usagetap.com/customers/cust_123/forecast?metric=agenticApi&timezone=UTC" \
  -H "Authorization: Bearer utk-..." \
  -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 when omitted through the API): Switch immediately, grant the target plan's full allowances, and start a new replenishment cycle
  • IMMEDIATE_PRORATED: Switch immediately, preserve usage already consumed, adjust remaining allowances to the target limits, and keep the current replenishment date
  • AT_NEXT_REPLENISH: Keep the current plan until the existing scheduled replenishment, then apply the target plan

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.

Use meterSlot: "AGENTIC_API" for the dedicated Agentic & API Calls meter. One unit represents one HTTP API request or MCP tool call. It uses the same allocation, purchased meter credits, PAYG overages, and single or graduated pricing as Custom Meter 1 and 2. No model, token count, standard/premium tier, or call lifecycle is required. See the integration example.

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" | "AGENTIC_API"; // Required: Which meter to increment
  amount: number;               // Required: Positive safe integer usage quantity
  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" | "AGENTIC_API";
    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 usage exceeds allocation and eligible overage funding
  },
  correlationId: string
}

Error Responses:

  • 400 BAD_REQUEST: Invalid meterSlot, missing customerId, or invalid amount
  • 409 CUSTOM_METER_CONFLICT: Concurrent update; retry with the same idempotency key
  • 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. Usage consumes the included allocation, then eligible purchased meter credits and/or PAYG credit according to the meter's funding configuration.
  3. Idempotency-Key is honored atomically with the subscription update. Reuse it only when retrying the same logical increment.
  4. Unfunded usage is recorded and returns HTTP 200 with data.blocked: true; the application must enforce the flag. The last included unit is allowed. LLM BLOCK/DOWNGRADE policies do not change this meter contract.
  5. PAYG overages require PAYG to be enabled and a valid single or graduated price. Settlement is asynchronous; this endpoint does not reserve dollar credit or enforce a rolling rate limit.
  6. Unlimited meters record usage events without consuming allocation or charging overages. Their remaining/used snapshot counters are informational; use event history for usage totals.
  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" | "AGENTIC_API", required): Which 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
  • Authentication or transport failures may throw. An accepted report with data.blocked: true does not throw; inspect the flag and enforce it in your application.

Use Cases:

// Track agent tool invocations
await client.incrementCustomMeter({
  customerId: "cust_123",
  meterSlot: "AGENTIC_API",
  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 HTTP API requests or MCP tool calls
await client.incrementCustomMeter({
  customerId: "cust_789",
  meterSlot: "AGENTIC_API",
  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" }],
});

const stream = claude.messages.stream({
  model: "claude-sonnet-4-5",
  max_tokens: 512,
  messages: [{ role: "user", content: "Stream it" }],
});
stream.on("text", (text) => process.stdout.write(text));
const streamedMessage = await stream.finalMessage();

In @usagetap/sdk 1.6.0 and newer, wrapAnthropic() instruments messages.create() and the synchronous messages.stream() helper, applies UsageTap model hints when model is omitted, extracts Anthropic token and prompt-cache usage, and can compress system, user text blocks, and tool_result content before the vendor request. Wrapped create results retain .withResponse() and .asResponse(). messages.stream() preserves Anthropic's event handlers, async iterator, cancellation, and final-message helpers while UsageTap finalizes the call on completion, failure, or abort.

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