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.
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/core02 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
| apiKeystring | Bearer API key. Highest priority — wins if set even when other credentials are also present. |
| clientIdstring | OAuth client ID. Required for OAuth auto-refresh. |
| clientSecretstring | OAuth client secret. Required for OAuth auto-refresh. |
| accessTokenstring | OAuth access token for the end-user. Selects OAuth mode. |
| refreshTokenstring | OAuth 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. |
| baseUrlstring | API base URL. Defaults to https://api.usequota.ai. |
| timeoutnumber | Per-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.
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: {}
// }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.
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. |
| balancenumber | The 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_balancenumber | Present 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_idstring | Present on developer and oauth_user shapes — the Quota user ID of the principal. |
| planstring | Optional plan identifier, when set on the wallet. |
| billing_modestring | Either "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.
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.
| packageIdstringrequired | Package id from getPackages(). |
| successUrlstringrequired | Absolute URL Stripe redirects to after a successful payment. |
| cancelUrlstringrequired | Absolute 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 urlReturns: 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(opts)
| payloadstring | ArrayBuffer | Uint8Arrayrequired | The raw request body, exactly as sent. Do not parse or re-stringify before verifying. |
| signaturestringrequired | Hex-encoded HMAC from the X-Quota-Signature request header. |
| secretstringrequired | The 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
balance and required (both in credits).retryAfter seconds (default 60) before retrying.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 } });
},
};