Skip to content
← Integration quickstart

@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

Animated upload, indexing, and cited search workflow

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

Pre-release: APIs may change. Pin the version you validate in production. Apache-2.0 licensed.

Primary sponsor

17th Street Labs logo

17th Street Labs is DocSlurp’s primary sponsor.