@docslurp/sdk
Turn documents into cited search results. Keep your own application and LLM.
Upload a PDF, Office file, image, or recording. DocSlurp processes it in a workspace; your Node.js application retrieves passages with document and page citations, or asks a grounded question. The SDK has no runtime npm dependencies and includes TypeScript declarations.
Quickstart · Recipes · Reference · Failure handling · CLI
Five-second workflow illustration; sample data and compressed timing. Watch the 20-second walkthrough.
From file to search results
Requires Node.js 22+ and ESM (.mjs, or "type": "module"). TypeScript can use the same API. Create a server API key with ingest and search scopes.
npm install @docslurp/sdk
export DOCSLURP_API_KEY='your-server-api-key'
Save as search.mjs, put handbook.pdf next to it, and run node search.mjs:
import { createReadStream } from "node:fs";
import { createDocSlurpClient } from "@docslurp/sdk";
const client = createDocSlurpClient();
const { workspace, run, warnings } = await client.upload(
createReadStream("handbook.pdf"),
);
// Persist these IDs so a restarted process can resume checking the same run.
console.log({ workspaceId: workspace.id, runId: run.id });
for (const warning of warnings ?? []) console.error(warning);
await client.waitForRun(run.id, { timeoutMs: 600_000 });
for await (const hit of client.search({
workspaceId: workspace.id,
q: "What is the parental leave policy?",
limit: 5,
})) {
console.log(hit.text);
console.log(hit.citation.filename, hit.citation.pageNumbers);
}
Upload acceptance is asynchronous. upload() returns workspace, run, and document records before indexing completes. waitForRun() checks that run; availability describes the searchable corpus, which can still include other pending uploads. An empty result set on a partially indexed workspace is not evidence that the answer is absent.
With no workspace configured, an upload creates one automatically. It does not change the client’s default workspace: pass the returned ID to search, as above. To append to a corpus, pass workspaceId or set DOCSLURP_WORKSPACE_ID.
Download the runnable quickstart. The examples/ directory also ships in the npm package.
Integration recipes
client.search() returns an async iterable directly. Use for await (const hit of client.search(input)) for hits, or await client.search(input) for the full response. A saved search can be iterated and awaited without repeating the request. Iteration starts yielding after the JSON response arrives; it is not server-streamed or automatically paginated. Request cancellation and timeouts still apply.
Inspect retrieval usage
const search = client.search({
workspaceId: workspace.id,
q: "How are urgent incidents escalated?",
});
for await (const hit of search) {
console.log(hit.text, hit.citation);
}
const { usage } = await search; // Same request
console.log(usage.totalTokens, usage.costCents);
usage includes input/output/total tokens and fractional US cents. costSource is provider, estimate, mixed, unknown, or none (no recorded AI calls). Costs cover retrieval AI only, not ingestion, storage, your own model, or a bill. Batch results each include their own usage. Native chat’s usage covers answer generation.
Bring evidence to your existing LLM
search() retrieves passages; it does not generate an answer. Use the formatter with any model adapter, or keep the structured results for your own citation UI.
import { createDocSlurpClient, formatSearchContext } from "@docslurp/sdk";
// Set DOCSLURP_WORKSPACE_ID to the ID returned by your upload.
const client = createDocSlurpClient();
const evidence = await client.search({ q: "How are urgent incidents escalated?" });
if (evidence.availability?.status !== "complete") {
throw new Error("Wait for the corpus to finish indexing before answering.");
}
if (!evidence.results.length) throw new Error("No supporting evidence found.");
const context = formatSearchContext(evidence);
console.log(context); // [1] filename · pages … · document …, followed by the passage
Keep evidence.results to map [1] back to the first citation, and evidence.sessionId for diagnostics. Tell your model to cite the numbered passages, admit insufficient evidence, and treat source text as untrusted data. Runnable context adapter.
Ask a grounded question, then follow up
const first = await client.chat({
workspaceId: workspace.id,
q: "What does the handbook say about parental leave?",
});
console.log(first.answer, first.citations, first.availability);
console.log(first.usage, first.runtime);
const followup = await client.chat({
workspaceId: workspace.id,
sessionId: first.sessionId,
q: "Which exceptions apply?",
});
console.log(followup.answer);
This continues the quickstart. Keep follow-ups sequential within a session. Omit sessionId for a new conversation. Search supports both async iteration and awaiting the full response; chat returns a promise; use the compatible chat API when you need streamed answer tokens.
Upload several files without reading them all into RAM
import { openAsBlob } from "node:fs";
const files = await Promise.all(["handbook.pdf", "benefits.pdf"].map(async (filename) => ({
data: await openAsBlob(filename),
filename,
})));
const accepted = await client.upload({
files,
workspaceId: workspace.id,
// Persist once per logical upload job. Reuse only for the same payload.
idempotencyKey: "handbook-import-2026-10-v1",
});
await client.waitForRun(accepted.run.id);
A single createReadStream() uses backpressure and is sent once; its bytes cannot be replayed. File-backed blobs support replayable multipart requests. Keep their backing files unchanged until the upload finishes. For an unnamed Blob or generic async iterable, supply { filename: "handbook.pdf" } as the second argument.
Ingest public URLs
const accepted = await client.ingestUrls({
urls: ["https://your-public-host.example/handbook.pdf"], // Replace with your URL.
workspaceId: workspace.id,
});
await client.waitForRun(accepted.run.id);
URLs must be reachable by the service. URL ingestion is not automatically retried. Local paths belong in upload().
Search several questions in one request
const batch = await client.searchMany([
{ workspaceId: workspace.id, q: "leave eligibility" },
{ workspaceId: workspace.id, q: "notice requirements", filenameContains: "handbook" },
], { concurrency: 2 }, { signal: AbortSignal.timeout(30_000) });
for (const [index, result] of batch.results.entries()) {
if (result.ok) console.log(index, result.data.results, result.data.availability);
else console.error(index, result.error.status, result.error.message);
}
Accepts 1–20 queries, with 1–4 concurrent searches (default 4). Results stay in input order. A successful batch response can contain failed queries: check each ok. Every query can incur retrieval charges; batching does not make them free.
Watch a run with a polling fallback
for await (const event of client.streamRun(run.id)) {
console.log(event.event, event.data);
const state = event.data.run;
if (state && (state.status === "completed" || state.status === "failed" ||
["paused", "cancelled", "dead-letter"].includes(state.controlState ?? ""))) break;
}
await client.waitForRun(run.id); // Confirms success; throws for a stopped/failed run.
Events are authenticated SSE snapshots and run/stage/document events. Some events have no data.run. Breaking iteration closes the connection. Streams have a five-minute default lifetime and do not reconnect automatically. Use the complete fallback example to recover from interrupted streams within one shared deadline.
Configuration
const client = createDocSlurpClient({
apiKey: process.env.DOCSLURP_API_KEY,
apiUrl: "https://docslurp.io", // Service origin; do not append /v1.
workspaceId: process.env.DOCSLURP_WORKSPACE_ID,
timeoutMs: 30_000,
});
| Setting | Resolution / default |
|---|---|
| API key | Explicit apiKey → server DOCSLURP_API_KEY |
| Service origin | Explicit apiUrl → DOCSLURP_URL → https://docslurp.io |
| Workspace | Per-call workspaceId → client option → DOCSLURP_WORKSPACE_ID |
| Request deadline | Per-call timeoutMs → client option → 30 seconds |
| Run wait / stream lifetime | Per-call timeoutMs → 5 minutes; independent of client request default |
| Polling interval | pollIntervalMs → 1 second |
| HTTP transport | Optional fetch implementation; defaults to native fetch |
Environment values are read when the client is created, on the server only. An empty upload workspaceId: "" creates a new workspace even when a default is configured. Search and chat require a nonempty workspace.
Browser boundary: never embed a server key in public code. Proxy through your backend. Within an authenticated DocSlurp dashboard session, createDocSlurpClient({ apiUrl: "" }) uses same-origin cookies; browser clients do not read environment variables. On the server, apiKey: "" disables the environment key explicitly. Browser File objects can be passed directly to upload().
Method reference
| Method | Resolves / yields | Key scope |
|---|---|---|
upload(fileOrStream, options?) |
{ workspace, run, documents, warnings? } |
ingest |
upload({ files, ...metadata }, options?) |
Same accepted ingestion response | ingest |
ingestUrls({ urls, ...metadata }, request?) |
Same accepted ingestion response | ingest |
getRun(runId, request?) |
Run record (not the progress envelope) | Any of ingest, search, artifacts |
waitForRun(runId, options?) |
Completed run, or throws | Same as getRun |
streamRun(runId, options?) |
Async iterable of { event, id?, data } |
Same as getRun |
search(input, request?) |
Async iterable of hits; await for { results, availability, sessionId, usage } |
search |
searchMany(inputs, batch?, request?) |
{ results: [{ ok, data? , error? }] } |
search |
chat({ q, workspaceId?, sessionId? }, request?) |
Answer, citations, availability, session IDs, usage, runtime | search |
getDocument(documentId, request?) |
Document viewer envelope; see version note below | artifacts |
formatSearchContext(searchResponse) |
Numbered source excerpts as a string | No request |
SearchInput: required q; optional workspaceId, limit, documentId, filenameContains, tags (string). Upload metadata: workspaceId, workspaceName, orgId, templateKey, idempotencyKey. URL ingestion accepts the same metadata except idempotencyKey. Template keys: engineering-docs, legal-contracts, support-knowledge-base, financial-documents, media-archive.
Citations include documentId, filename, pageNumbers, headingPath, chunkIndex, blockIds, and source. Some formats have no meaningful page numbers; do not invent a page when the list is empty. score is a retrieval score, not a probability of correctness. Availability is empty, pending, partial, or complete (or null when unavailable).
The SDK exports input/response types, DocSlurpClient, and all error classes. It uses ESM exports; CommonJS applications can use await import("@docslurp/sdk") inside an async function.
Version 0.1.2: document reads
The server returns a viewer envelope from GET /v1/documents/:id, with the document record nested under document. The SDK currently declares getDocument() as returning a flat DocumentRecord; that declaration does not match the wire response. Do not assume filename or id is at the top level. Until the return type is corrected, validate the envelope before accessing its nested record. Upload and search response types are unaffected.
Timeouts, cancellation, and errors
All network methods accept { signal, timeoutMs }; searchMany takes these as its third argument. A timeout or abort stops your local request/wait, not the server’s processing run. Resume with the stored run ID instead of blindly uploading again.
import { DocSlurpApiError, DocSlurpRunError, DocSlurpTimeoutError } from "@docslurp/sdk";
try {
await client.waitForRun(run.id, { timeoutMs: 600_000 });
} catch (error) {
if (error instanceof DocSlurpRunError) {
console.error("Run needs attention", error.run);
} else if (error instanceof DocSlurpTimeoutError) {
console.error("Check this run again later", error.run?.id ?? run.id);
} else if (error instanceof DocSlurpApiError) {
console.error("HTTP error", error.status, error.body);
}
throw error; // Let the caller/job runner decide how to recover.
}
| Operation | Automatic retries |
|---|---|
getRun, getDocument |
Up to 2 retries for HTTP 429, 502, 503, 504 |
Replayable Blob/File upload with idempotencyKey |
Same bounded retry policy |
| Stream upload, upload without a key, URL ingestion | None |
| Search, batch search, chat, SSE connection | None |
Retry delays respect Retry-After, capped at five seconds, within the original request deadline. Network errors are not automatically retried. Aborting with a signal preserves its reason. waitForRun throws for failed runs and paused, cancelled, or dead-letter control states.
Next steps
- CLI recipes and command reference
- HTTP integration and troubleshooting
- Search API and retrieval controls
- Runnable examples
Pre-release: APIs may change. Pin the version you validate in production. Apache-2.0 licensed.
Primary sponsor
17th Street Labs is DocSlurp’s primary sponsor.
