Ein Endpunkt.
Ein Header.
Eine Datei.
Alles hier unten ist gegen die laufende API verifiziert. Basis-URL https://api.optii.eu
Authentifizierung
Senden Sie Ihren Schlüssel im Header X-API-Key. Schlüssel beginnen mit optii_sk_ und werden einmal gezeigt, bei der Erstellung. Schlüssel werden gehasht gespeichert; ein verlorener Schlüssel wird rotiert, nicht wiederhergestellt.
Jede Antwort trägt einen X-Request-ID-Header. Nennen Sie ihn, wenn Sie uns schreiben.
curl https://api.optii.eu/ocr/extract \ -H "X-API-Key: optii_sk_…" \ -F "file=@document.pdf"
POST/ocr/extract
Multipart-Upload einer Datei. Liefert jede Zeile mit Bounding-Box und Konfidenz, in Lesereihenfolge, Seite für Seite.
Query-Parameter
- structured
- false (Standard) für Zeilen, true für belgische Dokumentfelder (nächster Abschnitt).
- format
- v2 (Standard). legacy liefert die flache Rechnungsform der Clients von 2025; nicht für neuen Code verwenden.
Antwort 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
} Felder
- text
- der volle Text, Zeilen mit Zeilenumbrüchen verbunden
- lines[]
- ein Objekt pro Zeile: text, confidence (0 bis 1), bbox als vier Eckpunkte [[x, y], …] in Seitenpixeln, im Uhrzeigersinn ab oben links
- confidence
- mittlere Zeilenkonfidenz
- source
- text, ocr oder hybrid, siehe Quelle und Qualität
- quality
- blur, long_side_px, low_confidence_ratio, warnings[]
- file_hash
- SHA-256 der Datei, erste Zeichen; dieselbe Datei ergibt immer denselben Wert
- processing_time_ms
- Serverzeit in Millisekunden, ohne Upload
POST/ocr/extract?structured=true
Derselbe Aufruf mit ?structured=true. Die Engine erkennt die Dokumentart aus dem Text (invoice, credit_note, debit_note, receipt, sonst unknown), extrahiert die belgischen Felder und führt die Prüfungen aus. Ist der Klassifikator sicher, trägt document_type stattdessen dessen Label.
Ein Feld, das nicht auf der Seite steht, ist null. Beträge sind Zahlen mit Punkt, Daten bleiben wie gedruckt (dd/mm/yyyy auf den meisten belgischen Belegen).
{
"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
} Prüfungen
- vat_checkdigit
- die Prüfziffern der belgischen MwSt.-Nummer (97 minus die ersten acht Ziffern mod 97)
- iban_mod97
- die umgestellte IBAN mod 97 ergibt 1
- structured_comm_mod97
- die Prüfziffern der strukturierten Mitteilung +++xxx/xxxx/xxxxx+++
- totals_sum · vat_subtotals_sum · vat_bases_sum
- total_excl_vat + total_vat = total_incl_vat auf einen Cent genau
- line_items_sum
- die Positionen ergeben die Summe ohne MwSt., wenn vorhanden
POST/ocr/classify
Multipart-Upload einer Datei. Der Text wird gelesen (Textebene oder OCR) und von einem XLM-RoBERTa-Modell in einen von 32 belgischen Dokumenttypen eingeordnet.
Query-Parameter
- top_k
- Anzahl zurückgegebener Vorhersagen, 1 bis 20, Standard 5.
Antwort 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
- das Label, wenn confident true ist, sonst unknown
- confident
- true, wenn die höchste Wahrscheinlichkeit die Schwelle erreicht und das Label kein Fallback ist
- predictions[]
- label, confidence, rank, bestes zuerst
Labels sind snake_case mit Familienpräfix, zum Beispiel acc_purchase_invoice oder acc_sales_invoice. Die vollständige Liste steht im Dashboard.
Quelle und Qualität
Jede extract-Antwort sagt, woher der Text stammt.
- text
- das PDF hatte eine brauchbare Textebene; Zeichen sind exakt, jede Zeile hat Konfidenz 1
- ocr
- jede Seite lief durch die Engine
- hybrid
- manche Seiten hatten eine Ebene, manche nicht
quality quality wird auf den Seiten berechnet, die durch OCR liefen:
- blur
- Varianz des Laplace-Operators auf einem auf 1500 px verkleinerten Graustufenbild; niedriger ist unschärfer
- long_side_px
- die kleinste lange Seitenkante in Pixeln
- low_confidence_ratio
- Anteil der Zeilen unter 0.80 Konfidenz
- warnings[]
- eines von low_resolution (unter 1500 px), blurry (unter 400), low_confidence (Anteil über 0.15)
Ist warnings nicht leer, zeigen Sie dem Nutzer die Seite und verlangen Sie einen neuen Scan. Die Zahlen können trotzdem stimmen; wissen können Sie es nicht.
Idempotenz
Senden Sie einen Idempotency-Key-Header (beliebiger String bis 255 Zeichen, eine UUID ist in Ordnung). 24 Stunden lang liefert derselbe Key mit derselben Datei und denselben Parametern die gespeicherte Antwort, ohne zweites Lesen und ohne zweite Zählung. Derselbe Key mit einer anderen Datei antwortet 409 idempotency_conflict.
Der Fingerabdruck ist der SHA-256 der Datei plus Parameter, sodass ein wiederholter Multipart-Upload auch bei anderer Boundary passt.
Rate-Limits und Kontingente
Limits gelten pro Tarif und pro Schlüssel. Die Tabelle ist die Live-Konfiguration.
| Tarif | Anfragen / min | Max. Datei | Schlüssel | Monatskontingent |
|---|---|---|---|---|
| Free | 10 | 5 MB | 1 | 500 |
| Pro | 60 | 20 MB | 5 | 10.000 |
| Enterprise | 300 | 20 MB | 100 | unbegrenzt |
Über dem Rate-Limit: 429 rate_limited mit Retry-After. Über dem Kontingent: 429 mit einer Meldung, die das Kontingent nennt. Der öffentliche Demo-Endpunkt erlaubt 10 Anfragen pro Minute pro IP und 5 MB.
Fehler
Jeder Fehler ist JSON mit zwei Feldern: error, ein snake_case-Code, und message, ein Satz für Menschen. Parsen Sie die message nie.
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)" } | Status | error | Wann | Was tun |
|---|---|---|---|
| 400 | unsupported_file_type | Endung nicht in pdf png jpg jpeg tiff tif bmp webp | Datei konvertieren |
| 401 | unauthorized | fehlender oder unbekannter Schlüssel | Schlüssel im Dashboard prüfen |
| 403 | forbidden | der Tarif erlaubt es nicht (Schlüssellimit, Größe) | siehe Preise |
| 409 | idempotency_conflict | gleicher Idempotency-Key, andere Datei | neuen Key verwenden |
| 413 | file_too_large | über der max. Dateigröße des Tarifs | komprimieren oder aufteilen |
| 422 | validation_failed | kein Dateifeld, falscher Query-Parameter | Abschnitt zur Anfrage lesen |
| 429 | rate_limited | Rate-Limit oder Monatskontingent | Retry-After abwarten oder upgraden |
| 500 | processing_failed | die Engine konnte die Datei nicht lesen | einmal wiederholen, dann mit der Request-ID an uns schreiben |
| 503 | model_unavailable | die Engine lädt nach einem Neustart | in ein paar Sekunden wiederholen |
| 503 | db_unavailable | Nutzungsdatenbank nicht erreichbar; Lesen funktioniert weiter | wiederholen |
Formate und Limits
Eingabe: PDF (Text oder gescannt, mehrseitig), PNG, JPG, TIFF, BMP, WebP. Max. 20 MB mit Schlüssel, 5 MB in der Demo. Gelesene Sprachen: Französisch, Niederländisch, Deutsch, Englisch; die Engine beherrscht 50 Schriften, aber Feldextraktion und Klassifikator sind belgisch.
Ausgabe: JSON, UTF-8. Die Zeilenreihenfolge ist die Lesereihenfolge pro Seite. Rahmen sind Polygone im Pixelraum der gerenderten Seite.
Snippets
Kein SDK zu installieren. Drei Snippets, die so funktionieren, wie sie sind.
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"
import requests, uuid
with open("facture-2024-0847.pdf", "rb") as f:
r = requests.post(
"https://api.optii.eu/ocr/extract",
params={"structured": "true"},
headers={"X-API-Key": "optii_sk_…", "Idempotency-Key": str(uuid.uuid4())},
files={"file": f},
timeout=90,
)
r.raise_for_status()
doc = r.json()
print(doc["supplier_vat"], doc["total_incl_vat"], doc["validations"])import { readFile } from "node:fs/promises";
const form = new FormData();
form.append("file", new Blob([await readFile("facture-2024-0847.pdf")]), "facture-2024-0847.pdf");
const res = await fetch("https://api.optii.eu/ocr/extract?structured=true", {
method: "POST",
headers: { "X-API-Key": "optii_sk_…", "Idempotency-Key": crypto.randomUUID() },
body: form,
});
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`);
const doc = await res.json();
console.log(doc.supplier_vat, doc.total_incl_vat, doc.validations); OpenAPI
Der maschinenlesbare Vertrag liegt unter https://api.optii.eu/openapi.json. Es ist das kuratierte Tool-Manifest der chut.app-Plattform; die Dashboard-Routen sind nicht enthalten.