TypeScript SDK
@vol/sdk is a small, dependency-free TypeScript client for the REST API.
It runs anywhere fetch and Web Streams exist: browsers, Node 18+, Cloudflare Workers, Deno and Bun.
It ships ESM, CJS and type declarations.
pnpm add @vol/sdknew VolClient(options)
Section titled “new VolClient(options)”interface VolClientOptions { baseUrl?: string; // API base WITHOUT "/v1". Default "/api" apiKey?: string; // sent as X-API-Key accessToken?: string | (() => string | null | undefined | Promise<string | null | undefined>); // Bearer fetch?: typeof fetch; // custom fetch (polyfills, retries, tests)}accessToken may be a function: it is awaited before every request, so refreshed Supabase sessions are
picked up automatically. If both apiKey and accessToken are set, both headers are sent.
Methods
Section titled “Methods”| Method | Endpoint | Returns |
|---|---|---|
health() |
GET /v1/health |
Health |
cases() |
GET /v1/cases |
CaseInfo[] |
chat(req) |
POST /v1/chat (stream:false) |
ChatResponse |
chatStream(req, signal?) |
POST /v1/chat (stream:true) |
AsyncGenerator<ChatStreamEvent> |
search(req) |
POST /v1/search |
SearchResponse |
graph.meta(case) |
GET /v1/graph/{case}/meta |
GraphMeta |
graph.positions(case) |
GET …/positions.bin |
Float32Array |
graph.links(case) |
GET …/links.bin |
Int32Array |
graph.labels(case) |
GET …/labels.json |
string[] |
graph.clusters(case) |
GET …/clusters.json |
ClustersFile |
graph.node(case, nodeIndex) |
GET …/nodes/{nodeIndex} |
NodeDetail |
graph.neighbors(case, nodeIndex, { hops?, limit? }) |
GET …/neighbors/{nodeIndex} |
Subgraph |
graph.subgraph(case, nodeIndices, { hops?, limit? }) |
POST …/subgraph |
Subgraph |
cluster(case, clusterId) |
GET /v1/clusters/{case}/{clusterId} |
AnswerCluster |
me() |
GET /v1/me |
{ id, email } |
keys.list() |
GET /v1/keys |
ApiKey[] |
keys.create(name) |
POST /v1/keys |
ApiKey & { key } |
keys.revoke(id) |
DELETE /v1/keys/{id} |
void |
docsAsk(question) |
POST /v1/docs/ask |
{ answer, sources } |
Streaming
Section titled “Streaming”type ChatStreamEvent = | { type: "delta"; text: string } | { type: "citations"; citations: Citation[]; clusters: AnswerCluster[] } | { type: "done"; sessionId: string } | { type: "error"; message: string };const ac = new AbortController();for await (const ev of vol.chatStream({ case: "papers", question: "…" }, ac.signal)) { if (ev.type === "delta") process.stdout.write(ev.text);}The parser accepts \n and \r\n line endings, ignores : comment lines and flushes a final event even
without a trailing blank line. It is exported as parseSseBlock(block: string): ChatStreamEvent | null
for custom transports.
An error event is yielded, not thrown — decide in your loop whether to throw. HTTP errors before the
stream starts are thrown as VolApiError.
Errors
Section titled “Errors”class VolApiError extends Error { status: number; // HTTP status code: string; // error.code, or "http_error" message: string;}All request and response types are exported: CaseId, CaseInfo, Citation, AnswerCluster,
ChatRequest, ChatResponse, ChatStreamEvent, SearchRequest, SearchResponse, GraphMeta,
ClusterInfo, ClustersFile, NodeDetail, SubgraphNode, Subgraph, ApiKey, Health, Vec3.
Recipes
Section titled “Recipes”Server-side (Node, Workers)
const vol = new VolClient({ baseUrl: process.env.VOL_API_BASE, apiKey: process.env.VOL_API_KEY });Retries with a custom fetch
const retrying: typeof fetch = async (input, init) => { for (let i = 0; ; i++) { const res = await fetch(input, init); if (res.status !== 429 && res.status < 500) return res; if (i === 3) return res; await new Promise((r) => setTimeout(r, 2 ** i * 500)); }};const vol = new VolClient({ apiKey, fetch: retrying });Testing — pass a mock fetch that returns new Response(JSON.stringify(...)); the SDK’s own test
suite (packages/sdk/test) is a good template.