Docs·SDKs

Python SDK

The xerj package. Standard library only, Python 3.9 or newer, typed responses, retries built in.

Install #

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

pip install xerj from PyPI is coming soon. The package has no dependencies beyond the Python standard library.

Create a client #

Python
from xerj import XERJ

client = XERJ()   # reads XERJ_API_KEY, and XERJ_BASE_URL if set
Option Default Description
api_key XERJ_API_KEY Your key. Pass it, or set the environment variable.
base_url XERJ_BASE_URL, else https://platform.xerj.ai/v1 Where the API lives.
timeout 60 Seconds to wait for one attempt.
max_retries 2 Retries after the first attempt. 0 turns retries off.
default_headers none Extra headers sent with every request.

client.with_options(...) returns a copy with some options changed:

Python
patient = client.with_options(timeout=120, max_retries=5)

Every method also takes timeout= and extra_headers= for one request.

Answers #

Python
res = 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
)

a = res.answer
a.type          # "span", "yes", "no" or "none"
a.text          # "48,200 EUR"
a.start, a.end  # 533, 543 -> email[533:543] == "48,200 EUR"
a.confidence    # 0.98
res.best_guess  # None, or BestGuess(text, start, end, confidence) when a.type == "none"
res.usage.input_tokens

See Answers for every field.

Extract #

Python
r = 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?"
    },
)

total = r.fields["total"]
if total.status == "ANSWER":
    print(total.value, total.evidence[0].start, total.evidence[0].end)

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

Convert #

Python
d = client.convert.create(file="invoice.pdf")                     # a path
d = client.convert.create(file=open("invoice.pdf", "rb"))         # a file object
d = client.convert.create(file=pdf_bytes, filename="invoice.pdf")  # bytes need a filename

res = client.answers.create(document=d.text, question="What is the total amount due?")

Models and usage #

Python
for m in client.models.list():
    print(m.id, m.status, m.pricing.input_per_million)

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

u = client.usage.retrieve(start="2026-11-01", end="2026-11-10")   # or datetime.date objects
print(u.totals.input_tokens, u.totals.cost_usd, u.today.remaining_input_tokens)

Responses #

Responses are dataclasses with attribute access. to_dict() gives the plain JSON-shaped dict, for logging or for passing on:

Python
import json
print(json.dumps(res.to_dict(), indent=2))

Async #

AsyncXERJ has the same options and methods. Await them:

Python
import asyncio
from xerj import AsyncXERJ

async def main():
    client = AsyncXERJ()
    questions = ["How much is invoice INV-20931?", "When does the first shipment reach Leeds?"]
    results = await asyncio.gather(*(client.answers.create(document=email, question=q) for q in questions))
    for q, r in zip(questions, results):
        print(q, "->", r.answer.text)

asyncio.run(main())

Mind your requests per minute when you run many requests at once.

Errors #

Every error the SDK raises is a XERJError:

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:

Python
from xerj import XERJ, BadRequestError, RateLimitError, APIConnectionError

try:
    res = client.answers.create(document=text, question=q)
except BadRequestError as err:
    print("Fix the request:", err.code, err.param, err.message)
except RateLimitError as err:
    print("Slow down. Retry after", err.headers.get("retry-after"), "s")
except APIConnectionError:
    print("Network trouble. Try again later.")

The errors page lists every code.

Retries #

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

Python
client = XERJ(max_retries=0)    # handle every error yourself
client = XERJ(max_retries=5)    # be more patient

Highlight an answer #

Python strings index by code point, the same unit as the API offsets, so slicing is direct:

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.type == "span":
    print(highlighted(email, a.start, a.end))

Details #