Skip to content

NodeJS SDK

The official TypeScript NodeJS SDK for InCheck. One client over the proprietary document-grounding engine plus the EMS knowledge layer — typed responses and streaming. It is not RAG; it is our IP, and it is materially more accurate than generic retrieval pipelines.

npm install @incheckai/sdk
# or
pnpm add @incheckai/sdk
# or
yarn add @incheckai/sdk
# or
bun add @incheckai/sdk

Requires Node.js 18+.

Configure

export INCHECK_API_KEY="incheck_prod_..."

# Pick an environment (production is the default):
export INCHECK_ENVIRONMENT="staging"        # api-acceptance.incheck.ai
# or override fully:
# export INCHECK_BASE_URL="https://my-internal-proxy.example/incheck"

Resolution priority for the base URL (high → low):

  1. baseUrl passed to new Client(...)
  2. INCHECK_BASE_URL env var
  3. environment passed to new Client(...)
  4. INCHECK_ENVIRONMENT env var
  5. Default https://api.incheck.ai

EMS mode — no setup, just chat

import { Client } from "@incheckai/sdk";

const client = new Client();

const meta = await client.metadata.statesAndScopes();
const reply = await client.chat.send(
  "Adult dose of atropine for symptomatic bradycardia?",
  {
    scope: meta.default_scope,
    state: meta.default_state
  }
);

console.log(reply.content);

That's it — no orgId, no Pod, no onboarding. The model answers from general EMS knowledge under the scope/state you specify. If you already know the values, you can skip the metadata lookup and pass literals (scope: "ALS", state: "California-LAC") directly.

Discovering valid scope / state

The accepted enumerations are owned by the gateway and can change without an SDK release. Fetch them through client.metadata instead of hard-coding values:

const meta = await client.metadata.statesAndScopes();

console.log(meta.default_state, meta.default_scope);
// California-LAC ALS

// All accepted state and scope values, with display labels:
for (const s of meta.states) {
  console.log(s.value, "—", s.label);
}

// Which scopes apply for a given state (falls back to "_default"):
const allowed =
  meta.scopes_by_state[meta.default_state] ?? meta.scopes_by_state["_default"];
console.log("scopes for", meta.default_state, ":", allowed);

Only the value on each entry is part of the wire contract — pass that on chat.send / chat.stream. label is for UI display only.

Unified mode — chat with your Pod

Onboard one or more documents into a Pod (one Pod per orgId), then chat against it.

import { Client } from "@incheckai/sdk";

const client = new Client();

const namespace = (await client.documents.listOrgs()).filtered_by;
const orgId = `${namespace}_dispatch`;

// Onboard the Pod — initiate → upload → complete → poll, in one call.
const status = await client.documents.upload(
  orgId,
  ["./dispatch_sop.pdf", "./policies.docx"],
  { wait: true }
);

console.log("processed:", status.progress?.processed_pages, "pages");

// Chat against the Pod
const reply = await client.chat.send("What's our hazmat escalation policy?", {
  orgId,
  userId: "alice@hospital.org"
});
console.log(reply.content);

A Pod holds multiple files, all queried together. Add or replace files later with another documents.upload(...) call, or use the explicit initiateUpdate / completeUpdate pair for finer control.

How does the document grounding work?

The engine — extraction, structuring, grounding, retrieval-time decisioning — is our IP. It is not RAG; it materially outperforms off-the-shelf retrieval pipelines on accuracy and faithfulness. The public contract you see (upload, poll, query) is the whole surface. For deeper guarantees, custom evaluations, or a tuned pipeline for your domain, talk to us.

Multi-Pod fan-out

Pass orgId as a list to query several Pods in one call. The engine retrieves from each Pod and grounds the answer across all of them — useful when knowledge is split (one Pod per protocol set, one per region, etc.) and you don't want the caller to pick.

const reply = await client.chat.send(
  "Compare hazmat escalation between dispatch and wilderness ops.",
  { orgId: [`${namespace}_dispatch`, `${namespace}_wilderness`] }
);
console.log(reply.content);

Every id in the list must still start with your namespace; the same namespace check that applies to a single orgId applies to each element of the list.

Multi-turn conversations

Carry prior turns as messages on chat.send / chat.stream. This is the same shape used by the OpenAI and Anthropic Messages APIs. The current user turn stays in the positional content argument and is appended by the gateway before forwarding upstream.

const reply = await client.chat.send("And for a 6-year-old?", {
  orgId,
  messages: [
    { role: "user", content: "Adult atropine dose for bradycardia?" },
    { role: "assistant", content: "1 mg IV/IO q3-5 min, max 3 mg." }
  ]
});

Turns must alternate user / assistant starting with user and ending with assistant; the gateway returns a typed IncheckValidationError otherwise.

Works in both EMS and unified mode.

conversationHx is deprecated

The legacy single-string history field is still accepted for back-compat but will be removed in a future release. Prefer messages. Sending both messages and conversationHx prioritizes messages.

Streaming

Both modes support streaming. The async iterator yields a ChatChunk per SSE event and terminates on type === "complete".

for await (const chunk of client.chat.stream({
  content: "Summarize the SOP.",
  options: { orgId }
})) {
  if (chunk.content) process.stdout.write(chunk.content);
}

EMS streaming is identical — just omit orgId:

for await (const chunk of client.chat.stream({
  content: "List three scene-safety bullets."
})) {
  if (chunk.content) process.stdout.write(chunk.content);
}

Async

import { AsyncClient } from "@incheckai/sdk";

const client = new AsyncClient();

// EMS
let r = await client.chat.send("Adult dose of epinephrine for anaphylaxis?");
console.log(r.content);

// Unified
await client.documents.upload("acme_dispatch", ["./sop.pdf"]);
r = await client.chat.send("Summarize.", { orgId: "acme_dispatch" });
console.log(r.content);

AsyncClient mirrors Client one-for-one (it's an alias of the same class) — use await on all resource methods.

Document onboarding

The convenience helper handles initiate → S3 → complete → poll. Pass any mix of file paths and inline { filename, data } objects:

import { readFile } from "node:fs/promises";

const status = await client.documents.upload(
  orgId,
  [
    "./sop.pdf",
    {
      filename: "policies.docx",
      data: new Uint8Array(await readFile("./policies.docx"))
    }
  ],
  {
    batchSize: 6,          // chunk batch size (1-20)
    wait: true,            // block until the processing job is terminal
    timeoutMs: 600_000,    // milliseconds
    pollIntervalMs: 10_000 // milliseconds
  }
);

For a lower-level flow — for example to surface upload progress in a UI — drive the three steps yourself:

const initiated = await client.documents.initiateUpload(orgId, ["sop.pdf"]);
// POST each file to its presigned URL …
await client.documents.completeUpload(initiated.job_id, ["sop.pdf"]);
const status = await client.documents.waitForJob(initiated.job_id, {
  timeoutMs: 600_000
});

Errors

Every non-2xx response becomes a typed exception:

import {
  Client,
  IncheckAuthenticationError,
  IncheckPermissionError,
  IncheckValidationError,
  IncheckJobFailedError,
  IncheckJobTimeoutError,
  IncheckRateLimitError
} from "@incheckai/sdk";

const client = new Client();

try {
  await client.documents.upload("royal_dispatch", ["./sop.pdf"]);
} catch (e) {
  if (e instanceof IncheckPermissionError) {
    console.log("namespace mismatch:", e.message);
  } else if (e instanceof IncheckValidationError) {
    console.log("bad request:", e.message);
  } else if (e instanceof IncheckJobFailedError) {
    console.log("job failed:", e.responseBody);
  } else if (e instanceof IncheckJobTimeoutError) {
    console.log("still pending:", e.responseBody);
  } else if (e instanceof IncheckRateLimitError) {
    console.log(`slow down; retry after ${e.retryAfter}s`);
  } else if (e instanceof IncheckAuthenticationError) {
    console.log("check your API key");
  } else {
    throw e;
  }
}

Full hierarchy:

IncheckError
├── IncheckAuthenticationError (401)
├── IncheckPermissionError     (403)
├── IncheckNotFoundError       (404)
├── IncheckValidationError     (400/422)
├── IncheckRateLimitError      (429, has .retryAfter)
├── IncheckApiError            (5xx)
├── IncheckApiConnectionError  (network)
├── IncheckJobFailedError
└── IncheckJobTimeoutError

Coverage

Surface Methods
client.chat send, create, stream (both modes; orgId accepts string or string[]; pass prior turns via messages)
client.documents listOrgs, list, version, upload, initiateUpload, completeUpload, initiateUpdate, completeUpdate, job, waitForJob, delete, deleteVersion
client.metadata statesAndScopes

Full type reference → HTTP API reference →