Docs·SDKs
Python SDK
The xerj package. Standard library only, Python 3.9 or newer, typed responses, retries built in.
Install #
pip install https://platform.xerj.ai/sdk/xerj-0.1.0-py3-none-any.whlpip install xerj from PyPI is coming soon. The package has no dependencies beyond the Python standard library.
Create a client #
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:
patient = client.with_options(timeout=120, max_retries=5)Every method also takes timeout= and extra_headers= for one request.
Answers #
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_tokensSee Answers for every field.
Extract #
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 #
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 #
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:
import json
print(json.dumps(res.to_dict(), indent=2))Async #
AsyncXERJ has the same options and methods. Await them:
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:
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:
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):
- 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 longerretry-afteris raised at once, so you can decide. Without it, exponential backoff with jitter: about 0.5 s, then 1 s, doubling up to 8 s.
client = XERJ(max_retries=0) # handle every error yourself
client = XERJ(max_retries=5) # be more patientHighlight an answer #
Python strings index by code point, the same unit as the API offsets, so slicing is direct:
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 #
- Requests send
User-Agent: xerj-python/0.1.0. - The package is typed (
py.typed), so editors and type checkers see every field.