Extraction models
An extraction model turns supplied evidence into named values your application can inspect. Cloudflare's Clairic, available as cf.clairic through AvalAI's /v1/extraction, adds typed computation, estimates, and forecasts to this pattern. It can propose an invoice identifier, a payment deadline, an amount including shipping, or an expected payment delay. These are different kinds of claims and need different checks.
Use the Extraction API reference for complete request/response fields and curl, Python, and Node.js examples. This guide develops the application workflow: extract → validate with code and authoritative records → review → act under application permissions. Its invoice fixture and answer are synthetic. It does not report live AvalAI validation or measured production accuracy.
Choose the right output
| Need | Model/API approach | Application responsibility |
|---|---|---|
| An identifier copied from a document | Extraction text | Check the source span, identifier syntax, and record lookup. |
| A numeric amount or calendar date | Extraction number / date | Check arithmetic, currency, valid dates, and authoritative terms. |
| Several names, amounts, or dates | Extraction list | Validate every element, duplicates, completeness, and source evidence. |
| A time, timestamp, or elapsed quantity | Extraction time, datetime, or duration | Establish format, timezone, ambiguity, and unit rules. |
| An uncertain future quantity | Extraction estimate / forecast | Preserve uncertainty and evaluate against later outcomes. |
| A known category, condition, or rubric | Decisions / SystemOne | Validate allowed answers and choose a review/routing policy. |
| A summary, explanation, or custom nested object | A suitable text model with Structured Outputs | Validate both the schema and factual content. |
Extraction's fields describe typed outputs rather than a custom JSON Schema. Decision choices describe a supplied answer set. A predicted payment delay is neither a verbatim extracted term nor a decision score. Keep these meanings explicit in stored records and user interfaces.
Design evidence and fields together
Keep document boundaries, source identifiers, and relevant terms in the evidence. state must be text on this endpoint; format structured records clearly rather than sending a native JSON object. Do not omit currency, reference dates, or the difference between an issue date and a payment date. Minimize unrelated text that can distract from the requested field.
Use instructions to define the role. “Purchase order number” distinguishes po_number from invoice_number; “Amount due including shipping” explains which total to compute. Prefer one clear purpose per field. Do not ask one field to invent missing source data to make a record complete.
Document contents are untrusted evidence. They can contain misleading text or embedded instructions. Keep account permissions, trusted supplier records, credentials, payment destinations, and executable actions outside that evidence. A schema and a confidence score do not establish the trustworthiness of a document.
For images, use embedded PNG/JPEG/WebP with the upstream limits explained in the reference. Test OCR mistakes, image-only input, low-resolution images, and non-ASCII text separately. Do not apply text span assumptions to pixel regions. Audio, video, PDFs, and remote image URLs are not documented inputs for this Extraction route.
Separate observations, calculations, and predictions
Store the evidence source and the field type with each value. A text identifier may be checked against an exact substring; a computed total must be compared with deterministic arithmetic; an estimated future payment date needs later outcome evaluation. A successful parse does not make these checks interchangeable.
Clairic can return value: null while still returning guess, span, and a high confidence. Accept only the primary value after validation. A plausible invoice number must never become a purchase-order number merely because the missing PO field guessed it.
Use p_null, confidence, and prediction intervals as separate signals. Their public schemas do not specify an application calibration formula. Choose thresholds using labeled data from your workflow. Adjusting null_threshold changes how often the model abstains; it does not prove better factual accuracy. If metadata is missing, keep it missing rather than replacing it with zero or inventing certainty.
Complete invoice validation example
The program below reuses the reference's six-field request. It uses Python's standard library, makes one request with a 120-second socket timeout, and prints either ready_for_review or review_required. It performs no payment, database write, or notification. Even a validated candidate proceeds to a human or approved application review process.
Save the block as invoice_extraction.py. Run python3 invoice_extraction.py --self-check to exercise the validator without an API key or network request. With AVALAI_API_KEY already set, python3 invoice_extraction.py sends one potentially billable synthetic request. The executable self-check is not a compatibility test of the live route.
The trusted fixture fixes the expected identifier, issue date, net terms, quantities, prices, and shipping. In a real workflow, retrieve those values from your purchase-order or supplier system; deriving all comparison values from the same model answer would make the validation circular. The example's 0.95 confidence gate is an illustrative application choice, not a calibrated AvalAI guarantee.
import copy
import json
import math
import os
import sys
from datetime import date, timedelta
from decimal import Decimal, InvalidOperation
from urllib.error import HTTPError
from urllib.request import Request, urlopen
payload = {
"model": "cf.clairic",
"state": "Invoice INV-2291 from Northwind Supply, issued 2026-09-02, net 30. 4 pallets at $250.00 each, shipping $80.00. Contact: Dana Ruiz, Priya Shah.",
"fields": {
"invoice_number": {"type": "text"},
"due_date": {
"type": "date",
"instructions": "Issue date plus the net payment days",
},
"total": {
"type": "number",
"decimals": 2,
"unit": "USD",
"instructions": "Amount due including shipping",
},
"contacts": {"type": "list", "items": "text"},
"po_number": {"type": "text", "instructions": "Purchase order number"},
"days_to_pay": {
"type": "estimate",
"min": 0,
"max": 120,
"step": 1,
"instructions": "Days after issue until this invoice is paid",
},
},
}
trusted = {
"invoice_number": "INV-2291",
"issue_date": date(2026, 9, 2),
"net_days": 30,
"quantity": Decimal("4"),
"unit_price": Decimal("250.00"),
"shipping": Decimal("80.00"),
}
def validate_candidate(result):
reasons = []
if not isinstance(result, dict) or result.get("model") != "cf.clairic":
return {
"status": "review_required",
"reasons": ["unexpected_model_or_envelope"],
}
if result.get("truncated") is not False:
reasons.append("truncated_or_unknown_evidence")
answers = result.get("answers")
if not isinstance(answers, dict):
return {"status": "review_required", "reasons": reasons + ["missing_answers"]}
def accepted(name, kind):
answer = answers.get(name)
if (
not isinstance(answer, dict)
or answer.get("type") != kind
or "value" not in answer
):
reasons.append(f"{name}:malformed_answer")
return None
value = answer["value"]
if value is None:
reasons.append(f"{name}:unresolved")
return None
confidence = answer.get("confidence")
if (
isinstance(confidence, bool)
or not isinstance(confidence, (int, float, Decimal))
or not math.isfinite(confidence)
or not 0.95 <= confidence <= 1
):
reasons.append(f"{name}:confidence_review")
return value
invoice_number = accepted("invoice_number", "text")
if invoice_number is not None:
if (
not isinstance(invoice_number, str)
or invoice_number != trusted["invoice_number"]
):
reasons.append("invoice_number:trusted_record_mismatch")
span = answers["invoice_number"].get("span")
valid_span = (
isinstance(span, list)
and len(span) == 2
and all(type(i) is int for i in span)
and 0 <= span[0] <= span[1] <= len(payload["state"])
)
if not valid_span or payload["state"][span[0] : span[1]] != invoice_number:
reasons.append("invoice_number:invalid_source_span")
due_date = accepted("due_date", "date")
if due_date is not None:
try:
if not isinstance(due_date, str):
raise ValueError("date must be text")
parsed_date = date.fromisoformat(due_date)
expected_date = trusted["issue_date"] + timedelta(days=trusted["net_days"])
if parsed_date.isoformat() != due_date or parsed_date != expected_date:
reasons.append("due_date:trusted_terms_mismatch")
except ValueError:
reasons.append("due_date:invalid_calendar_date")
total = accepted("total", "number")
if total is not None:
try:
if isinstance(total, bool) or not isinstance(total, (int, float, Decimal)):
raise InvalidOperation
amount = Decimal(str(total))
expected_total = (
trusted["quantity"] * trusted["unit_price"] + trusted["shipping"]
)
if not amount.is_finite() or amount < 0 or amount != expected_total:
reasons.append("total:trusted_arithmetic_mismatch")
except (InvalidOperation, ValueError):
reasons.append("total:invalid_number")
contacts = accepted("contacts", "list")
if contacts is not None and (
not isinstance(contacts, list)
or not all(
isinstance(item, str) and item.strip() and item in payload["state"]
for item in contacts
)
or len(set(contacts)) != len(contacts)
):
reasons.append("contacts:invalid_or_duplicate_items")
# The fixture has no PO; a guess is never an accepted PO number.
po = answers.get("po_number")
if (
not isinstance(po, dict)
or po.get("type") != "text"
or "value" not in po
or po["value"] is not None
):
reasons.append("po_number:unexpected_value")
# Keep the prediction separate from payable terms and validation.
prediction = answers.get("days_to_pay")
if (
not isinstance(prediction, dict)
or prediction.get("type") != "estimate"
or "value" not in prediction
):
reasons.append("days_to_pay:malformed_prediction")
return {
"status": "review_required" if reasons else "ready_for_review",
"reasons": reasons,
"candidate": {
"invoice_number": invoice_number,
"due_date": due_date,
"total": total,
"contacts": contacts,
},
"prediction": prediction,
}
def self_check():
fixture = {
"model": "cf.clairic",
"truncated": False,
"answers": {
"invoice_number": {
"type": "text",
"value": "INV-2291",
"span": [8, 16],
"confidence": 1,
},
"due_date": {"type": "date", "value": "2026-10-02", "confidence": 1},
"total": {"type": "number", "value": 1080, "confidence": 1},
"contacts": {
"type": "list",
"value": ["Dana Ruiz", "Priya Shah"],
"confidence": 1,
},
"po_number": {
"type": "text",
"value": None,
"guess": "INV-2291",
"confidence": 1,
},
"days_to_pay": {"type": "estimate", "value": 30, "p10": 21, "p90": 38},
},
}
assert validate_candidate(fixture)["status"] == "ready_for_review"
fractional = copy.deepcopy(fixture)
fractional["answers"]["total"]["confidence"] = Decimal("0.9999")
assert validate_candidate(fractional)["status"] == "ready_for_review"
for field, value in [
("total", True),
("total", 1),
("total", float("nan")),
("due_date", "2026-02-30"),
("po_number", "INV-2291"),
]:
invalid = copy.deepcopy(fixture)
invalid["answers"][field]["value"] = value
assert validate_candidate(invalid)["status"] == "review_required"
invalid = copy.deepcopy(fixture)
invalid["answers"]["invoice_number"]["value"] = None
invalid["answers"]["invoice_number"]["guess"] = "INV-2291"
assert validate_candidate(invalid)["status"] == "review_required"
invalid = copy.deepcopy(fixture)
invalid["truncated"] = True
assert validate_candidate(invalid)["status"] == "review_required"
print("self-check passed; no network request")
if __name__ == "__main__":
if sys.argv[1:] == ["--self-check"]:
self_check()
elif sys.argv[1:]:
raise SystemExit("usage: invoice_extraction.py [--self-check]")
else:
request = Request(
"https://api.avalai.ir/v1/extraction",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {os.environ['AVALAI_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
result = json.load(response, parse_float=Decimal)
request_id = response.headers.get("avalai-request-id")
except HTTPError as error:
print("avalai-request-id:", error.headers.get("avalai-request-id"))
print(
f"HTTP {error.code}: {error.read().decode('utf-8', errors='replace')}"
)
raise
print("avalai-request-id:", request_id)
print(json.dumps(validate_candidate(result), indent=2, default=str))The example preserves optional missing PO data as null and keeps days_to_pay outside payable terms. Add a required-PO review rule if your own purchasing policy requires one. The text fixture is ASCII, so its span check does not establish Unicode indexing behavior. The contact check validates supplied items, not completeness or identity in a trusted address book.
The prediction is retained for review, not consumed by the payment workflow. A forecasting application also needs checks for value types, finite summaries, ordered intervals, grid and horizon alignment, and missing/null steps. The public output schema leaves nested forecast structure broad; establish the actual route representation before implementing those checks.
Evaluate before operational use
Build labeled examples containing correct documents, missing fields, conflicting identifiers, duplicate line items, currency ambiguity, impossible dates, Unicode, OCR errors, and injected instructions. Measure accepted-value accuracy and the rate of unresolved/reviewed fields separately. A system that fills every field can perform worse than one that reliably abstains on missing evidence.
For computed amounts and dates, compare with trusted arithmetic and calendar rules. For estimates and forecasts, retain predictions before outcomes occur and measure later error, interval coverage, tail behavior, and change over time. Good extraction accuracy does not establish forecast quality. Preview status means compatibility and behavior should be rechecked after provider changes.
Retain the requested model ID, field definitions, reference timing, source identifier, HTTP/request ID, truncation flag, raw answer metadata, usage, and your validator's result. Keep API keys and sensitive document bodies out of shared logs. An echoed model ID is not a pinned upstream revision.
Handle incomplete results and failures
An HTTP 200 response still needs envelope, answer-type, and value validation. Missing fields, unknown types, invalid JSON, unexpected nulls, truncation, and invalid spans go to review; they are not substitutes for a zero amount or an empty identifier. Preserve the raw response for authorized diagnosis without silently repairing it.
Correct 400, 413, and 422 request problems before repeating the request. Inspect 429 bodies for terminal quota codes; retries cannot replenish exhausted credit. For temporary 429/503 failures, use a bounded retry policy only when repeating inference is appropriate, and account for additional billable input. The reference's clients and this program use one attempt so the initial failure is visible.
Payment execution, supplier changes, customer notifications, and database updates require their own permission and duplicate-action controls after validation and review. Keep those actions outside any inference retry loop. See the Extraction reference, error handling, and Cloudflare.