Quota
/ docs
Dashboard
Docs/Core SDK

Core SDK

@usequota/core is a framework-agnostic TypeScript client for the Quota API. One client class, three auth modes, every endpoint typed end-to-end.

On Next.js?
Use @usequota/nextjs instead. It re-exports everything from core and adds React hooks, server utilities, and pre-built components.

01 Install

Runs on any JavaScript runtime with fetch and Web Crypto: Node 18+, Deno, Bun, Cloudflare Workers, Vercel Edge, and React Native.

npm install @usequota/core

02 Authenticate

QuotaClient picks an auth mode from the config you pass it. The SDK validates that exactly one valid combination is present and throws a typed QuotaError if not.

API key (developer billing)

Charges your own account directly. Use for first-party apps where you cover the inference cost.

import { QuotaClient } from "@usequota/core";

const quota = new QuotaClient({
  apiKey: process.env.QUOTA_API_KEY!,
});

OAuth access token

Uses an OAuth access token minted for an end-user. Pass refreshToken, clientId, and clientSecret alongside the access token to enable automatic refresh on 401 responses — the SDK retries the original request transparently and emits an onTokenRefresh callback so you can persist the rotated tokens.

const quota = new QuotaClient({
  accessToken: session.accessToken,
  refreshToken: session.refreshToken,
  clientId: process.env.QUOTA_CLIENT_ID!,
  clientSecret: process.env.QUOTA_CLIENT_SECRET!,
  onTokenRefresh: async ({ accessToken, refreshToken, expiresIn }) => {
    await db.sessions.update(session.id, {
      accessToken,
      refreshToken,
      expiresAt: new Date(Date.now() + expiresIn * 1000),
    });
  },
});

Constructor options

apiKeystringBearer API key. Highest priority — wins if set even when other credentials are also present.
clientIdstringOAuth client ID. Required for OAuth auto-refresh.
clientSecretstringOAuth client secret. Required for OAuth auto-refresh.
accessTokenstringOAuth access token for the end-user. Selects OAuth mode.
refreshTokenstringOAuth refresh token. Enables auto-refresh on 401 responses when clientId + clientSecret are also set.
onTokenRefresh(tokens) => void | Promise<void>Called after a successful auto-refresh with the rotated accessToken, refreshToken, and expiresIn. Persist the new tokens here.
baseUrlstringAPI base URL. Defaults to https://api.usequota.ai.
timeoutnumberPer-request timeout in milliseconds. Defaults to 10000.

03 Methods

Every method returns a typed promise. Errors are thrown as typed QuotaError subclasses (see Errors below).

getMe()

Returns the profile of the authenticated user.

GET/v1/me
const me = await quota.getMe();
// {
//   id: "u_abc123",
//   email: "ada@example.com",
//   balance: 1500000,                  // number | null — see note below
//   name: "Ada Lovelace",
//   picture: "https://...",
//   user_metadata: {}
// }
balance can be null

balance is number | null. Integer credits when the user has a Quota wallet (divide by 1,000,000 for USD); null when the user is signed in via the Quota IdP but hasn't linked a credits wallet to this app yet. Render a “Link wallet” CTA in the null case — don't display $0.00, which falsely implies an empty wallet.

Returns: Promise<QuotaUser>. Throws QuotaTokenExpiredError on expired/revoked credentials.

getBalance()

Returns the authenticated principal's current credit balance. The response shape depends on what kind of bearer token the client is configured with: API key → developer wallet, OAuth user token → that user's wallet.

GET/v1/balance
import { formatCredits } from "@usequota/core";

// Simple case: just want a number? Use the convenience field.
const { balance } = await quota.getBalance();
console.log(`Balance: ${formatCredits(balance)}`);

// Need to know which wallet you're looking at? Branch on the discriminator.
const b = await quota.getBalance();
if (b.wallet === "developer") {
  console.log(`Developer: ${formatCredits(b.developer_balance!)}`);
} else if (b.wallet === "oauth_user") {
  console.log(`User ${b.user_id}: ${formatCredits(b.balance)}`);
}

Returns: a discriminated union of three shapes (#357), widened to a single object type with optional fields. The wallet field tells you which shape you got.

wallet"developer" | "oauth_user"Discriminator. developer for API-key auth, oauth_user for OAuth bearer tokens.
balancenumberThe numeric credit balance for the returned wallet — present on every shape. For the developer wallet it mirrors developer_balance, so simple callers that only want a number don't have to branch on wallet. Divide by 1,000,000 (or call formatCredits()) for USD.
developer_balancenumberPresent when wallet === "developer". The renamed field that prompted the reshape — disambiguates the developer's own wallet from any user wallet they might be proxying for.
user_idstringPresent on developer and oauth_user shapes — the Quota user ID of the principal.
planstringOptional plan identifier, when set on the wallet.
billing_modestringEither "developer" (your wallet pays) or "user" (an OAuth user's wallet pays).

getPackages()

Returns the credit packages your account has configured for purchase. Public endpoint — no auth required, but the client's configured auth is sent anyway.

GET/v1/packages
const packages = await quota.getPackages();
// [
//   {
//     id: "starter",
//     credits: 4050000,         // 4,050,000 credits = $4.05 of balance for $5.00 paid
//     price_cents: 500,
//     price_display: "$5.00"
//   },
//   ...
// ]

Returns: Promise<CreditPackage[]>. The package fields are id, credits, price_cents, and price_display.

createCheckout(opts)

Creates a Stripe checkout session for purchasing a credit package and returns the hosted URL. Open url in a new tab — Quota handles webhook verification and credits the wallet on payment.

POST/api/payments/checkout
packageIdstringrequiredPackage id from getPackages().
successUrlstringrequiredAbsolute URL Stripe redirects to after a successful payment.
cancelUrlstringrequiredAbsolute URL Stripe redirects to if the user backs out.
const { url, sessionId } = await quota.createCheckout({
  packageId: "starter",
  successUrl: "https://example.com/billing/done",
  cancelUrl: "https://example.com/billing",
});

// Redirect (or window.open) the user to url

Returns: Promise<{ url: string; sessionId?: string }>.

04 Chat completions

Two helpers wrap POST /v1/chat/completions: chat() for one-shot calls and chatStream() for SSE streaming as an async iterable.

const response = await quota.chat({
  model: "anthropic/claude-sonnet-4.6",
  messages: [{ role: "user", content: "Hello" }],
});

console.log(response.choices[0].message.content);
// The final response includes a `quota` block with credits used.

Full request/response reference, including streaming chunk shapes and the quota billing block, lives on the Chat completions page.

05 Voice-to-voice (V2V)

connectVoiceConversion() opens the WebSocket relay described on the Audio API page, handles the first-frame auth handshake, and returns a session object with a binary send()/onAudio()pair plus typed close-code reporting. Use it in a non-browser runtime (Node, Deno, Edge functions); browser callers should reach for useVoiceConversion instead — it bundles the PCM AudioWorklet for capturing mic input.

import { connectVoiceConversion } from "@usequota/core";

const session = await connectVoiceConversion({
  apiKey: process.env.QUOTA_API_KEY!,
  model: "elevenlabs/voice-conversion-v1",
  voice: "rachel",
  format: "pcm_16le_16k_mono",
  maxDurationSeconds: 600,
});

// Forward your own PCM source (file, microphone, RTC track):
session.send(pcmChunk); // Uint8Array

// Receive transformed audio frames:
session.onAudio((chunk) => audioOut.append(chunk));

// Typed close codes — the SDK maps numeric WebSocket codes to enum
// values so client logic doesn't depend on RFC 6455 numerics.
session.onClose((code, reason) => {
  if (code === "BALANCE_EXHAUSTED") {
    // You declared a 10-minute cap; the session ran out of credits
    // before max_duration. Don't auto-reconnect — top up first.
  } else if (code === "FORMAT_VIOLATION") {
    // Your input bytes/sec exceeded 1.5x the declared PCM rate.
  } else if (code === "HEARTBEAT_LOST") {
    // Network dropped; safe to reconnect.
  }
});

Returns a VoiceConversionSession with:

  • send(bytes: Uint8Array): void — forward a PCM frame.
  • onAudio(handler): () => void — subscribe to inbound binary frames. Returns an unsubscribe function.
  • onClose(handler: (code: V2VCloseCode, reason: string) => void) — typed close codes (BALANCE_EXHAUSTED, UPSTREAM_DISCONNECTED, SESSION_TIMEOUT, FORMAT_VIOLATION, INVALID_AUTH, HEARTBEAT_LOST).
  • close(): Promise<void> — graceful close (sends a 1000 close to the server; the reservation refund happens on the server side).

Billing is per-second-of-input-audio at $9.00/hour on elevenlabs/voice-conversion-v1. The handshake reserves maxDurationSeconds × price atomically; the diff refunds on close. See the wire protocol for the exact frame sequence.

06 Webhooks

verifyWebhookSignature is a runtime-agnostic HMAC-SHA256 verifier built on Web Crypto. It works in Node, Deno, Bun, Cloudflare Workers, and Vercel Edge.

verifyWebhookSignature is async
It returns Promise<boolean>. You must await it. Without await, if (isValid) branches on a Promise (always truthy) and silently bypasses signature verification — every forged request would be accepted.

verifyWebhookSignature(opts)

payloadstring | ArrayBuffer | Uint8ArrayrequiredThe raw request body, exactly as sent. Do not parse or re-stringify before verifying.
signaturestringrequiredHex-encoded HMAC from the X-Quota-Signature request header.
secretstringrequiredThe webhook secret from your Quota dashboard.
import { verifyWebhookSignature } from "@usequota/core";

export async function POST(request: Request) {
  const rawBody = await request.text();
  const signature = request.headers.get("x-quota-signature");

  // `await` is REQUIRED — without it, an unresolved Promise is truthy
  // and verification is silently bypassed.
  const isValid = await verifyWebhookSignature({
    payload: rawBody,
    signature: signature ?? "",
    secret: process.env.QUOTA_WEBHOOK_SECRET!,
  });

  if (!isValid) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(rawBody);
  // ...handle event.type
  return new Response("ok");
}

Returns: Promise<boolean>. Returns false on a malformed signature, length mismatch, or verification failure — never throws.

parseWebhook(request, secret) / createWebhookHandler(secret, handlers)

Higher-level helpers that read the body, verify the signature, and dispatch to per-event handlers. See Webhooks for the full event payload reference.

07 Typed errors

Every failed request throws a QuotaError subclass. Match with instanceof for branchy recovery.

import {
  QuotaError,
  QuotaInsufficientCreditsError,
  QuotaNotConnectedError,
  QuotaTokenExpiredError,
  QuotaRateLimitError,
} from "@usequota/core";

try {
  await quota.chat({ model: "gpt-4o", messages });
} catch (e) {
  if (e instanceof QuotaInsufficientCreditsError) {
    // e.balance, e.required (in credits)
    showTopUpDialog({ short: e.required - e.balance });
  } else if (e instanceof QuotaNotConnectedError) {
    redirectToOAuth();
  } else if (e instanceof QuotaTokenExpiredError) {
    redirectToOAuth();
  } else if (e instanceof QuotaRateLimitError) {
    await new Promise((r) => setTimeout(r, e.retryAfter * 1000));
  } else if (e instanceof QuotaError) {
    console.error(`[${e.code}] ${e.message}`);
  }
}

Error classes

402QuotaInsufficientCreditsErrorWallet can't cover the reservation. Carries balance and required (both in credits).
401QuotaNotConnectedErrorThe user has not linked a Quota wallet to your app.
401QuotaTokenExpiredErrorOAuth access token expired and could not be refreshed.
429QuotaRateLimitErrorRate limit exceeded. Wait retryAfter seconds (default 60) before retrying.
*QuotaErrorBase class. Carries code, statusCode, and an optional hint.

08 Token storage

OAuth integrations need somewhere to persist access and refresh tokens across requests. QuotaTokenStorage is the pluggable interface; InMemoryTokenStorage is a single-process implementation suitable for tests and development. Implement your own adapter backed by Redis, Postgres, or your session store to share tokens across instances.

import type { QuotaTokenStorage } from "@usequota/core";

const dbTokenStorage: QuotaTokenStorage = {
  async getTokens(request) {
    const userId = await getUserIdFromSession(request);
    const row = await db.quotaTokens.findUnique({ where: { userId } });
    if (!row) return null;
    return {
      accessToken: row.accessToken,
      refreshToken: row.refreshToken,
    };
  },

  async setTokens(tokens, request) {
    const userId = await getUserIdFromSession(request);
    await db.quotaTokens.upsert({
      where: { userId },
      update: { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken },
      create: { userId, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken },
    });
  },

  async deleteTokens(request) {
    const userId = await getUserIdFromSession(request);
    await db.quotaTokens.delete({ where: { userId } });
  },
};