SDKs
Quota provides two JavaScript/TypeScript SDK packages: @usequota/core for any runtime, and @usequota/nextjs for Next.js-specific features like React hooks and route handlers.
Core SDK (@usequota/core)
The core SDK is a framework-agnostic client that works in any JavaScript or TypeScript environment: Node.js, Deno, Bun, Cloudflare Workers, React Native, edge functions, and more. It provides:
QuotaClient— typed HTTP client for the Quota API (balance, billing, user management, packages, OAuth)- Typed error classes for structured error handling
- SSE streaming parser for chat completions
- Web Crypto webhook signature verification (works in edge runtimes)
- Token storage interface with an in-memory implementation
Installation
npm install @usequota/coreUsage
import { QuotaClient } from "@usequota/core";
const quota = new QuotaClient({
apiKey: process.env.QUOTA_API_KEY,
baseUrl: "https://api.usequota.ai", // optional, this is the default
});
// Check balance
const balance = await quota.getBalance();
// Make a chat completion (OpenAI-compatible)
const response = await quota.createChatCompletion({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello!" }],
});
// Verify webhook signatures (Web Crypto, works on edge)
import { verifyWebhookSignature } from "@usequota/core";
const isValid = await verifyWebhookSignature({
payload: rawBody,
signature: request.headers.get("X-Quota-Signature")!,
secret: process.env.QUOTA_WEBHOOK_SECRET!,
});If you are building a Next.js app, use @usequota/nextjs instead — it re-exports everything from core plus adds React-specific features.
Next.js SDK (@usequota/nextjs)
The @usequota/nextjs package builds on the core SDK and adds middleware, React hooks, server utilities, and pre-built components for integrating Quota into Next.js apps.
Installation
npm install @usequota/nextjsQuick Start
Three pieces: middleware to handle OAuth callbacks, a provider for React context, and hooks to access user data.
1. Middleware
Create middleware.ts at your project root:
import { createQuotaMiddleware } from "@usequota/nextjs";
export default createQuotaMiddleware({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
baseUrl: "https://api.usequota.ai",
callbackPath: "/api/quota/callback", // default
storageMode: "client", // or "hosted"
});
export const config = { matcher: ["/api/quota/callback"] };2. Provider
Wrap your app in QuotaProvider:
import { QuotaProvider } from "@usequota/nextjs";
export default function Layout({ children }) {
return (
<QuotaProvider
clientId={process.env.NEXT_PUBLIC_QUOTA_CLIENT_ID!}
baseUrl="https://api.usequota.ai"
>
{children}
</QuotaProvider>
);
}3. Hooks
import { useQuotaAuth, useQuotaBalance } from "@usequota/nextjs";
function MyComponent() {
const { isAuthenticated, login, logout } = useQuotaAuth();
const { balance, refetch } = useQuotaBalance();
if (!isAuthenticated) return <button onClick={login}>Connect Wallet</button>;
return (
<div>
<p>Balance: ${balance}</p>
<button onClick={logout}>Disconnect</button>
</div>
);
}Storage Modes
Client Mode (default)
OAuth tokens are stored in httpOnly cookies on the user's browser. Your app reads them to make API calls.
createQuotaMiddleware({
clientId: "...",
clientSecret: "...",
storageMode: "client",
});Hosted Mode
Tokens are stored server-side by Quota. Your app receives an external_user_id and sends it via the X-Quota-User header. This avoids handling tokens directly.
createQuotaMiddleware({
clientId: "...",
clientSecret: "...",
storageMode: "hosted",
getExternalUserId: async (request) => {
// Return your app's user ID for the current request
const session = await getSession(request);
return session.userId;
},
});See Hosted Users API for how the server-side flow works.
Hooks
useQuota()
Access the full Quota context including user, loading state, error, login, logout, and refetch.
useQuotaUser()
Returns the current QuotaUser or null if not authenticated.
useQuotaAuth()
const {
isAuthenticated, // boolean
isLoading, // boolean
login, // () => void — redirects to OAuth
logout, // () => Promise<void> — clears cookies
} = useQuotaAuth();useQuotaBalance()
const {
balance, // number | null (in micro-dollars; divide by 1,000,000 for display)
isLoading, // boolean
error, // Error | null
refetch, // () => Promise<void>
} = useQuotaBalance();useQuotaPackages()
Fetches available purchase packages from the public /v1/packages endpoint. No authentication is required. Results are cached for the component lifetime — the hook auto-fetches on first render and subsequent calls return cached data.
const {
packages, // PurchasePackage[]
isLoading, // boolean
error, // Error | null
refetch, // () => Promise<void>
} = useQuotaPackages();Components
QuotaConnectButton
Pre-styled OAuth login button with loading states.
import { QuotaConnectButton } from "@usequota/nextjs";
<QuotaConnectButton
variant="primary" // "primary" | "secondary" | "ghost"
onSuccess={() => {}}
onError={(err) => {}}
showWhenConnected={false} // hide after connect
/>QuotaBalance
Displays the user's dollar balance.
<QuotaBalance format="dollars" showRefresh />QuotaBuyCredits
Button that opens a Stripe checkout session for purchasing balance.
<QuotaBuyCredits packageId="basic" checkoutPath="/api/quota/checkout" />QuotaUserMenu
Dropdown menu showing avatar, balance, and actions (add funds, disconnect). Supports keyboard navigation and ARIA.
<QuotaUserMenu showBuyCredits />Server Utilities
Use these in Server Components, Route Handlers, or Server Actions.
getQuotaUser
import { getQuotaUser } from "@usequota/nextjs";
const user = await getQuotaUser({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
storageMode: "client", // or "hosted"
});
// Returns QuotaUser | nullrequireQuotaAuth
Same as getQuotaUser but redirects to / if not authenticated.
getQuotaPackages
import { getQuotaPackages } from "@usequota/nextjs";
const packages = await getQuotaPackages();
// [{ id: "starter", name: "Starter", price_cents: 500, balance_display: "$4.05" }]createQuotaCheckout
import { createQuotaCheckout } from "@usequota/nextjs";
const checkoutUrl = await createQuotaCheckout({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
packageId: "basic-500",
successUrl: "https://yourapp.com/success",
cancelUrl: "https://yourapp.com/pricing",
});clearQuotaAuth
Deletes auth cookies (access token, refresh token, external user ID).
Webhook Utilities
createWebhookHandler
Returns a Next.js Route Handler that verifies signatures and routes events to your handler functions.
// app/api/webhooks/quota/route.ts
import { createWebhookHandler } from "@usequota/nextjs";
export const POST = createWebhookHandler(
process.env.QUOTA_WEBHOOK_SECRET!,
{
"balance.low": async (event) => {
// Send notification to user
},
"user.connected": async (event) => {
// Update your database
},
"usage.completed": async (event) => {
// Log usage analytics
},
}
);verifyWebhookSignature
Low-level helper if you need custom verification logic:
import { verifyWebhookSignature } from "@usequota/nextjs";
const isValid = verifyWebhookSignature({
payload: rawBody,
signature: request.headers.get("X-Quota-Signature")!,
secret: process.env.QUOTA_WEBHOOK_SECRET!,
});parseWebhook
Reads the request body, verifies the signature, and returns the typed event object. Throws on invalid signatures.
import { parseWebhook } from "@usequota/nextjs";
const event = await parseWebhook(request, process.env.QUOTA_WEBHOOK_SECRET!);
// event.type, event.data, event.id, event.created_atRoute Handler Factory
createQuotaRouteHandlers generates all six API route handlers your app needs in one call. No more writing boilerplate for each route.
Before: 6 separate route files
// app/api/quota/authorize/route.ts — custom OAuth redirect logic
// app/api/quota/callback/route.ts — token exchange + cookie setting
// app/api/quota/status/route.ts — read cookies, fetch user, refresh tokens
// app/api/quota/packages/route.ts — proxy to Quota API
// app/api/quota/checkout/route.ts — parse body, create Stripe session
// app/api/quota/disconnect/route.ts — clear cookiesAfter: one config + thin route exports
// lib/quota.ts
import { createQuotaRouteHandlers } from "@usequota/nextjs/server";
export const {
authorize,
callback,
status,
packages,
checkout,
disconnect,
} = createQuotaRouteHandlers({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
// All other options are optional with sensible defaults
});Then create thin route files that re-export the handlers:
// app/api/quota/authorize/route.ts
export { authorize as GET } from "@/lib/quota";
// app/api/quota/callback/route.ts
export { callback as GET } from "@/lib/quota";
// app/api/quota/status/route.ts
export { status as GET } from "@/lib/quota";
// app/api/quota/packages/route.ts
export { packages as GET } from "@/lib/quota";
// app/api/quota/checkout/route.ts
export { checkout as POST } from "@/lib/quota";
// app/api/quota/disconnect/route.ts
export { disconnect as POST } from "@/lib/quota";Config reference
| Option | Type | Default |
|---|---|---|
clientId | string | required |
clientSecret | string | required |
baseUrl | string | https://api.usequota.ai |
storageMode | "client" | "hosted" | client |
cookiePrefix | string | quota |
cookieMaxAge | number | 604800 (7 days) |
callbackPath | string | /api/quota/callback |
successRedirect | string | / |
errorRedirect | string | / |
tokenStorage | QuotaTokenStorage | - |
getExternalUserId | function | required for hosted mode |
callbacks.onConnect | function | - |
callbacks.onDisconnect | function | - |
Returned handlers
| Handler | Method | Purpose |
|---|---|---|
authorize | GET | Initiates the OAuth flow |
callback | GET | Handles OAuth callback, exchanges code for tokens |
status | GET | Returns connection status and balance |
packages | GET | Returns available purchase packages |
checkout | POST | Creates a Stripe checkout session |
disconnect | POST | Disconnects the user's Quota account |
withQuotaAuth
Wraps an API route handler with automatic Quota authentication. It reads the access token, fetches the user, refreshes expired tokens, and returns structured error responses for common failure modes.
// app/api/summarize/route.ts
import { withQuotaAuth } from "@usequota/nextjs/server";
export const POST = withQuotaAuth(
{
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
},
async (request, { user, accessToken }) => {
// user is guaranteed to exist here
// accessToken can be used to proxy requests through Quota
const body = await request.json();
const response = await fetch(
"https://api.usequota.ai/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: body.messages,
}),
}
);
return Response.json(await response.json());
}
);What the wrapper does
- Reads the access token from cookies (or tokenStorage adapter)
- Fetches the user from
GET /v1/me - If the token is expired, refreshes it automatically
- Passes
{ user, accessToken }to your handler - Catches any
QuotaErrorthrown inside your handler and returns the correct HTTP status and JSON body
Handler context
| Property | Type | Description |
|---|---|---|
user | QuotaUser | The authenticated user (guaranteed non-null) |
accessToken | string | OAuth access token (empty string in hosted mode) |
Typed Errors
The SDK exports typed error classes that let you handle specific failure modes with instanceof checks. Import from @usequota/nextjs/server.
Error classes
| Class | Code | Status | Extra properties |
|---|---|---|---|
QuotaError | (varies) | (varies) | code, statusCode, hint |
QuotaInsufficientCreditsError | insufficient_credits | 402 | balance, required |
QuotaNotConnectedError | not_connected | 401 | — |
QuotaTokenExpiredError | token_expired | 401 | — |
QuotaRateLimitError | rate_limit_exceeded | 429 | retryAfter (seconds) |
Catching errors
import {
QuotaInsufficientCreditsError,
QuotaNotConnectedError,
QuotaRateLimitError,
QuotaError,
} from "@usequota/nextjs/server";
try {
const response = await fetch("https://api.usequota.ai/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ model: "gpt-4o-mini", messages }),
});
if (!response.ok) {
// errorFromResponse() maps HTTP responses to typed errors
const { errorFromResponse } = await import("@usequota/nextjs/server");
const error = await errorFromResponse(response);
if (error) throw error;
}
} catch (e) {
if (e instanceof QuotaInsufficientCreditsError) {
// e.balance — current balance (if available)
// e.required — credits needed (if available)
showBuyCreditsDialog();
}
if (e instanceof QuotaNotConnectedError) {
redirectToLogin();
}
if (e instanceof QuotaRateLimitError) {
// e.retryAfter — seconds to wait
await new Promise(r => setTimeout(r, e.retryAfter * 1000));
retry();
}
if (e instanceof QuotaError) {
// e.code, e.statusCode, e.hint
console.error(`Quota error [${e.code}]: ${e.message}`);
}
}Configuration Reference
QuotaConfig (Middleware)
| Option | Type | Default | Description |
|---|---|---|---|
clientId | string | required | OAuth client ID |
clientSecret | string | required | OAuth client secret |
baseUrl | string | https://api.usequota.ai | Quota API base URL |
callbackPath | string | /api/quota/callback | OAuth callback route |
storageMode | "client" | "hosted" | client | Where tokens are stored |
getExternalUserId | function | - | Required for hosted mode |
cookie.prefix | string | quota | Cookie name prefix |
cookie.maxAge | number | 604800 | Cookie max age in seconds (7 days) |
QuotaProviderProps
| Prop | Type | Default | Description |
|---|---|---|---|
clientId | string | required | OAuth client ID (use NEXT_PUBLIC_ env var) |
baseUrl | string | https://api.usequota.ai | Quota API base URL |
callbackPath | string | /api/quota/callback | OAuth callback route |
apiPath | string | /api/quota/me | User info API route |
fetchStrategy | "eager" | "lazy" | eager | When to fetch user data |
fetchStrategy
Controls when QuotaProvider fetches user data. The default "eager" strategy fetches on mount, which is ideal when you need the user state immediately. Set to "lazy" to defer the fetch until refetch() is called manually. This is useful when you want to avoid unnecessary network requests on pages where user data may not be needed right away.
// Eager (default): fetches user data on mount
<QuotaProvider clientId="..." fetchStrategy="eager">
// Lazy: does NOT fetch on mount; call refetch() when needed
<QuotaProvider clientId="..." fetchStrategy="lazy">