optii by chut.app

One endpoint.
One header.
One file.

Everything below is verified against the running API. Base URL https://api.optii.eu

Authentication

Send your key in the X-API-Key header. Keys start with optii_sk_ and are shown once, at creation. Keys are stored hashed; a lost key is rotated, not recovered.

Every response carries an X-Request-ID header. Quote it when you write to us.

curl https://api.optii.eu/ocr/extract \
  -H "X-API-Key: optii_sk_…" \
  -F "file=@document.pdf"

POST/ocr/extract

Multipart upload of one file. Returns every line with its bounding box and confidence, in reading order, page after page.

Query parameters

structured
false (default) for lines, true for Belgian document fields (next section).
format
v2 (default). legacy returns the flat invoice shape of 2025 clients; do not use it for new code.

Response 200

{
  "success": true,
  "text": "DUMONT & FILS SPRL\nRue de la Loi 42 · 1000 Bruxelles\nTVA BE0202239951\n…",
  "lines": [
    { "text": "DUMONT & FILS SPRL", "confidence": 0.996, "bbox": [[50.0, 50.0], [304.0, 50.0], [304.0, 84.0], [50.0, 84.0]] },
    { "text": "Rue de la Loi 42 · 1000 Bruxelles", "confidence": 0.991, "bbox": [[56.0, 82.0], [286.0, 82.0], [286.0, 100.0], [56.0, 100.0]] },
    { "text": "TVA BE0202239951", "confidence": 0.993, "bbox": [[56.0, 106.0], [198.0, 106.0], [198.0, 124.0], [56.0, 124.0]] }
  ],
  "confidence": 0.991,
  "source": "ocr",
  "quality": { "blur": 812.4, "long_side_px": 2339, "low_confidence_ratio": 0.0, "warnings": [] },
  "file_hash": "a3b2c1d4e5f6",
  "processing_time_ms": 342
}

Fields

text
the full text, lines joined with newlines
lines[]
one object per line: text, confidence (0 to 1), bbox as four corner points [[x, y], …] in page pixels, clockwise from the top left
confidence
mean line confidence
source
text, ocr or hybrid, see Source and quality
quality
blur, long_side_px, low_confidence_ratio, warnings[]
file_hash
SHA-256 of the file, first characters; the same file always gives the same value
processing_time_ms
server time in milliseconds, upload excluded

POST/ocr/extract?structured=true

The same call with ?structured=true. The engine detects the document kind from its text (invoice, credit_note, debit_note, receipt, else unknown), extracts the Belgian fields and runs the checks. When the classifier is confident, document_type carries its label instead.

A field that is not on the page is null. Amounts are numbers with a dot, dates are kept as printed (dd/mm/yyyy on most Belgian paper).

{
  "document_type": "invoice",
  "source": "ocr",
  "quality": { "blur": 812.4, "long_side_px": 2339, "low_confidence_ratio": 0.0, "warnings": [] },
  "supplier_name": "DUMONT & FILS SPRL",
  "supplier_vat": "BE0202239951",
  "supplier_iban": "BE68539007547034",
  "document_number": "2024-0847",
  "document_date": "15/03/2024",
  "due_date": "14/04/2024",
  "structured_communication": "+++123/4567/89002+++",
  "currency": "EUR",
  "line_items": [
    { "description": "Conseil juridique — mars 2024", "quantity": 10, "unit_price": 125.0, "line_total": 1250.0, "vat_rate": 21.0 }
  ],
  "vat_subtotals": [ { "rate": 21.0, "base_amount": 1250.0, "vat_amount": 262.5 } ],
  "total_excl_vat": 1250.0,
  "total_vat": 262.5,
  "total_incl_vat": 1512.5,
  "validations": [
    { "check": "vat_checkdigit", "passed": true, "message": "" },
    { "check": "iban_mod97", "passed": true, "message": "" },
    { "check": "structured_comm_mod97", "passed": true, "message": "" },
    { "check": "totals_sum", "passed": true, "message": "" }
  ],
  "confidence": 0.91,
  "processing_time_ms": 342
}

Checks

vat_checkdigit
the Belgian VAT number’s check digits (97 minus the first eight digits mod 97)
iban_mod97
the IBAN rearranged mod 97 equals 1
structured_comm_mod97
the structured communication +++xxx/xxxx/xxxxx+++ check digits
totals_sum · vat_subtotals_sum · vat_bases_sum
total_excl_vat + total_vat = total_incl_vat within one cent
line_items_sum
the line items sum to the total excl. VAT when present

POST/ocr/classify

Multipart upload of one file. The text is read (text layer or OCR) and classified by an XLM-RoBERTa model into one of 32 Belgian document types.

Query parameters

top_k
number of predictions returned, 1 to 20, default 5.

Response 200

{
  "success": true,
  "document_type": "acc_purchase_invoice",
  "confidence": 0.9361,
  "confident": true,
  "predictions": [
    { "label": "acc_purchase_invoice", "confidence": 0.9361, "rank": 1 },
    { "label": "acc_sales_invoice", "confidence": 0.0298, "rank": 2 },
    { "label": "acc_credit_note", "confidence": 0.0112, "rank": 3 }
  ],
  "processing_time_ms": 388
}
document_type
the label when confident is true, otherwise unknown
confident
true when the top probability is at or above the threshold and the label is not a fallback
predictions[]
label, confidence, rank, best first

Labels are snake_case with a family prefix, for example acc_purchase_invoice or acc_sales_invoice. The full list is on the dashboard.

Source and quality

Every extract response says where the text came from.

text
the PDF had a usable text layer; characters are exact, every line has confidence 1
ocr
every page went through the engine
hybrid
some pages had a layer, some did not

quality quality is computed on the pages that went through OCR:

blur
variance of the Laplacian on a grayscale downscaled to 1500 px; lower is blurrier
long_side_px
the smallest page long side in pixels
low_confidence_ratio
share of lines under 0.80 confidence
warnings[]
any of low_resolution (under 1500 px), blurry (under 400), low_confidence (ratio over 0.15)

When warnings is not empty, show the user the page and ask for a rescan. The numbers may still be right; you cannot know.

Idempotency

Send an Idempotency-Key header (any string up to 255 characters, a UUID is fine). For 24 hours the same key with the same file and parameters returns the stored response without a second read and without a second count. The same key with a different file answers 409 idempotency_conflict.

The fingerprint is the SHA-256 of the file plus the parameters, so a retried multipart upload matches even when the boundary differs.

Rate limits and quotas

Limits are per plan and per key. The table is the live configuration.

PlanRequests / minMax fileKeysMonthly quota
Free105 MB1500
Pro6020 MB510.000
Enterprise30020 MB100unlimited

Over the rate limit: 429 rate_limited with Retry-After. Over the quota: 429 with a message that names the quota. The public demo endpoint allows 10 requests per minute per IP and 5 MB.

Errors

Every error is JSON with two fields: error, a snake_case code, and message, a sentence for humans. Never parse the message.

HTTP/1.1 413 Payload Too Large
X-Request-ID: 6f1c0a2b9d3e
Content-Type: application/json

{ "error": "file_too_large", "message": "File too large (max 5MB)" }
StatuserrorWhenWhat to do
400unsupported_file_typeextension not in pdf png jpg jpeg tiff tif bmp webpconvert the file
401unauthorizedmissing or unknown keycheck the key in the dashboard
403forbiddenthe plan does not allow it (key limit, size)see pricing
409idempotency_conflictsame Idempotency-Key, different fileuse a new key
413file_too_largeover the plan’s max filecompress or split
422validation_failedno file field, bad query parameterread the request section
429rate_limitedrate limit or monthly quotawait for Retry-After, or upgrade
500processing_failedthe engine could not read the fileretry once, then write to us with the request id
503model_unavailablethe engine is loading after a restartretry in a few seconds
503db_unavailableusage database down; reads still workretry

Formats and limits

Input: PDF (text or scanned, multi-page), PNG, JPG, TIFF, BMP, WebP. Max 20 MB with a key, 5 MB on the demo. Languages read: French, Dutch, German, English; the engine handles 50 scripts but the field extraction and classifier are Belgian.

Output: JSON, UTF-8. Line order is reading order per page. Bounding boxes are polygons in the pixel space of the rendered page.

Snippets

No SDK to install. Three snippets that work as they are.

curl -X POST "https://api.optii.eu/ocr/extract?structured=true" \
  -H "X-API-Key: optii_sk_…" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@facture-2024-0847.pdf"

OpenAPI

The machine-readable contract is at https://api.optii.eu/openapi.json. It is the curated tool manifest used by the chut.app platform; the dashboard routes are not in it.

Changelog

See the changelog →