Docs·Get started

Quickstart

From nothing to an answer highlighted in your own page, in six steps.

1. Get access and a key #

Access is by invitation during the preview. It takes four steps, and the first two take a minute:

  1. Request access. Sign in with GitHub or Hugging Face (through hub.xerj.org, the same account you use on the hub). On the request access page, give your company, work email and role.
  2. Wait for the approval email. We review requests in batches. If you are approved, we email an activation link to your work address, with the subject “Your XERJ Platform access is approved”.
  3. Open the activation link while signed in with the same GitHub or Hugging Face account. The link works for 14 days. It switches your access on and opens the API keys page.
  4. Create a key. On the API keys page, create a key and copy it. We show it once.

Got an invite code? Enter it on the request access page, or open the link you were given. Invite codes skip the waitlist; company and work email are optional.

While you wait, the examples show real answers on invoices, contracts, claims, logs and email.

Then put the key in an environment variable on your machine or server:

Shell
export XERJ_API_KEY="xerj_…"

Keep the key on the server. Never ship it in browser code or commit it to a repository.

2. Ask with curl #

Send a document and a question to POST /v1/answers:

Shell
curl https://platform.xerj.ai/v1/answers \
  -H "Authorization: Bearer $XERJ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "document": "Invoice INV-20931 for 48,200 EUR is due on 30 November.",
    "question": "How much is invoice INV-20931?"
  }'

The answer is the exact text from the document, and where it is:

JSON
{
  "id": "ans_Qm4pT7vL2x",
  "object": "answer",
  "created": 1794326400,
  "model": "xerj-reader-1",
  "answer": {"type": "span", "text": "48,200 EUR", "start": 22, "end": 32, "confidence": 0.98},
  "best_guess": null,
  "usage": {"input_tokens": 22, "output_tokens": 3, "total_tokens": 25}
}

start and end are character offsets: document[22:32] is 48,200 EUR. Values in these examples are for illustration. Every response also has latency_ms, the time the request took on our side.

3. Python #

Install the SDK. It has no dependencies and needs Python 3.9 or newer.

Shell
pip install https://platform.xerj.ai/sdk/xerj-0.1.0-py3-none-any.whl

pip install xerj from PyPI is coming soon.

Python
from xerj import XERJ

client = XERJ()  # reads XERJ_API_KEY from the environment

res = client.answers.create(
    document="Invoice INV-20931 for 48,200 EUR is due on 30 November.",
    question="How much is invoice INV-20931?",
)
print(res.answer.type, res.answer.text, res.answer.start, res.answer.end)
# span 48,200 EUR 22 32

More in the Python SDK reference.

4. JavaScript #

Install the SDK. It is ESM, has no dependencies, and runs on Node 18 or newer.

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

npm install xerj from the npm registry is coming soon.

JavaScript
import XERJ from "xerj";

const client = new XERJ({ apiKey: process.env.XERJ_API_KEY });

const res = await client.answers.create({
  document: "Invoice INV-20931 for 48,200 EUR is due on 30 November.",
  question: "How much is invoice INV-20931?",
});
console.log(res.answer.type, res.answer.text, res.answer.start, res.answer.end);
// span 48,200 EUR 22 32

More in the JavaScript SDK reference.

5. Read the answer #

There are three cases. Handle all three. Here doc is the text you sent as document:

Python
a = res.answer
if a.type == "span":
    print("Answer:", a.text, "at", a.start, a.end)
elif a.type in ("yes", "no"):
    print("Answer:", a.text)            # "Yes" or "No"
    if a.start is not None:
        print("Because of:", doc[a.start:a.end])
else:  # "none": the document does not answer the question
    print("Not in this document")
    if res.best_guess:
        print("Closest passage (not trusted):", res.best_guess.text)
JavaScript
const a = res.answer;
if (a.type === "span") {
  console.log("Answer:", a.text, "at", a.start, a.end);
} else if (a.type === "yes" || a.type === "no") {
  console.log("Answer:", a.text);       // "Yes" or "No"
  if (a.start !== null) console.log("Because of:", doc.slice(a.start, a.end));
} else {
  // "none": the document does not answer the question
  console.log("Not in this document");
  if (res.best_guess) console.log("Closest passage (not trusted):", res.best_guess.text);
}

6. Highlight the answer in your page #

The offsets let your own UI show the answer inside the document. Here is a complete, minimal example: a Node server that keeps the key, and a page that asks a question and highlights the answer.

server.mjs keeps the API key on the server:

JavaScript
import http from "node:http";
import { readFile } from "node:fs/promises";
import XERJ from "xerj";

const client = new XERJ({ apiKey: process.env.XERJ_API_KEY });
const doc = await readFile("email.txt", "utf8");

http.createServer(async (req, res) => {
  if (req.method === "POST" && req.url === "/ask") {
    let body = "";
    for await (const chunk of req) body += chunk;
    const { question } = JSON.parse(body);
    const r = await client.answers.create({ document: doc, question });
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ document: doc, answer: r.answer, best_guess: r.best_guess }));
    return;
  }
  res.writeHead(200, { "content-type": "text/html" });
  res.end(await readFile("index.html"));
}).listen(3000);

index.html has a form, a <pre id="doc"> and this script. It inserts text with textContent, never as HTML, so a document cannot inject markup:

JavaScript
const form = document.querySelector("form");
const view = document.querySelector("#doc");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const question = new FormData(form).get("q");
  const r = await fetch("/ask", { method: "POST", body: JSON.stringify({ question }) });
  const { document: doc, answer, best_guess } = await r.json();

  const hit = answer.start !== null ? answer : best_guess;  // a guess is shown, not trusted
  if (!hit || hit.start === null) {
    view.textContent = doc;
    return;
  }
  const mark = Object.assign(document.createElement("mark"), {
    textContent: doc.slice(hit.start, hit.end),
  });
  if (hit === best_guess) mark.className = "guess";  // style it differently, e.g. a dashed outline
  view.replaceChildren(doc.slice(0, hit.start), mark, doc.slice(hit.end));
  mark.scrollIntoView({ block: "center" });
});

If your documents can contain emoji or other characters outside the Basic Multilingual Plane, convert the offsets first: see offsets are characters. On the server, the SDK's codePointSlice gives the answer text the same way the API counts.

The same in Python, for server-rendered HTML:

Python
import html

def highlighted(doc: str, start: int, end: int) -> str:
    return (html.escape(doc[:start]) + "<mark>" + html.escape(doc[start:end])
            + "</mark>" + html.escape(doc[end:]))

a = res.answer
if a.start is not None:
    page = "<pre>" + highlighted(doc, a.start, a.end) + "</pre>"

Python strings index by code point, the same unit as the offsets, so no conversion is needed.

Next #