> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Install ndi-sdk and run parse, extract, ground, and workspace search

The published client is [`ndi-sdk`](https://www.npmjs.com/package/ndi-sdk) on npm. It covers the same `/v1` surface as this site. The Python package is separate (`pip install ndi-sdk`).

Requires Node 20+ (or any runtime with native `fetch`, `FormData`, and `Blob`: browsers, Deno, Bun, edge). Zero runtime dependencies. Dual ESM + CommonJS.

```bash theme={"dark"}
npm install ndi-sdk
```

Mint a key on the API keys page in the NDI console, then:

```bash theme={"dark"}
export NDI_API_KEY="ndi_sk_..."
# Defaults to production. Set this only for another host:
# export NDI_BASE_URL="http://localhost:8003"
```

`new NdiClient()` reads `$NDI_API_KEY` and `$NDI_BASE_URL`. You can also pass `{ api_key, base_url }`.

There is one async client — `fetch` is async-only. `close()` is a no-op for the default fetch. If you pass your own `fetch`, you own its lifetime.

***

## What you can do

| Job                                  | Call                                                        |
| ------------------------------------ | ----------------------------------------------------------- |
| Parse a file into markdown           | `client.documents.parse`                                    |
| Extract JSON against a schema        | `client.documents.extract`                                  |
| Split a packet / classify a document | `client.documents.split` / `.classify`                      |
| Pin quoted text to a location        | `client.documents.ground`                                   |
| Build a searchable corpus            | `workspaces` + `files` + `ingestion` + `tools.hybridSearch` |

One-shot calls on `client.documents` do not write to a workspace. Uploads for those calls are single-use: the bytes are dropped when the job that reads them finishes.

***

## Parse a local file

```ts theme={"dark"}
import { NdiClient } from "ndi-sdk";

const client = new NdiClient();
const upload = await client.documents.createUpload("report.pdf");
const job = await client.documents.parse(upload, {
  wait_seconds: 30,
});
console.log(job.result && "markdown" in job.result ? job.result.markdown : job.result);
```

`wait_seconds` holds the HTTP response open when the work finishes quickly. For everything else, poll:

```ts theme={"dark"}
const done = await client.jobs.wait(job.job_id, { timeout: 600 });
```

***

## Extract structured JSON

Pass a JSON Schema. Extract from an upload, or from a prior parse with `{ type: "parse_result", job_id }` so the parse is not paid for twice. `not_found` on a field is an answer about the document, not an error.

```ts theme={"dark"}
import { NdiClient } from "ndi-sdk";

const schema = {
  type: "object",
  properties: {
    invoice_number: { type: "string" },
    total: { type: "number" },
  },
  required: ["invoice_number", "total"],
};

const client = new NdiClient();
const upload = await client.documents.createUpload("invoice.pdf");
const parsed = await client.jobs.wait(
  (await client.documents.parse(upload, { wait_seconds: 30 })).job_id,
);
const job = await client.jobs.wait(
  (
    await client.documents.extract(
      { type: "parse_result", job_id: parsed.job_id },
      { json_schema: schema, wait_seconds: 30 },
    )
  ).job_id,
);
console.log(job.result && "data" in job.result ? job.result.data : job.result);
```

***

## Split and classify

`split` cuts a scanned packet into logical documents. `classify` labels one document against classes you define. Classify reads the original file, so it rejects a parse-result source.

```ts theme={"dark"}
import { NdiClient } from "ndi-sdk";

const client = new NdiClient();
const packet = await client.documents.createUpload("packet.pdf");
await client.documents.split(packet, {
  classes: [
    { id: "invoice", label: "Invoice", description: "A supplier invoice" },
    { id: "receipt", label: "Receipt", description: "A payment receipt" },
  ],
  wait_seconds: 30,
});

const document = await client.documents.createUpload("invoice.pdf");
await client.documents.classify(document, {
  classes: [{ id: "invoice", label: "Invoice", description: "A supplier invoice" }],
  wait_seconds: 30,
});
```

***

## Ground quoted text

Ground pins a quote back to a location in the source.

```ts theme={"dark"}
import { NdiClient } from "ndi-sdk";

const client = new NdiClient();
const upload = await client.documents.createUpload("report.pdf");
const job = await client.jobs.wait(
  (
    await client.documents.ground(upload, {
      targets: [{ id: "total", text: "1,200.50" }],
      wait_seconds: 30,
    })
  ).job_id,
);

if (job.result && "targets" in job.result) {
  for (const target of job.result.targets) {
    for (const match of target.matches ?? []) {
      console.log(target.id, match.matched_text, match.location);
    }
  }
}
```

***

## Search a workspace

A workspace is a durable corpus: upload files, ingest them once, then search and ask questions repeatedly. Uploading does not make a file searchable — ingestion does.

```ts theme={"dark"}
import { NdiClient } from "ndi-sdk";

const client = new NdiClient();
const workspace = await client.workspaces.create({ name: "fy25-audit" });
await client.jobs.wait(
  (
    await client.files.upload(workspace.workspace_id, "report.pdf", {
      path: "reports/report.pdf",
    })
  ).job_id,
);

const ingestion = await client.ingestion.ingest(workspace.workspace_id, {
  path_prefix: "reports/",
});
await client.jobs.wait(ingestion.job_id, { timeout: 600 });

const hits = await client.tools.hybridSearch(workspace.workspace_id, {
  query: "total liabilities at year end",
  k: 5,
});
for (const hit of hits.hits) {
  console.log(hit.path, hit.snippet);
}

const answer = await client.tools.qaFile(workspace.workspace_id, {
  path: "reports/report.pdf",
  query: "What were total liabilities at year end?",
});
console.log(answer.answer);
```

For a single file, `client.uploadAndIngest(...)` collapses the upload and ingest calls. Across several spreadsheets, `client.tools.queryTables(...)` answers one question over the set.

***

## Next steps

* [Authentication](/authentication) — minting and rotating keys
* [Python SDK](/sdks/python)
* [MCP](/sdks/mcp) — use NDI from Claude Code, Cursor, or opencode
* [Document operations](/api-reference/document-operations)
* [Workspaces](/concepts/workspaces)
* [Search tools](/api-reference/search-tools)
* [File tools](/api-reference/file-tools)
* [Jobs and idempotency](/concepts/jobs-idempotency)
