Docs·API reference

Extract

POST /v1/extract reads up to 20 fields from one document in one call, each with its evidence span, or a clear DECLINE.

HTTP
POST https://platform.xerj.ai/v1/extract
Authorization: Bearer $XERJ_API_KEY
Content-Type: application/json

Extract is many questions about one document in one request. Each field is read separately, with its own confidence and its own evidence.

Request #

Field Type Required Description
document string yes The text to read. Up to 1,000,000 characters. state is accepted as another name for it.
fields object yes 1 to 20 fields. Each key is a field name; each value defines the field (below).
min_confidence number no The bar for every field, with 0 < x < 1. Default 0.5.
model string no Default xerj-reader-1.

Defining a field #

A field value is one of three things:

You write It means
"total": "What is the total amount due?" A text field with this question.
"paid": {"question": "Has the invoice been paid?", "type": "boolean"} An object with an optional question and an optional type.
"due_date": {} No question: it becomes “What is the due date?”, from the name with _ and - read as spaces.

Response #

Field Type Description
id string ext_…
object string Always "extraction".
created integer Unix time, in seconds.
model string The model that read the document.
fields object One result per field name, below.
usage object input_tokens, output_tokens, total_tokens, summed over all fields.
latency_ms integer Time spent on our side, in milliseconds.

A field result #

Field Type Description
status string "ANSWER" or "DECLINE".
value string, boolean or null Text field: the span text. Boolean field: true or false. null on DECLINE.
confidence number Between 0 and 1.
evidence array On ANSWER: the spans the value comes from, [{start, end, quote}], where quote == document[start:end]. One span for a text field; possibly none for a boolean.
best_guess object or null On DECLINE: the closest candidate {text, start, end, confidence}, or null.

A DECLINE means the document does not answer the field above your bar. A boolean field that the model answers with a span instead of Yes/No is also a DECLINE, with that span as best_guess.

Example: an invoice #

The document:

Text
INVOICE

Invoice number: INV-20931
Invoice date: 14 October 2025
Due date: 30 November 2025

From: Northwind Logistics B.V., Havenstraat 12, 3011 AB Rotterdam, Netherlands
Bill to: Acme Retail Ltd, 4 Park Row, Leeds LS1 5HD, United Kingdom
Purchase order: PO-5512

Item                       Qty     Unit price     Amount
Model X40, Q4 shipment 1   1,200   40.00 EUR      48,000.00 EUR
Sea freight surcharge      1       200.00 EUR     200.00 EUR

Total due: 48,200.00 EUR
Pay by bank transfer to IBAN NL91 ABNA 0417 1643 00, reference INV-20931.
Status: unpaid

The request, with six fields:

Python
from xerj import XERJ

client = XERJ()
invoice = open("invoice.txt", encoding="utf-8").read()

r = client.extract.create(
    document=invoice,
    fields={
        "invoice_number": "What is the invoice number?",
        "total": {"question": "What is the total amount due?"},
        "due_date": {},
        "purchase_order": "Which purchase order does this invoice refer to?",
        "paid": {"question": "Has the invoice been paid?", "type": "boolean"},
        "vat_number": {},
    },
)

for name, f in r.fields.items():
    if f.status == "ANSWER":
        where = f"at {f.evidence[0].start}-{f.evidence[0].end}" if f.evidence else ""
        print(f"{name}: {f.value!r} {where}")
    else:
        print(f"{name}: not in the document")
JavaScript
import { readFile } from "node:fs/promises";
import XERJ from "xerj";

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

const r = await client.extract.create({
  document: invoice,
  fields: {
    invoice_number: "What is the invoice number?",
    total: { question: "What is the total amount due?" },
    due_date: {},
    purchase_order: "Which purchase order does this invoice refer to?",
    paid: { question: "Has the invoice been paid?", type: "boolean" },
    vat_number: {},
  },
});

for (const [name, f] of Object.entries(r.fields)) {
  if (f.status === "ANSWER") console.log(name, f.value, f.evidence[0] ?? "");
  else console.log(name, "not in the document");
}
curl
curl https://platform.xerj.ai/v1/extract \
  -H "Authorization: Bearer $XERJ_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json

The response (values for illustration; id, created and latency_ms left out):

JSON
{
  "object": "extraction",
  "model": "xerj-reader-1",
  "fields": {
    "invoice_number": {"status": "ANSWER", "value": "INV-20931", "confidence": 0.98,
                       "evidence": [{"start": 25, "end": 34, "quote": "INV-20931"}]},
    "total": {"status": "ANSWER", "value": "48,200.00 EUR", "confidence": 0.96,
              "evidence": [{"start": 459, "end": 472, "quote": "48,200.00 EUR"}]},
    "due_date": {"status": "ANSWER", "value": "30 November 2025", "confidence": 0.97,
                 "evidence": [{"start": 75, "end": 91, "quote": "30 November 2025"}]},
    "purchase_order": {"status": "ANSWER", "value": "PO-5512", "confidence": 0.95,
                       "evidence": [{"start": 256, "end": 263, "quote": "PO-5512"}]},
    "paid": {"status": "ANSWER", "value": false, "confidence": 0.9,
             "evidence": [{"start": 555, "end": 561, "quote": "unpaid"}]},
    "vat_number": {"status": "DECLINE", "value": null, "confidence": 0.07,
                   "best_guess": {"text": "NL91 ABNA 0417 1643 00", "start": 502, "end": 524, "confidence": 0.07}}
  },
  "usage": {"input_tokens": 892, "output_tokens": 14, "total_tokens": 906}
}

Read it field by field:

Show the evidence #

Every ANSWER carries offsets, so a reviewer can see each value in place. One way, in the browser, building on the quickstart:

JavaScript
// Highlight every answered field in the invoice text. Spans do not overlap here.
const spans = Object.entries(r.fields)
  .filter(([, f]) => f.status === "ANSWER" && f.evidence.length)
  .map(([name, f]) => ({ name, ...f.evidence[0] }))
  .sort((a, b) => a.start - b.start);

const parts = [];
let at = 0;
for (const s of spans) {
  parts.push(invoice.slice(at, s.start));
  const mark = document.createElement("mark");
  mark.textContent = invoice.slice(s.start, s.end);
  mark.title = s.name;
  parts.push(mark);
  at = s.end;
}
parts.push(invoice.slice(at));
document.querySelector("#invoice").replaceChildren(...parts);

Billing #

Each field is one read of the document. Six fields on this invoice are six reads: input_tokens is six times the document, plus the six questions. That is the same as six calls to /v1/answers, in one round trip. Output is free.

Errors #

The errors of /v1/answers, plus:

Status Code When
400 invalid_fields fields is not an object, a name breaks the naming rule, a value is not a string or an object, or a type is not text or boolean.
400 too_many_fields More than 20 fields.