Skip to content

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.

Terminal window
pnpm add @vol/sdk
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.

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 }
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.

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.

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.