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 #
npm install https://platform.xerj.ai/sdk/xerj-0.1.0.tgznpm 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 #
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:
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:
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 #
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 #
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 #
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:
import { codePointSlice } from "xerj";
codePointSlice(email, res.answer.start, res.answer.end) === res.answer.text; // always trueTo 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:
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 outAn APIError has status, type, code, param, message, request_id and headers (a Headers object):
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):
- Retried: connection errors, timeouts, and the statuses 429, 502, 503 and 504.
- Never retried: 400, 401, 403 and 404. They would fail again.
- Waiting: the
retry-afterheader when the response has one, up to 60 seconds; a longer one is thrown at once, so you can decide. Without it, exponential backoff with jitter: about 0.5 s, then 1 s, doubling up to 8 s.
Pass an AbortSignal to stop a request and its retries:
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 #
- In Node.js, Deno and Bun, requests send
User-Agent: xerj-js/0.1.0. Browsers set their own. VERSIONandDEFAULT_BASE_URLare named exports.