Un endpoint.
Un en-tête.
Un fichier.
Tout ce qui suit est vérifié contre l’API en service. URL de base https://api.optii.eu
Authentification
Envoyez votre clé dans l’en-tête X-API-Key. Les clés commencent par optii_sk_ et ne sont affichées qu’une fois, à la création. Les clés sont stockées hachées ; une clé perdue se remplace, elle ne se récupère pas.
Chaque réponse porte un en-tête X-Request-ID. Citez-le quand vous nous écrivez.
curl https://api.optii.eu/ocr/extract \ -H "X-API-Key: optii_sk_…" \ -F "file=@document.pdf"
POST/ocr/extract
Envoi multipart d’un fichier. Renvoie chaque ligne avec sa boîte englobante et sa confiance, dans l’ordre de lecture, page après page.
Paramètres de requête
- structured
- false (défaut) pour les lignes, true pour les champs de documents belges (section suivante).
- format
- v2 (défaut). legacy renvoie le format de facture à plat des clients de 2025 ; ne l’utilisez pas pour du nouveau code.
Réponse 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
} Champs
- text
- le texte complet, lignes jointes par des sauts de ligne
- lines[]
- un objet par ligne : text, confidence (0 à 1), bbox sous forme de quatre coins [[x, y], …] en pixels de page, dans le sens horaire depuis le coin supérieur gauche
- confidence
- confiance moyenne des lignes
- source
- text, ocr ou hybrid, voir Source et qualité
- quality
- blur, long_side_px, low_confidence_ratio, warnings[]
- file_hash
- SHA-256 du fichier, premiers caractères ; le même fichier donne toujours la même valeur
- processing_time_ms
- temps serveur en millisecondes, envoi exclu
POST/ocr/extract?structured=true
Le même appel avec ?structured=true. Le moteur détecte le type de document d’après son texte (invoice, credit_note, debit_note, receipt, sinon unknown), extrait les champs belges et lance les contrôles. Quand le classifieur est confiant, document_type porte son label à la place.
Un champ absent de la page vaut null. Les montants sont des nombres avec un point, les dates sont gardées telles qu’imprimées (jj/mm/aaaa sur la plupart des documents belges).
{
"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
} Contrôles
- vat_checkdigit
- les chiffres de contrôle du numéro de TVA belge (97 moins les huit premiers chiffres mod 97)
- iban_mod97
- l’IBAN réarrangé mod 97 vaut 1
- structured_comm_mod97
- les chiffres de contrôle de la communication structurée +++xxx/xxxx/xxxxx+++
- totals_sum · vat_subtotals_sum · vat_bases_sum
- total_excl_vat + total_vat = total_incl_vat au cent près
- line_items_sum
- la somme des lignes égale le total HTVA quand elles sont présentes
POST/ocr/classify
Envoi multipart d’un fichier. Le texte est lu (couche texte ou OCR) et classé par un modèle XLM-RoBERTa parmi 32 types de documents belges.
Paramètres de requête
- top_k
- nombre de prédictions renvoyées, de 1 à 20, 5 par défaut.
Réponse 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
- le label quand confident vaut true, sinon unknown
- confident
- true quand la probabilité la plus haute atteint le seuil et que le label n’est pas un repli
- predictions[]
- label, confidence, rank, meilleur en premier
Les labels sont en snake_case avec un préfixe de famille, par exemple acc_purchase_invoice ou acc_sales_invoice. La liste complète est dans le tableau de bord.
Source et qualité
Chaque réponse d’extraction indique d’où vient le texte.
- text
- le PDF avait une couche texte utilisable ; les caractères sont exacts, chaque ligne a une confiance de 1
- ocr
- chaque page est passée par le moteur
- hybrid
- certaines pages avaient une couche, d’autres non
quality quality est calculé sur les pages passées par l’OCR :
- blur
- variance du laplacien sur une image en niveaux de gris réduite à 1500 px ; plus bas, plus flou
- long_side_px
- le plus petit grand côté de page en pixels
- low_confidence_ratio
- part des lignes sous 0.80 de confiance
- warnings[]
- l’un de low_resolution (sous 1500 px), blurry (sous 400), low_confidence (ratio au-dessus de 0.15)
Quand warnings n’est pas vide, montrez la page à l’utilisateur et demandez un nouveau scan. Les chiffres peuvent être justes ; vous ne pouvez pas le savoir.
Idempotence
Envoyez un en-tête Idempotency-Key (toute chaîne jusqu’à 255 caractères, un UUID convient). Pendant 24 heures, la même clé avec le même fichier et les mêmes paramètres renvoie la réponse stockée, sans seconde lecture ni second décompte. La même clé avec un autre fichier répond 409 idempotency_conflict.
L’empreinte est le SHA-256 du fichier plus les paramètres ; un envoi multipart rejoué correspond donc même si la boundary diffère.
Limites de débit et quotas
Les limites sont par plan et par clé. Le tableau est la configuration en service.
| Plan | Requêtes / min | Fichier max | Clés | Quota mensuel |
|---|---|---|---|---|
| Free | 10 | 5 MB | 1 | 500 |
| Pro | 60 | 20 MB | 5 | 10 000 |
| Enterprise | 300 | 20 MB | 100 | illimité |
Au-delà de la limite de débit : 429 rate_limited avec Retry-After. Au-delà du quota : 429 avec un message qui nomme le quota. L’endpoint public de démo autorise 10 requêtes par minute et par IP et 5 MB.
Erreurs
Chaque erreur est un JSON à deux champs : error, un code en snake_case, et message, une phrase pour les humains. Ne parsez jamais le 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)" } | Statut | error | Quand | Que faire |
|---|---|---|---|
| 400 | unsupported_file_type | extension hors pdf png jpg jpeg tiff tif bmp webp | convertissez le fichier |
| 401 | unauthorized | clé absente ou inconnue | vérifiez la clé dans le tableau de bord |
| 403 | forbidden | le plan ne le permet pas (nombre de clés, taille) | voir les tarifs |
| 409 | idempotency_conflict | même Idempotency-Key, fichier différent | utilisez une nouvelle clé |
| 413 | file_too_large | au-delà du fichier max du plan | compressez ou découpez |
| 422 | validation_failed | pas de champ file, mauvais paramètre de requête | relisez la section requête |
| 429 | rate_limited | limite de débit ou quota mensuel | attendez Retry-After, ou passez au plan supérieur |
| 500 | processing_failed | le moteur n’a pas pu lire le fichier | réessayez une fois, puis écrivez-nous avec l’id de requête |
| 503 | model_unavailable | le moteur se charge après un redémarrage | réessayez dans quelques secondes |
| 503 | db_unavailable | base d’usage indisponible ; les lectures fonctionnent | réessayez |
Formats et limites
Entrée : PDF (texte ou scanné, multipage), PNG, JPG, TIFF, BMP, WebP. Max 20 MB avec une clé, 5 MB sur la démo. Langues lues : français, néerlandais, allemand, anglais ; le moteur gère 50 écritures mais l’extraction de champs et le classifieur sont belges.
Sortie : JSON, UTF-8. L’ordre des lignes est l’ordre de lecture par page. Les boîtes sont des polygones dans l’espace de pixels de la page rendue.
Extraits de code
Aucun SDK à installer. Trois extraits qui fonctionnent tels quels.
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
Le contrat lisible par machine est à https://api.optii.eu/openapi.json. C’est le manifeste d’outils sélectionné qu’utilise la plateforme chut.app ; les routes du tableau de bord n’y figurent pas.