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.

Option 1: Bearer Token (Recommended)

Authorization: Bearer YOUR_USAGETAP_API_KEY

Option 2: API Key Header (Legacy)

x-api-key: YOUR_USAGETAP_API_KEY

If both headers are present, Authorization takes precedence.


Request Headers

Required Headers

Accept: application/vnd.usagetap.v1+json
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

Optional Headers

Header Description Example
Idempotency-Key Unique key for safe retries 550e8400-e29b-41d4-a716-446655440000
x-usage-correlation-id Track related requests corr_abc123
x-usage-sdk SDK identifier js/0.7.1

Important: The Accept header must be application/vnd.usagetap.v1+json. Requests without this header will receive 406 Not Acceptable.


Response Format

All responses follow the canonical envelope format:

Success Response

{
  result: {
    status: "ACCEPTED",
    code: string,           // Status code (e.g., "CALL_BEGIN_SUCCESS")
    message?: string,       // Optional human-readable message
    timestamp: string       // ISO 8601 timestamp
  },
  data: {                   // Endpoint-specific data
    // ...
  },
  correlationId: string     // For request tracing
}

Error Response

{
  result: {
    status: "ERROR",
    code: string,           // Error code
    message: string,        // Error description
    timestamp: string
  },
  error: {
    code: string,           // Detailed error code
    message: string,        // Error details
    details?: object        // Additional context
  },
  correlationId: string
}

Idempotency

UsageTap supports three methods for providing idempotency keys:

1. Request Header (Preferred)

POST /call_begin
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{
  "customerId": "cust_123",
  "requested": { "standard": true }
}

2. Request Body (Recommended Field: idempotencyKey)

{
  "customerId": "cust_123",
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
  "requested": { "standard": true }
}

3. Request Body (Deprecated: idempotency)

{
  "customerId": "cust_123",
  "idempotency": "550e8400-e29b-41d4-a716-446655440000",
  "requested": { "standard": true }
}

Priority: Header > idempotencyKey > idempotency

Auto-Generated Keys

If no idempotency key is provided, UsageTap generates one deterministically by hashing:

  • Organization ID
  • Customer ID
  • Feature name
  • Requested entitlements

The resolved key is returned in data.idempotency.key.

Idempotency Behavior

  • Same key + same payload → Returns cached response (same callId)
  • Same key + different payload → Returns error
  • Different key + same payload → Creates new call

REST API Endpoints

POST /call_begin

Start a usage tracking session and retrieve customer entitlements.

Endpoint: POST {baseUrl}/call_begin

Request Body:

{
  customerId: string;           // Required: Your customer identifier
  feature?: string;             // Optional: Feature being accessed
  requested?: {                 // Optional: Requested capabilities
    standard?: boolean;         // Standard tier models
    premium?: boolean;          // Premium tier models
    audio?: boolean;            // Audio processing
    image?: boolean;            // Image generation
    search?: boolean;           // Web search
    reasoningLevel?: "NONE" | "LOW" | "MEDIUM" | "HIGH";
  };
  holdUsd?: number;             // Optional: PAYG hold hint (USD) for this call
  idempotencyKey?: string;      // Optional: Unique key for retries
  tags?: string[];              // Optional: Tags for analytics
  customerName?: string;        // HIGHLY IMPORTANT BUT OPTIONAL: Display name (for new customers)
  customerEmail?: string;       // HIGHLY IMPORTANT BUT OPTIONAL: Email (for new customers)
  stripeCustomerId?: string;    // Optional: Link to Stripe customer
}

Response (200 OK):

{
  result: {
    status: "ACCEPTED",
    code: "CALL_BEGIN_SUCCESS",
    timestamp: "2025-11-05T12:00:00.000Z"
  },
  data: {
    callId: string;             // Use in call_end
    startTime: string;          // ISO 8601 timestamp
    feature: string;            // Echo of feature
    tags?: string[];            // Echo of tags
    newCustomer: boolean;       // True if customer was just created
    canceled: boolean;          // True if subscription is canceled
    policy: "NONE" | "BLOCK" | "DOWNGRADE";
    
    // What customer can actually use
    allowed: {
      standard: boolean;
      premium: boolean;
      audio: boolean;
      image: boolean;
      search: boolean;
      reasoningLevel: "NONE" | "LOW" | "MEDIUM" | "HIGH";
    };
    
    // Guidance for selecting models
    entitlementHints: {
      suggestedModelTier: "premium" | "standard" | "none";
      reasoningLevel: "NONE" | "LOW" | "MEDIUM" | "HIGH";
      policy: "NONE" | "BLOCK" | "DOWNGRADE";
      downgrade?: {
        reason: string;         // Why downgraded (e.g., "PREMIUM_QUOTA_EXHAUSTED")
        fallbackTier?: string;  // Suggested fallback
      };
    };
    
    // Current usage levels per meter
    meters: {
      [meterName: string]: {
        remaining: number | null;
        limit: number | null;
        used: number;
        unlimited: boolean;
        ratio: number | null;   // remaining/limit (0-1)
      };
    };
    
    // Quick lookup for remaining ratios
    remainingRatios: {
      [meterName: string]: number | null;
    };
    
    // Subscription details
    subscription: {
      id: string;
      usagePlanVersionId: string;
      planName: string;
      planVersion: string;
      limitType: "NONE" | "BLOCK" | "DOWNGRADE";
      reasoningLevel: "NONE" | "LOW" | "MEDIUM" | "HIGH";
      lastReplenishedAt: string;
      nextReplenishAt: string;
      subscriptionVersion: number;
      customerFriendlyName?: string;
      customerEmail?: string;
      stripeCustomerId?: string;
      pending?: {               // Scheduled plan change
        usagePlanVersionId: string;
        strategy: string;
        effectiveAt: string;
      };
    };
    
    // Organization-specific model mappings
    models?: {
      standard?: string[];      // Standard tier models
      premium?: string[];       // Premium tier models
    };
    
    // Resolved idempotency key
    idempotency: {
      key: string;              // Always matches callId
      source: "explicit" | "derived";
    };
    
    // Vendor-specific hints (optional)
    vendorHints?: {
      preferredModel?: string;
      maxInputTokens?: number;
      maxResponseTokens?: number;
    };
    
    // Legacy fields (backward compatibility)
    plan?: {
      id: string;
      name: string;
      version: string;
    };
    balances?: {
      tokensRemaining?: number;
      searchesRemaining?: number;
    };
  },
  correlationId: string
}

Example:

curl -X POST https://api.usagetap.com/call_begin \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.usagetap.v1+json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "customerId": "cust_123",
    "feature": "chat.completions",
    "requested": {
      "standard": true,
      "premium": true,
      "search": true,
      "reasoningLevel": "HIGH"
    },
    "tags": ["production", "web-app"]
  }'

POST /call_end

Report actual usage for a tracked call.

Endpoint: POST {baseUrl}/call_end

Request Body:

{
  callId: string;               // Required: From call_begin response
  modelUsed?: string;           // Model identifier (e.g., "gpt-4o")
  inputTokens?: number;         // Prompt tokens
  responseTokens?: number;      // Completion tokens
  cachedTokens?: number;        // Cached/reused tokens
  reasoningTokens?: number;     // Reasoning tokens (o1 models)
  searches?: number;            // Number of web searches
  audio?: number;               // Audio processing units
  audioSeconds?: number;        // Audio duration in seconds
  isPremium?: boolean;          // Override premium detection
  error?: {                     // Report error if call failed
    code: string;
    message: string;
  };
  stripeCustomerId?: string;    // Override Stripe customer ID
}

Response (200 OK):

{
  result: {
    status: "ACCEPTED",
    code: "CALL_END_SUCCESS",
    timestamp: "2025-11-05T12:00:05.000Z"
  },
  data: {
    callId: string;
    costUSD: number;            // Calculated cost in USD
    metered: {                  // Usage that was counted
      calls?: number;
      tokens?: number;
      reasoningTokens?: number;
      searches?: number;
      audio?: number;
      audioSeconds?: number;
    };
    balances?: {                // Remaining quotas
      tokensRemaining?: number;
      searchesRemaining?: number;
      audioSecondsRemaining?: number;
    };
    stripeCustomerId?: string;
  },
  correlationId: string
}

Premium Detection:

UsageTap automatically classifies calls as premium based on model pricing:

  • Premium: Output token price > $4.00 per million tokens
  • Standard: Output token price ≤ $4.00 per million tokens

Override with isPremium: true or isPremium: false for custom models.

Example:

curl -X POST https://api.usagetap.com/call_end \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.usagetap.v1+json" \
  -H "x-usage-correlation-id: corr_abc123" \
  -d '{
    "callId": "call_xyz789",
    "modelUsed": "gpt-4o",
    "inputTokens": 512,
    "responseTokens": 256,
    "reasoningTokens": 0,
    "searches": 1
  }'

POST /call

Unified endpoint that combines begin, vendor call, and end in one request.

Endpoint: POST {baseUrl}/call

Request Body:

{
  // Standard call_begin fields
  customerId: string;
  feature?: string;
  requested?: { /* entitlements */ };
  idempotencyKey?: string;
  holdUsd?: number;             // Optional: PAYG hold hint (USD) for this call
  tags?: string[];
  
  // Optional vendor call configuration
  vendor?: {
    url: string;                // Vendor API endpoint
    method: "POST" | "GET";     // HTTP method
    headers: {                  // Headers to send
      [key: string]: string;
    };
    body?: object;              // Request body
    timeoutMs?: number;         // Request timeout (default: 30000)
    responseType?: "json" | "text" | "stream";
  };
  
  // Usage overrides (if vendor block is omitted)
  usage?: {
    modelUsed?: string;
    inputTokens?: number;
    responseTokens?: number;
    // ... other usage fields
  };
  
  // Optional error details
  end?: {
    error?: {
      code: string;
      message: string;
    };
  };
}

Response (200 OK):

{
  result: {
    status: "ACCEPTED",
    code: "CALL_SUCCESS" | "CALL_VENDOR_WARNING",
    timestamp: string
  },
  data: {
    begin: {                    // call_begin response
      callId: string;
      allowed: { /* ... */ };
      // ... other begin fields
    };
    end: {                      // call_end response
      metered: { /* ... */ };
      balances: { /* ... */ };
    };
    vendor?: {                  // Vendor response (if vendor block provided)
      ok: boolean;
      status: number;
      modelUsed?: string;
      error?: {
        message: string;
        details?: object;
      };
    };
    endUsage: {                 // Final reported usage
      tokens: number;
      modelUsed: string;
      // ... other usage fields
    };
  },
  correlationId: string
}

Behavior:

  • If 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 sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.usagetap.v1+json" \
  -d '{
    "customerId": "cust_123",
    "feature": "chat.completions",
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "requested": {
      "standard": true,
      "premium": true
    },
    "vendor": {
      "url": "https://api.openai.com/v1/chat/completions",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer sk-...",
        "Content-Type": "application/json"
      },
      "body": {
        "model": "gpt-4o-mini",
        "messages": [
          { "role": "user", "content": "Hello" }
        ]
      },
      "responseType": "json"
    },
    "usage": {
      "modelUsed": "gpt-4o-mini"
    }
  }'

POST /customers

Create or retrieve a customer subscription idempotently.

Endpoint: POST {baseUrl}/customers

Request Body:

{
  customerId: string;           // Required: Your customer identifier
  customerFriendlyName?: string; // HIGHLY IMPORTANT BUT OPTIONAL: Display name (preferred field; alias: customerName)
  customerName?: string;        // HIGHLY IMPORTANT BUT OPTIONAL: Alias for customerFriendlyName
  customerEmail?: string;       // HIGHLY IMPORTANT BUT OPTIONAL: Email for notifications
  stripeCustomerId?: string;    // Link to Stripe customer
}

Response (200 OK):

{
  result: {
    status: "ACCEPTED",
    code: "CUSTOMER_READY",
    timestamp: string
  },
  data: {
    customerId: string;
    newCustomer: boolean;       // True if just created, false if existing
    canceled: boolean;
    policy: "NONE" | "BLOCK" | "DOWNGRADE";
    allowed: { /* ... */ };     // Current entitlements
    entitlementHints: { /* ... */ };
    meters: { /* ... */ };      // Current usage
    remainingRatios: { /* ... */ };
    subscription: { /* ... */ }; // Full subscription details
    models?: { /* ... */ };
    plan?: { /* ... */ };
    balances?: { /* ... */ };
    stripeCustomerId?: string;
  },
  correlationId: string
}

Idempotent: Calling multiple times returns the same customer. Only newCustomer: true on first call.

Example:

curl -X POST https://api.usagetap.com/customers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.usagetap.v1+json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "customerId": "cust_123",
    "customerFriendlyName": "Acme Corp",
    "customerEmail": "billing@acme.com",
    "stripeCustomerId": "cus_stripe123"
  }'

GET /customers/{customerId}/usage

Check current usage status without creating a call.

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

Path Parameters:

  • customerId (string, required): Customer identifier

Response (200 OK):

Same structure as POST /customers but without newCustomer field.

Example:

curl -X GET https://api.usagetap.com/customers/cust_123/usage \
  -H "Authorization: Bearer sk_test_..." \
  -H "Accept: application/vnd.usagetap.v1+json"

POST /customers/{customerId}/change_plan

Change customer's usage plan.

Endpoint: POST {baseUrl}/customers/{customerId}/change_plan

Path Parameters:

  • customerId (string, required): Customer identifier

Request Body:

{
  planId: string;               // Required: Target plan ID
  strategy?: "IMMEDIATE_RESET" | "IMMEDIATE_PRORATED" | "AT_NEXT_REPLENISH";
}

Strategy Options:

  • IMMEDIATE_RESET (default): Switch immediately, reset all usage to 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 sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.usagetap.v1+json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "planId": "plan_premium_v2",
    "strategy": "IMMEDIATE_RESET"
  }'

POST /custom_meter

Update (increment) a custom meter for a customer by recording a new usage event. Custom meters allow you to track any usage metric beyond the standard LLM usage counters.

Endpoint: POST {baseUrl}/custom_meter

Headers:

  • Accept: application/vnd.usagetap.v1+json
  • Content-Type: application/json
  • Idempotency-Key (recommended) or rely on server-side derivation

Request Body:

{
  customerId: string;           // Required: Customer identifier
  meterSlot: "CUSTOM1" | "CUSTOM2";  // Required: Which custom meter to increment
  amount: number;               // Required: Positive number to decrement from quota
  feature?: string;             // Optional: Feature identifier for tracking
  tags?: string[];              // Optional: Tags for categorization
  metadata?: object;            // Optional: Additional metadata
}

Response (200 OK):

{
  result: {
    status: "ACCEPTED",
    code: "CUSTOM_METER_SUCCESS",
    timestamp: string
  },
  data: {
    success: boolean;           // true
    eventId: string;            // Unique event identifier
    meterSlot: "CUSTOM1" | "CUSTOM2";
    amount: number;             // Amount that was recorded
    meter: {                    // Updated meter snapshot
      remaining: number | null;
      limit: number | null;
      used: number | null;
      unlimited: boolean;
      label: string;
    };
    blocked: boolean;           // true if quota exceeded and policy is BLOCK
  },
  correlationId: string
}

Error Responses:

  • 400 BAD_REQUEST: Invalid meterSlot, missing customerId, or invalid 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 sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.usagetap.v1+json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "customerId": "cust_123",
    "meterSlot": "CUSTOM1",
    "amount": 5,
    "feature": "agent_actions",
    "tags": ["workflow_automation"],
    "metadata": {
      "workflowId": "wf_abc123",
      "actionType": "email_send"
    }
  }'

Use Cases:

Custom meters are ideal for tracking:

  • Agent actions or tool invocations
  • Document processing units
  • API calls to non-LLM services
  • Custom workflow steps
  • Any usage that doesn't fit standard LLM metrics

Important Notes:

  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. With BLOCK policy, requests exceeding quota return 403 QUOTA_EXCEEDED
  4. With DOWNGRADE policy, usage continues but quota can go negative
  5. Unlimited meters don't track usage but still record events

SDK API

The TypeScript/JavaScript SDK provides a high-level interface to the REST API.

Installation

npm install @usagetap/sdk openai

Initialization

import { UsageTapClient } from "@usagetap/sdk";

const client = new UsageTapClient({
  apiKey: process.env.USAGETAP_API_KEY!,
  baseUrl: process.env.USAGETAP_BASE_URL!,
  
  // Optional configuration
  defaultFeature?: string;          // Default feature name
  defaultTags?: string[];           // Default tags
  fetchImpl?: typeof fetch;         // Custom fetch implementation
  headers?: Record<string, string>; // Additional headers
  retries?: {
    maxAttempts?: number;           // Default: 3
    baseDelayMs?: number;           // Default: 250
    maxDelayMs?: number;            // Default: 5000
    jitterRatio?: number;           // Default: 0.2
  };
  idempotencyGenerator?: () => string; // Custom ID generator
  autoIdempotency?: boolean;        // Default: true
  onLog?: (entry) => void;          // Logging callback
  useApiKeyHeader?: boolean;        // Use x-api-key header
  allowBrowser?: boolean;           // Allow browser usage (testing only)
});

Core Methods

beginCall(request, options?)

const response = await client.beginCall({
  customerId: "cust_123",
  feature: "chat.send",
  idempotencyKey: crypto.randomUUID(),
  requested: {
    standard: true,
    premium: true,
    search: true,
    reasoningLevel: "HIGH",
  },
  tags: ["production"],
});

console.log("Call ID:", response.data.callId);
console.log("Allowed:", response.data.allowed);

endCall(request, options?)

const response = await client.endCall({
  callId: "call_xyz789",
  modelUsed: "gpt-4o",
  inputTokens: 512,
  responseTokens: 256,
  reasoningTokens: 0,
  searches: 1,
});

console.log("Cost:", response.data.costUSD);
console.log("Metered:", response.data.metered);

withUsage(request, handler, options?)

const result = await client.withUsage(
  {
    customerId: "cust_123",
    feature: "chat.send",
    idempotencyKey: crypto.randomUUID(),
    requested: { standard: true, premium: true },
  },
  async ({ begin, setUsage, setError }) => {
    try {
      const model = begin.data.allowed.premium ? "gpt-4o" : "gpt-4o-mini";
      
      const response = await openai.chat.completions.create({
        model,
        messages: [{ role: "user", content: "Hello" }],
      });

      setUsage({
        modelUsed: model,
        inputTokens: response.usage?.prompt_tokens ?? 0,
        responseTokens: response.usage?.completion_tokens ?? 0,
      });

      return response.choices[0].message.content;
    } catch (error) {
      setError({
        code: "VENDOR_ERROR",
        message: error instanceof Error ? error.message : String(error),
      });
      throw error;
    }
  }
);

createCustomer(request, options?)

const response = await client.createCustomer({
  customerId: "cust_123",
  customerFriendlyName: "Acme Corp",
  customerEmail: "billing@acme.com",
  stripeCustomerId: "cus_stripe123",
});

console.log("New customer?", response.data.newCustomer);
console.log("Plan:", response.data.subscription.planName);

checkUsage(request, options?)

const response = await client.checkUsage({
  customerId: "cust_123",
});

console.log("Meters:", response.data.meters);
console.log("Allowed:", response.data.allowed);

changePlan(request, options?)

const response = await client.changePlan({
  customerId: "cust_123",
  planId: "plan_premium_v2",
  strategy: "IMMEDIATE_RESET",
});

console.log("Success:", response.data.success);
console.log("New plan:", response.data.subscription.planName);

incrementCustomMeter(request, options?)

Track custom usage metrics beyond standard LLM counters.

const response = await client.incrementCustomMeter({
  customerId: "cust_123",
  meterSlot: "CUSTOM1",
  amount: 5,
  feature: "agent_actions",
  tags: ["workflow_automation"],
  metadata: {
    workflowId: "wf_abc123",
    actionType: "email_send",
  },
});

console.log("Event ID:", response.data.eventId);
console.log("Remaining:", response.data.meter.remaining);
console.log("Blocked:", response.data.blocked);

Parameters:

  • customerId (string, required): Customer 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);

Express Middleware

import { withUsage } from "@usagetap/sdk/express";

app.use(
  withUsage(client, (req) => req.user?.id || "anonymous")
);

app.post("/api/chat", async (req, res) => {
  const ai = req.usageTap!.openai(openai, {
    feature: "chat.assistant",
    requested: { standard: true, premium: true },
  });
  
  const stream = await ai.chat.completions.create({
    messages: req.body.messages,
    stream: true,
  });
  
  req.usageTap!.pipeToResponse(stream, res);
});

React Hook

import { useChatWithUsage } from "@usagetap/sdk/react";

function Chat({ userId }: { userId: string }) {
  const { messages, input, setInput, handleSubmit, isLoading } = 
    useChatWithUsage({
      api: "/api/chat",
      customerId: userId,
      feature: "chat.assistant",
    });

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>{m.content}</div>
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={(e) => setInput(e.target.value)} />
        <button type="submit" disabled={isLoading}>Send</button>
      </form>
    </div>
  );
}

Type Definitions

ReasoningLevel

type ReasoningLevel = "NONE" | "LOW" | "MEDIUM" | "HIGH";

LimitType

type LimitType = "NONE" | "BLOCK" | "DOWNGRADE";

RequestedEntitlements

interface RequestedEntitlements {
  standard?: boolean;
  premium?: boolean;
  audio?: boolean;
  image?: boolean;
  search?: boolean;
  reasoningLevel?: ReasoningLevel;
}

AllowedEntitlements

type AllowedEntitlements = Required<RequestedEntitlements>;

MeterSummary

interface MeterSummary {
  remaining: number | null;     // Null if unlimited
  limit: number | null;         // Null if unlimited
  used: number;
  unlimited: boolean;
  ratio: number | null;         // remaining/limit (0-1), null if unlimited
}

SubscriptionSnapshot

interface SubscriptionSnapshot {
  id: string;
  usagePlanVersionId: string;
  planName: string;
  planVersion: string;
  limitType: LimitType;
  reasoningLevel: ReasoningLevel;
  lastReplenishedAt: string;    // ISO 8601
  nextReplenishAt: string;      // ISO 8601
  subscriptionVersion: number;
  customerFriendlyName?: string;
  customerEmail?: string;
  stripeCustomerId?: string;
  pending?: {
    usagePlanVersionId: string;
    strategy: string;
    effectiveAt: string;
  };
}

EntitlementHints

interface EntitlementHints {
  suggestedModelTier: "premium" | "standard" | "none";
  reasoningLevel: ReasoningLevel;
  policy: LimitType;
  downgrade?: {
    reason: string;
    fallbackTier?: "premium" | "standard" | "none";
  };
}

IdempotencyMetadata

interface IdempotencyMetadata {
  key: string;                  // Always matches callId
  source: "explicit" | "derived";
}

Error Codes

SDK Error Codes

Code Description Retryable
USAGETAP_AUTH_ERROR Invalid API key or unauthorized No
USAGETAP_BAD_REQUEST Invalid request parameters No
USAGETAP_RATE_LIMITED Too many requests Yes
USAGETAP_SERVER_ERROR Server-side error Yes
USAGETAP_NETWORK_ERROR Network failure Yes
USAGETAP_INVALID_RESPONSE Unexpected response format No
USAGETAP_END_CALL_ERROR Failed to finalize call No
USAGETAP_BROWSER_RUNTIME SDK used in browser without allowBrowser No

API Result Codes

Code Status Description
CALL_BEGIN_SUCCESS ACCEPTED Call started successfully
CALL_END_SUCCESS ACCEPTED Call ended successfully
CALL_SUCCESS ACCEPTED Unified /call succeeded
CALL_VENDOR_WARNING ACCEPTED /call succeeded but vendor returned error
CUSTOMER_READY ACCEPTED Customer subscription ready
PLAN_CHANGED ACCEPTED Plan change successful

HTTP Status Codes

Status Meaning Retryable
200 Success N/A
400 Bad Request (invalid parameters) No
401 Unauthorized (invalid API key) No
403 Forbidden No
404 Not Found No
406 Not Acceptable (missing Accept header) No
409 Conflict (idempotency key mismatch) No
429 Rate Limited Yes
500 Internal Server Error Yes
502 Bad Gateway Yes
503 Service Unavailable Yes
504 Gateway Timeout Yes

Examples

Complete Node.js Script

import OpenAI from "openai";
import { UsageTapClient } from "@usagetap/sdk";
import crypto from "crypto";

const usageTap = new UsageTapClient({
  apiKey: process.env.USAGETAP_API_KEY!,
  baseUrl: process.env.USAGETAP_BASE_URL!,
});

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
});

async function chat(userId: string, message: string) {
  return usageTap.withUsage(
    {
      customerId: userId,
      feature: "chat.send",
      idempotencyKey: crypto.randomUUID(),
      requested: {
        standard: true,
        premium: true,
        search: true,
        reasoningLevel: "MEDIUM",
      },
    },
    async ({ begin, setUsage }) => {
      // Select model based on entitlements
      const model = begin.data.allowed.premium ? "gpt-4o" : "gpt-4o-mini";
      
      // Add search tool if allowed
      const tools = begin.data.allowed.search
        ? [{ type: "web_search" as const }]
        : undefined;

      const response = await openai.chat.completions.create({
        model,
        messages: [{ role: "user", content: message }],
        tools,
      });

      setUsage({
        modelUsed: model,
        inputTokens: response.usage?.prompt_tokens ?? 0,
        responseTokens: response.usage?.completion_tokens ?? 0,
        searches: tools ? response.usage?.web_search_queries ?? 0 : 0,
      });

      return response.choices[0].message.content;
    }
  );
}

chat("user_123", "What's new in AI today?")
  .then((response) => console.log("Response:", response))
  .catch((error) => console.error("Error:", error));

Raw HTTP Request

const response = await fetch("https://api.usagetap.com/call", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
    "Accept": "application/vnd.usagetap.v1+json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    customerId: "cust_123",
    feature: "chat.completions",
    requested: {
      standard: true,
      premium: true,
    },
    vendor: {
      url: "https://api.openai.com/v1/chat/completions",
      method: "POST",
      headers: {
        "Authorization": "Bearer sk-...",
        "Content-Type": "application/json",
      },
      body: {
        model: "gpt-4o-mini",
        messages: [
          { role: "user", content: "Hello" }
        ],
      },
      responseType: "json",
    },
    usage: {
      modelUsed: "gpt-4o-mini",
    },
  }),
});

const result = await response.json();
console.log("Result:", result.data);

Support


Last Updated: November 5, 2025 API Version: v1 SDK Version: 0.7.1