OpenRouter has opened an alpha Extractions API, a new route that pulls typed fields out of text, images and PDFs. Instead of asking a chat model to “return JSON” and hoping it behaves, you describe each field once (a text value, a number, a date, a list, even an estimate with a range) and get back one typed answer per field, plus the probability that the document contains nothing for it. This tutorial walks through the request shape, the field types, which models support what, and a small Python invoice parser you can adapt.
What the Extractions API does
According to the official Extractions guide, you send POST /api/alpha/extractions with three main things:
model: one of the extraction-capable models listed below.state: the text to read from (it can be empty when you only send images or a PDF).fields: a map of field name to a typed field spec.
Each answer comes back keyed by the field name, with a typed value, a p_null (the probability the source has nothing for that field), often a confidence, and for text-based answers a character span showing exactly where the value came from. That span is the part that makes this useful for audit trails: you can highlight the source text in your UI instead of trusting a free-form reply.
Two warnings before you build on it. The route is marked alpha, and OpenRouter says its shape can change. And unknown keys are rejected with a 400 rather than ignored, so typos fail loudly, which is good, but it also means you should pin your request format in tests.
Supported models and what they can read
Not every model supports every field type. The docs list these at the time of writing:
- cloudflare/clairic: text plus up to 4 PNG, JPEG or WebP images; supports every field type.
- inception/mercury-decide: text only;
textfields and lists of text. - inference-net/schematron-v2-small and -turbo: text only; text, number, date, time, datetime and lists.
- levanto/sage-2: text only; adds
durationand lists of any item type. - perplexity/pplx-decider-v1.3 and -flash-v1.3: text only; text and lists of text, up to 128 fields, with exact passage spans.
- scaledown/extract: text, or one PDF or image per request; text and lists of text.
If you send something a model cannot handle (a date field to Mercury Decide, an image to a text-only model), OpenRouter returns a 400 before anything reaches the provider. You can also list the supported models programmatically:
curl "https://openrouter.ai/api/v1/models?endpoints=/api/alpha/extractions"
The field types
Every field takes a type, an optional instructions string, and an optional null_threshold (default 0.5): if p_null is above it, value becomes null, though a guess may still be included. The main types are:
text,date,time,datetime: plain values.numberandduration: supportinteger,decimals(0-6) and aunit. Duration units are seconds through years.list: needsitemsset totext,numberordate.estimate: needsminandmax, and returns a distribution (mean,median,p10,p90).forecast: like an estimate, plushorizon(1-64 steps) andinterval.
A top-level reference_date (YYYY-MM-DD) anchors dates written without a year, such as “due 2 Oct”.
Your first request with curl
This is the quickstart from the docs. Export your key first (export OPENROUTER_API_KEY=...):
curl https://openrouter.ai/api/alpha/extractions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "cloudflare/clairic",
"state": "Invoice INV-2291 issued 2026-09-02, net 30. Contacts: Dana Ruiz, Priya Shah. Total $1080.",
"fields": {
"invoice_number": { "type": "text" },
"due_date": { "type": "date", "instructions": "Issue date plus the net payment days" },
"total": { "type": "number", "decimals": 2, "unit": "USD" },
"contacts": { "type": "list", "items": "text" },
"days_to_pay": { "type": "estimate", "min": 0, "max": 120, "step": 1 }
},
"reference_date": "2026-10-28"
}'
The example response in the docs returns "INV-2291" with a span of [8, 16], a computed due date of 2026-10-02, a numeric total of 1080, both contacts as a list with spans, and an estimate distribution for days_to_pay. Note that the due date was computed from the instructions, not just copied, and the response reports output_tokens: 0 in usage.
A Python invoice parser
Here is a small, reusable wrapper using requests. It sends the fields, then sorts answers into “trusted” and “needs review” based on p_null and confidence.
import os
import requests
URL = "https://openrouter.ai/api/alpha/extractions"
HEADERS = {
"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
"Content-Type": "application/json",
}
INVOICE_FIELDS = {
"invoice_number": {"type": "text"},
"issue_date": {"type": "date"},
"total": {"type": "number", "decimals": 2, "unit": "USD"},
"vendor": {"type": "text", "instructions": "The company that issued the invoice"},
"line_items": {"type": "list", "items": "text"},
}
def extract(text, fields, model="cloudflare/clairic", reference_date=None):
body = {"model": model, "state": text, "fields": fields}
if reference_date:
body["reference_date"] = reference_date
resp = requests.post(URL, headers=HEADERS, json=body, timeout=60)
resp.raise_for_status()
return resp.json()
def triage(result, min_conf=0.9):
trusted, review = {}, {}
for name, ans in result["answers"].items():
conf = ans.get("confidence")
if ans.get("value") is None or (conf is not None and conf < min_conf):
review[name] = ans
else:
trusted[name] = ans["value"]
return trusted, review
if __name__ == "__main__":
text = (
"ACME Supplies Ltd. Invoice A-7781, dated 14 Oct 2026. "
"Items: 20 desk lamps, 5 office chairs. Amount due: $2,349.50"
)
result = extract(text, INVOICE_FIELDS, reference_date="2026-11-13")
trusted, review = triage(result)
print("Trusted:", trusted)
print("Needs review:", list(review))
print("Usage:", result.get("usage"))
Because every field returns its own confidence and null probability, you can auto-accept the easy ones and send only the uncertain fields to a person. That is much cheaper than reviewing every document by hand.
Reading images and PDFs
Files go into a top-level assets map as base64 (no data: prefix), and you reference each asset key in the state or in a field’s instructions:
import base64
with open("receipt.jpg", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
body = {
"model": "cloudflare/clairic",
"state": "Read the totals from the invoice photo: invoice-photo",
"assets": {"invoice-photo": {"format": "jpg", "data_base64": b64}},
"fields": {"total": {"type": "number", "decimals": 2}},
}
resp = requests.post(URL, headers=HEADERS, json=body, timeout=60)
print(resp.json()["answers"]["total"])
Limits to remember from the docs: Clairic reads up to 4 images (4 MiB each decoded, 8 MiB total) and no PDFs. ScaleDown Extract reads one PDF, PNG or JPEG, and reads either the text or the document, not both. Every asset must be referenced when state has text, or the request fails with a 400 naming the unused key.
Practical tips
- Check spans before indexing. Spans are Unicode code point offsets, and in a list an item can have
nullinstead of offsets. - Watch
truncated. If it istrue, the provider only read the start of your text; split long documents. - Use the right model. Need dates, numbers or estimates? Clairic, Schematron or Sage. Need exact quoted passages? Perplexity Decider.
- Provider options go under
provider.options.<slug>, for examplereturn_state_textfor Clairic. Wrong keys fail with a400. - Treat it as alpha. Wrap the call in one function (as above) so a schema change touches one place.
Takeaway
The Extractions API turns “please give me JSON” prompts into a typed contract: you declare fields, and you get values, null probabilities and source spans back. For invoices, forms, support emails and receipts, that makes validation and human review far simpler. Start with the curl quickstart, wrap it in a small client like the one above, and keep an eye on the API reference while the route is in alpha.
