Get started
TypeScript SDK
Call Drex's decision model and run Perception's document tools from Node with nace-sdk — client options, calibrated questions, typed answers, parse, split, classify, extract, ground, uploads, jobs, workspaces, workspace files, domains, search, saved schemas, errors, retries and logging.
The nace-sdk npm package is the official TypeScript client for https://console.nace.ai. One client covers Drex's decision model, systemOne, decisions.create and models.list, and Perception's document tools: the five document jobs, uploads, jobs, job files, workspaces, workspace files, domains, storage connections, search and saved extraction schemas. It needs Node 20 or later and ships as both ESM and CommonJS.
The client is built on the TypeSafe SDK, so systemOne, noul, choice, score, the inferred answer types, RetryPolicy and the error classes keep the names they have in @typesafe-ai/sdk. The SDK types take instructions and criteria as text (the API also renders JSON instructions, see Questions), and it has its own classes for 402 and 529; Migrate from TypeSafe lists every difference.
npm install nace-sdk
export NACE_API_KEY="nace_sk_..."Create a key as shown in Authentication, and keep it on a server: it spends your account's balance, and a browser can't call the /v1 routes from another origin (see Browsers). The samples on this page use a client made with new NaceClient(), which reads NACE_API_KEY, NACE_BASE_URL, NACE_DEFAULT_DECISION_MODEL and NACE_LOG_LEVEL.
Configure the client
new NaceClient(config?)
| Parameter | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes, or NACE_API_KEY | Your nace_sk_ key. |
baseURL | string | No | API root, with trailing slashes removed. Default: NACE_BASE_URL, then https://console.nace.ai. |
defaultModel | string | No | The model for calls that don't name one. Default: NACE_DEFAULT_DECISION_MODEL, then drex-latest. |
timeout | number | No | Milliseconds per attempt, more than 0. Default: 60000, because Drex waits up to 55 s for the model before it answers 529. A document create adds its wait_seconds to it. |
retry | Partial<RetryPolicy> | No | Retry overrides. Fields you leave out keep the defaults in Retries. |
logLevel | LogLevel | No | "debug", "info", "warn", "error" or "off". Default: NACE_LOG_LEVEL, then "warn". See Logging. |
logger | Logger | No | Where log lines go. Default: console, with a [nace-sdk] prefix. |
defaultHeaders | Record<string, string> | No | Headers sent with every request to Drex. They aren't sent to the document service's upload URLs or to signed links. |
clientInfo | string | No | The application or agent using the SDK, as name/version, sent as X-Nace-Client so Nace can record it with each request, and appended to User-Agent after the SDK's own nace-sdk/<version>. Not sent to upload URLs or signed links. |
fetch | Fetch | No | The fetch to send requests with, for a proxy agent or a test double. Default: the global fetch. |
dangerouslyAllowBrowser | boolean | No | Let the constructor run where the browser globals window, window.document and navigator are all defined. Default: false. See Browsers. |
config is a NaceClientConfig. Fetch is (input: string, init?: RequestInit) => Promise<Response>, the shape of the global fetch.
An option you pass wins over its environment variable, and the variable wins over the default. Blank or whitespace-only environment values are ignored; an option you pass is used as it is, even when empty. The variables are read from process.env, so in a runtime without it, pass the options. The client doesn't read the CLI's ~/.nace/config.toml: pass apiKey, or set NACE_API_KEY.
| Variable | Option | ENV key |
|---|---|---|
NACE_API_KEY | apiKey | ENV.apiKey |
NACE_BASE_URL | baseURL | ENV.baseURL |
NACE_DEFAULT_DECISION_MODEL | defaultModel | ENV.defaultModel |
NACE_LOG_LEVEL | logLevel | ENV.logLevel |
Each variable's name from before the package was renamed from drex-sdk still works when the new one is unset or blank: DREX_API_KEY, DREX_BASE_URL, DREX_DEFAULT_MODEL and DREX_LOG_LEVEL (LEGACY_ENV). The new name wins when both are set.
The constructor throws a NaceError when neither apiKey nor NACE_API_KEY is set, when timeout or a retry field is out of range, when baseURL (or NACE_BASE_URL) is not an absolute URL or contains user information (user@), when the log level is unknown (from the option or from NACE_LOG_LEVEL), when the runtime has no global fetch and you passed none, and in a browser without dangerouslyAllowBrowser. The SDK counts a runtime as a browser when the globals window, window.document and navigator are all defined.
import { NaceClient } from "nace-sdk";
const client = new NaceClient({
timeout: 90_000,
retry: { maxRetries: 4, timeoutMs: 500_000 }, // allow 5 attempts; opt into a 500 s budget for starting retries
defaultHeaders: { "X-Team": "support" },
logLevel: "info",
});The SDK's X-Nace-SDK and X-Nace-Runtime remain the transport identity.
A valid X-Nace-Client is recorded separately as client_app; a recognized
application can determine the traffic channel without replacing SDK identity.
No client declaration is sent to external upload or signed download URLs.
NACE_API_KEY and NACE_BASE_URL configure the key and API root for cloud, custom and on-prem deployments. No constructor arguments or login profile are required. Explicit arguments override the environment, and the legacy DREX_* names remain supported. The configured API root is trusted configuration: select the key and root for the same deployment. A URL with a user@ part is refused.
For an on-prem deployment, set the environment pair and use the client normally:
export NACE_API_KEY="nace_sk_..."
export NACE_BASE_URL="https://nace.customer.example"new NaceClient() reads this configuration. Explicit apiKey and baseURL options are supported too. Server-side authenticated API and upload-grant requests follow same-origin redirects; a change of scheme, host or effective port is rejected. Credential-free signed links use native redirect following without API headers or cookies. A browser that hides authenticated redirect destinations cannot safely follow them.
Client properties
Every option resolves to a property typed readonly, so TypeScript refuses an assignment to it; JavaScript does not stop one at runtime. The API key is never exposed. A JavaScript change to baseURL selects a new trusted API root for subsequent calls.
| Property | Type | Description |
|---|---|---|
baseURL | string | The API root in use. |
defaultModel | string | The model for calls that don't name one. |
timeout | number | Milliseconds per attempt. |
retry | RetryPolicy | The full retry policy, with your overrides applied. |
logLevel | LogLevel | The resolved log level. |
logger | Logger | Your logger, or the console one, filtered to logLevel. |
defaultHeaders | Readonly<Record<string, string>> | A copy of defaultHeaders. |
fetch | Fetch | The fetch you passed, or by default a function that calls the global fetch. |
models | Models | List models. |
decisions | Decisions | POST /v1/decisions, OpenAI's Decisions shape on the same models and billing. See OpenAI-compatible decisions. |
documents | Documents | Document jobs and uploads. See Create a job and Uploads. |
jobs | Jobs | Reading, waiting for and deleting document jobs, and their files. See Jobs and Job files. |
extractionSchemas | ExtractionSchemas | Saved extraction schemas. See Saved schemas. |
browserRuns | BrowserRuns | Cloud browser runs. See Browser runs. |
workspaces | Workspaces | Workspaces, with their files and ingestion. See Workspaces and Workspace files. |
domains | Domains | The taxonomies a workspace classifies its files against: create, edit, publish and duplicate them. See Domains. |
storageConnections | StorageConnections | Your own S3 buckets and Azure Blob containers a new workspace can keep its files in. See Storage connections. |
search | Search | Questions over a workspace's files. See Search. |
Browsers
new NaceClient() throws in a browser, because the page would hand your API key to anyone who opens it. dangerouslyAllowBrowser: true only turns that check off. It doesn't make Drex reachable: the /v1 routes send no Access-Control-Allow-Origin header, so the browser blocks a page on another origin from reading their answers, and every call fails with APIConnectionError (or with a NaceError when the response is a redirect the browser hides). The option is for code that has browser globals but no cross-origin checks, such as tests under jsdom.
Call Drex from your server. To move files in a browser, mint an upload grant on the server and let the browser send the file to the document service, and hand the browser signed links from jobs.fileLink; see Upload grants.
Per-call options
Every method takes a RequestOptions as its last argument. A few document methods take more, or less; their sections say so.
| Parameter | Type | Required | Description |
|---|---|---|---|
signal | AbortSignal | No | Cancels the request and any wait before a retry. Throws APIUserAbortError. |
timeout | number | No | Milliseconds per attempt, in place of the client's timeout. The retry budget decides whether another attempt starts; it doesn't shorten an attempt. |
retry | Partial<RetryPolicy> | No | Retry overrides for this call. Fields you leave out keep the client's policy. { maxRetries: 0 } turns retries off. |
headers | Record<string, string> | No | Extra headers, merged over defaultHeaders. Names compare case-insensitively. |
The SDK sets Authorization, Accept, User-Agent, X-Nace-SDK, X-Nace-Runtime, Content-Type, Idempotency-Key and, on a retry, X-Nace-Retry-Count itself, so a value you pass for one of them is replaced or dropped.
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
await client.systemOne(
{ state: "Where is my parcel?", questions: { shipping: noul("Is it about shipping?") } },
{ signal: controller.signal, retry: { maxRetries: 0 }, headers: { "X-Trace-Id": "t-123" } },
);Document batches
Start with 8 concurrent documents, keeping each upload, create and wait in one worker. wait_seconds: 60 holds the create open for up to 60 seconds; jobs.wait polls with the SDK's delay if the job is still running. Job reads and upload setup each have their own RPM window, at 5x and 2x the create allowance respectively (including an account RPM override). Creates stay at 120/min free or 600/min paid; reads allow 600/3000 and upload setup 240/1200. They cannot spend the create budget, but can return their own 429; NDI can also throttle them. The current SDK honors Retry-After (seconds until retry) automatically, without a default total retry budget. See Rate limits.
import { openAsBlob } from "node:fs";
import { readdir } from "node:fs/promises";
import { NaceClient, workspaceFile } from "nace-sdk";
const client = new NaceClient();
const paths = (await readdir("invoices")).filter((name) => name.endsWith(".pdf")).sort();
let next = 0;
const failures: string[] = [];
await Promise.all(Array.from({ length: 8 }, async () => {
while (next < paths.length) {
const name = paths[next++];
try {
const file = await client.documents.upload(await openAsBlob(`invoices/${name}`), {
file_name: name,
});
const job = await client.documents.extract({
source: workspaceFile(file),
schema: { type: "object", properties: { total: { type: "number" } } },
wait_seconds: 60,
});
console.log(name, await client.jobs.wait(job.job_id));
} catch (error) {
failures.push(name);
console.error(name, error);
}
}
}));
if (failures.length) throw new Error(`${failures.length} documents failed; inspect the per-file errors`);