TypeScript SDK
@status302/sdk is the official TypeScript and JavaScript client. Its types are generated from the same public OpenAPI spec as the
API reference, and the package version is the spec version it was generated from.
npm install @status302/sdk # or pnpm add / yarn addIt needs Node.js 22 or newer (it uses the built-in fetch and WebSocket) and is published as ES modules with type declarations.
Client
Section titled “Client”import { Status302 } from "@status302/sdk";
const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY });| Option | Default | |
|---|---|---|
apiKey |
Your API key, sent as Authorization: Bearer. |
|
baseUrl |
https://api.status302.com |
Another environment’s API origin. |
maxRetries |
3 |
Retries after 429, 5xx (idempotent methods) and connection errors. |
maxRetryAfter |
60 |
The longest Retry-After in seconds the client waits out before throwing RateLimitError. |
timeoutMs |
30000 |
Timeout per attempt. |
fetch |
globalThis.fetch |
A custom fetch, for proxies or instrumentation. |
Requests
Section titled “Requests”Call operations by method and path. Paths, path and query parameters, bodies and responses are all typed from the spec, so your editor completes them and the compiler catches mistakes:
// The next five upcoming events in the catalog.import { Status302 } from "@status302/sdk";
const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY, baseUrl: process.env.STATUS302_API_URL });
const page = await status302.get("/v1/events", { query: { limit: 5 } });for (const event of page.items) { console.log(event.id, event.name, event.startsAt ?? "date TBA", event.venue?.city ?? "");}const metrics = await status302.get("/v1/events/{id}/metrics", { path: { id: 4102 } });const list = await status302.post("/v1/lists", { body: { name: "Tonight's arena shows" } });await status302.post("/v1/lists/{listId}/items", { path: { listId: list.id }, body: { eventIds: [4102] } });get, post, put, patch and delete return the parsed body. request(method, path, options) returns { data, status, headers, rateLimit } when you need the response details. Use Schema<"Name"> to name any schema of the spec:
import type { Schema } from "@status302/sdk";
function label(event: Schema<"EventSummary">): string { return `${event.name} · ${event.venue?.city ?? "TBA"}`;}Pagination
Section titled “Pagination”paginate yields every item of a cursor-paginated collection, fetching pages as needed; pages yields whole pages, so you can save
nextCursor and resume later.
// Every upcoming event, 200 per page. `paginate` follows nextCursor for you;// `pages` yields whole pages when you want to checkpoint the cursor.import { Status302 } from "@status302/sdk";
const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY, baseUrl: process.env.STATUS302_API_URL });
let total = 0;for await (const event of status302.paginate("/v1/events", { query: { limit: 200 } })) { total += 1; if (total <= 3) console.log(event.id, event.name);}console.log(`${total} events`);
for await (const page of status302.pages("/v1/events", { query: { limit: 200 } })) { console.log(`page of ${page.items.length}, next cursor: ${page.nextCursor ?? "none (last page)"}`);}Errors
Section titled “Errors”Non-2xx responses throw a subclass of ApiError carrying the problem (err.problem, err.type, err.status):
| Class | Status |
|---|---|
BadRequestError |
400 |
AuthenticationError |
401 |
PlanRequiredError |
402, with requiredPlan, currentPlan and feature (problem.limit for count limits) |
PermissionError |
403, with requiredScope for a missing scope |
NotFoundError |
404 |
ConflictError |
409 |
RateLimitError |
429, with retryAfter and rateLimit |
ServerError |
5xx |
Network failures throw ConnectionError. All of them extend Status302Error.
// Every non-2xx answer becomes a typed error carrying the problem+json body.import { AuthenticationError, NotFoundError, PermissionError, PlanRequiredError, RateLimitError, Status302,} from "@status302/sdk";
const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY, baseUrl: process.env.STATUS302_API_URL });
try { await status302.get("/v1/events/{id}", { path: { id: 999_999_999 } });} catch (err) { if (err instanceof NotFoundError) { console.log(`404 ${err.type}: ${err.problem.detail ?? err.problem.title}`); } else if (err instanceof PlanRequiredError) { console.log(`needs the ${err.requiredPlan} plan (you have ${err.currentPlan}) for ${err.feature}`); } else if (err instanceof PermissionError) { console.log(`forbidden: ${err.requiredScope ? `the key needs the ${err.requiredScope} scope` : err.message}`); } else if (err instanceof RateLimitError) { console.log(`quota exhausted, retry in ${err.retryAfter}s`); } else if (err instanceof AuthenticationError) { console.log("the API key is missing, revoked or expired"); } else { throw err; }}Rate limits and retries
Section titled “Rate limits and retries”Every response’s RateLimit-Policy and RateLimit headers are parsed into rateLimit (policy, remaining, reset, policies).
429 responses are retried for every method after Retry-After; 5xx responses and connection errors are retried with exponential
backoff for GET, PUT and DELETE only, so a failed POST is never sent twice behind your back. See rate limits.
// Reading the quota headers, and handling 429 without the SDK.import { setTimeout as sleep } from "node:timers/promises";import { Status302 } from "@status302/sdk";
const api = process.env.STATUS302_API_URL ?? "https://api.status302.com";const headers = { Authorization: `Bearer ${process.env.STATUS302_API_KEY}` };
// Plain fetch: retry a 429 after Retry-After seconds, a bounded number of times.async function getJSON(url: string, attempts = 5): Promise<unknown> { for (let attempt = 1; ; attempt++) { const res = await fetch(url, { headers }); console.log(`RateLimit-Policy: ${res.headers.get("ratelimit-policy")}`); console.log(`RateLimit: ${res.headers.get("ratelimit")}`); if (res.status === 429 && attempt < attempts) { const wait = Number(res.headers.get("retry-after") ?? "1"); console.log(`rate limited, retrying in ${wait}s`); await sleep(wait * 1000); continue; } if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }}await getJSON(`${api}/v1/events?limit=1`);
// The SDK retries 429s itself and parses the headers on every response.const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY, baseUrl: api });const res = await status302.request("get", "/v1/events", { query: { limit: 1 } });if (res.rateLimit) { const { policy, remaining, reset, policies } = res.rateLimit; console.log(`${remaining} requests left in the ${policy} window, resets in ${reset}s`); for (const p of policies) console.log(`quota ${p.name}: ${p.quota} per ${p.window}s`);}Stream
Section titled “Stream”status302.stream() opens the realtime stream and keeps it open: it reconnects with backoff, resubscribes every topic with
since set to the last seq it delivered, drops duplicates, and reports a resync with a helper that loads the snapshot.
// Your organization's alert firings, live. The client reconnects on its own,// resumes after the last seq it delivered, and drops duplicates.import { Status302, type Schema } from "@status302/sdk";
const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY, baseUrl: process.env.STATUS302_API_URL });const stream = status302.stream({ onStateChange: (state) => console.log(`stream ${state}`), onError: (err) => console.error(`stream error: ${err.message}`),});
const alerts = stream.subscribe<Schema<"AlertEvent">>("alerts", { onData: (msg) => console.log(`alert #${msg.seq} at ${msg.ts}: ${msg.data.title}`), // The gap could not be replayed: reload the REST snapshot, then continue live. onResync: async (msg) => { const snapshot = await alerts.fetchSnapshot(msg); console.log(`resynced ${msg.topic} (${msg.reason}) from ${msg.snapshot}`, snapshot); }, onError: (err) => console.error(`subscription refused: ${err.code}`),});
console.log(`subscribed to alerts at seq ${await alerts.ready}`);process.on("SIGINT", () => stream.close());subscribe(topic, { filter, since, onData, onResync, onError })returns aSubscription;await sub.readyresolves with the head seq,sub.unsubscribe()ends it.- On the server, the client sends your key on the upgrade. In a browser, pass
auth: "ticket"and agetTicketfunction that asks your backend for a ticket; it is called before every connection. onStateChangereportsconnecting,open,reconnectingandclosed. Close codes4401and4403are final and close the client;4429is reported throughonErrorand retried.CloseCodenames every code.- Coalesced topics (
event.{id}.kpis) report a resync after a reconnect instead of resuming, since they carry only the latest state.
// Browser: the page never sees the key.const stream = new Status302({ baseUrl: "https://api.status302.com" }).stream({ auth: "ticket", getTicket: async () => (await (await fetch("/api/status302-ticket", { method: "POST" })).json()).ticket,});Webhooks
Section titled “Webhooks”verifyWebhook(rawBody, signatureHeader, secret) checks a delivery’s signature in constant time
and its timestamp against a 5-minute tolerance, and returns the parsed envelope. Pass an array of secrets while rotating. It uses Web Crypto,
so it also runs in edge runtimes. It throws WebhookVerificationError for an invalid delivery.
// A webhook receiver on node:http. Verify the signature over the raw body// before parsing, answer 2xx fast, and process each event `id` once (retries// and redeliveries repeat it).import { createServer } from "node:http";import { verifyWebhook, WebhookVerificationError } from "@status302/sdk";
const secret = process.env.STATUS302_WEBHOOK_SECRET ?? "";const processed = new Set<string>(); // use your database in production
const server = createServer(async (req, res) => { if (req.method !== "POST" || req.url !== "/webhooks/status302") { res.writeHead(404).end(); return; } const chunks: Buffer[] = []; for await (const chunk of req) chunks.push(chunk as Buffer); const body = Buffer.concat(chunks);
try { const event = await verifyWebhook(body, req.headers["status302-signature"]?.toString(), secret); if (processed.has(event.id)) { console.log(`duplicate ${event.type} ${event.id} (delivery ${req.headers["status302-delivery-id"]})`); } else { processed.add(event.id); console.log(`verified ${event.type} ${event.id}${event.test ? " (test)" : ""}`); } res.writeHead(204).end(); } catch (err) { if (!(err instanceof WebhookVerificationError)) throw err; console.log(`rejected: ${err.message}`); res.writeHead(400).end(err.message); }});
server.listen(Number(process.env.PORT ?? 3000), () => console.log(`listening on :${process.env.PORT ?? 3000}`));signWebhook(body, secret) produces a valid header, for testing your receiver.

