Docs·SDKs

JavaScript SDK

The xerj package. ESM, no dependencies, Node 18 or newer, typed with a hand-written index.d.ts, retries built in.

Install #

Shell
npm install https://platform.xerj.ai/sdk/xerj-0.1.0.tgz

npm install xerj from the npm registry is coming soon. The package is ESM only and uses the global fetch. It runs on Node.js 18 or newer, and on Deno and Bun.

Create a client #

JavaScript
import XERJ from "xerj";

const client = new XERJ();   // reads XERJ_API_KEY, and XERJ_BASE_URL if set
Option Default Description
apiKey XERJ_API_KEY Your key. Pass it, or set the environment variable.
baseURL XERJ_BASE_URL, else https://platform.xerj.ai/v1 Where the API lives.
timeout 60000 Milliseconds to wait for one attempt.
maxRetries 2 Retries after the first attempt. 0 turns retries off.
defaultHeaders none Extra headers sent with every request.
dangerouslyAllowBrowser false See browsers.
fetch global fetch Your own fetch, for tests or proxies.

client.withOptions({...}) returns a copy with some options changed:

JavaScript
const patient = client.withOptions({ timeout: 120000, maxRetries: 5 });

Every method also takes a second argument for one request: { headers, timeout, maxRetries, signal }.

Answers #

Request fields use the API's names, such as min_confidence:

JavaScript
const res = await client.answers.create({
  document: email,
  question: "How much is invoice INV-20931?",
  min_confidence: 0.5,          // optional, 0 < x < 1
  model: "xerj-reader-1",       // optional
});

res.answer.type;        // "span", "yes", "no" or "none"
res.answer.text;        // "48,200 EUR"
res.answer.start;       // 533
res.answer.end;         // 543
res.answer.confidence;  // 0.98
res.best_guess;         // null, or {text, start, end, confidence} when the type is "none"
res.usage.input_tokens;

See Answers for every field.

Extract #

JavaScript
const r = await client.extract.create({
  document: invoice,
  fields: {
    total: "What is the total amount due?",
    paid: { question: "Has the invoice been paid?", type: "boolean" },
    due_date: {},   // asks "What is the due date?"
  },
});

const total = r.fields.total;
if (total.status === "ANSWER") console.log(total.value, total.evidence[0].start, total.evidence[0].end);

See Extract for the field rules and the full invoice example.

Convert #

JavaScript
const d = await client.convert.create({ file: "invoice.pdf" });   // a path (Node.js, Deno, Bun)

const bytes = await readFile("invoice.pdf");                    // or bytes, a Blob or a File
const d2 = await client.convert.create({ file: bytes, filename: "invoice.pdf" });

const res = await client.answers.create({ document: d.text, question: "What is the total amount due?" });

Models and usage #

JavaScript
const models = await client.models.list();
for (const m of models.data) console.log(m.id, m.status, m.pricing.input_per_million);

const reader = await client.models.retrieve("xerj-reader-1");

const u = await client.usage.retrieve({ start: "2026-11-01", end: "2026-11-10" });
console.log(u.totals.input_tokens, u.today.remaining_input_tokens);

Highlight an answer #

The API counts offsets in Unicode code points. JavaScript strings count UTF-16 units, which differ after an emoji. The SDK exports codePointSlice, which slices the way the API counts:

JavaScript
import { codePointSlice } from "xerj";

codePointSlice(email, res.answer.start, res.answer.end) === res.answer.text;   // always true

To put the answer in a page, convert the offsets to string indexes, then build the nodes. The quickstart has a complete example, and concepts a toIndex helper.

Errors #

Every error the SDK throws is a XERJError. The classes are named exports, and also static members of XERJ, such as XERJ.RateLimitError:

Text
XERJError
├── APIError                  the API answered with an error
│   ├── BadRequestError         400
│   ├── AuthenticationError     401
│   ├── PermissionDeniedError   403
│   ├── NotFoundError           404
│   ├── RateLimitError          429
│   ├── OverloadedError         503
│   └── InternalServerError     500 and other 5xx
└── APIConnectionError        no response
    └── APITimeoutError         the request timed out

An APIError has status, type, code, param, message, request_id and headers (a Headers object):

JavaScript
import XERJ, { BadRequestError, RateLimitError, APIConnectionError } from "xerj";

try {
  const res = await client.answers.create({ document: text, question });
} catch (err) {
  if (err instanceof BadRequestError) console.log("Fix the request:", err.code, err.param, err.message);
  else if (err instanceof RateLimitError) console.log("Retry after", err.headers.get("retry-after"), "s");
  else if (err instanceof APIConnectionError) console.log("Network trouble. Try again later.");
  else throw err;
}

Every response object also carries the request id in res._request_id (not enumerable, so it stays out of JSON.stringify). The errors page lists every code.

Retries #

The client retries for you, up to maxRetries times (2 by default):

Pass an AbortSignal to stop a request and its retries:

JavaScript
const controller = new AbortController();
setTimeout(() => controller.abort(), 10000);
await client.answers.create({ document, question }, { signal: controller.signal });

Browsers #

An API key in a web page is visible to everyone who opens the page. So in a browser, new XERJ() throws unless you pass dangerouslyAllowBrowser: true.

Do not set it on a public site. Call the API from your server, and send the answer to the page, as in the quickstart. Turn it on only for a tool that runs on your own machine with your own key.

TypeScript #

The package ships a hand-written index.d.ts. Every request and response type is there, so editors complete res.answer.type to "span" | "yes" | "no" | "none".

Details #