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):
baseUrlpassed tonew Client(...)INCHECK_BASE_URLenv varenvironmentpassed tonew Client(...)INCHECK_ENVIRONMENTenv var- 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 |