Référence API
URL de base : https://api.hotdoc.io. Les corps de requête et de réponse sont en JSON ; les noms de champs sont en camelCase. Exceptions : POST /v1/jobs/upload (réponse) et le corps de la requête webhook — tous deux utilisent snake_case.
Endpoints de traitement
| Méthode | Endpoint | Objet |
|---|---|---|
POST | /v1/jobs | créer un job |
GET | /v1/jobs/{id} | statut du job et liste des fichiers (sans les résultats de reconnaissance) |
GET | /v1/jobs/{id}/result | résultat complet : texte reconnu et réponses du modèle par fichier |
GET | /v1/jobs | lister les jobs du compte (pagination : pageSize, pageToken, filtre statusEq) |
POST | /v1/jobs/upload | envoyer un seul fichier (multipart/form-data) |
POST /v1/jobs — créer un job
Corps de la requête :
| Champ | Type | Requis | Description |
|---|---|---|---|
sourceUrls | string[] | oui | URL des fichiers à traiter |
prompts | string[] | non | instructions du modèle (seul le premier prompt s’exécute) ; sans prompts, le LLM n’est pas appelé |
neural | object | oui | configuration du modèle (voir « Connexion d’un modèle ») |
ocr | object | oui | configuration du fournisseur OCR (BYOK) ; toujours requise (voir « Configuration OCR ») |
extractionMode | enum | non | EXTRACTION_MODE_UNSPECIFIED | EXTRACTION_MODE_HYBRID | EXTRACTION_MODE_OCR_ALWAYS ; omis / UNSPECIFIED hérite de la valeur par défaut du compte (elle-même HYBRID par défaut) |
responseSchema | string | non | JSON Schema optionnel (chaîne brute) restreignant la réponse JSON du modèle ; autonome (uniquement des #/$defs internes), ≤128 KiB, profondeur ≤64, ≤10000 nœuds ; un schéma invalide est rejeté avec 400 à la création (voir « Sortie structurée » dans la documentation du Job API) |
merge | object | non | fusion facultative côté serveur des réponses par chunk en un seul résultat pour tout le document ; désactivée par défaut ; voir « Options de fusion » ci-dessous et « Fusion des résultats en chunks » dans la documentation du Job API |
consensus | object | non | vote de consensus facultatif sur k exécutions par chunk ; nécessite responseSchema (sinon 400) ; désactivé par défaut ; voir « Options de consensus » ci-dessous et « Exécutions de consensus » dans la documentation du Job API |
title | string | non | nom de job arbitraire |
metadata | map<string,string> | non | paires clé-valeur de chaînes arbitraires |
webhookUrl | string | non | endpoint absolu http/https à notifier à la fin du job (voir « Webhooks ») |
webhookSecret | string | non | secret HMAC facultatif pour signer les requêtes webhook ; accepté uniquement en entrée, jamais renvoyé dans les réponses |
idempotencyKey | string | non | clé client pour des réessais de création sûrs ; max 255 caractères (voir « Idempotence ») |
neural et ocr sont toujours requis, même lorsque prompts est vide. Avec un prompts vide, le modèle n’est pas appelé : chaque fichier est marqué JOB_LLM_STATUS_SKIPPED avec skipReason=no_prompt, et en cas d’OCR réussi le job se termine avec le statut JOB_STATUS_COMPLETE.
La réponse est le job créé au statut JOB_STATUS_NEW ; responseSchema est renvoyé dans cet objet (vide si non défini) ; consensus est renvoyé comme un objet peuplé — mode: "CONSENSUS_MODE_UNSPECIFIED" si non défini, jamais comme un champ omis ou null.
Configuration OCR
ocr sélectionne le backend de reconnaissance (OCR) par requête (BYOK) :
| Champ | Type | Requis | Description |
|---|---|---|---|
ocr.provider | enum | oui | l’un de NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI |
ocr.model | string | oui | l’identifiant du modèle du fournisseur, par ex. mistral-ocr-latest (Mistral) ou le modèle vision du fournisseur choisi |
ocr.providerKey | string | oui | clé BYOK pour le fournisseur OCR ; en entrée uniquement, jamais renvoyée |
Mistral est le backend OCR typique (mistral-ocr-latest) ; les autres fournisseurs exécutent
un OCR de type vision-chat. La clé est stockée chiffrée et supprimée avec le job.
Remarque. Le mode d’extraction est un champ de premier niveau, extractionMode, il ne fait pas partie de ocr : il détermine si l’OCR s’exécute toujours (EXTRACTION_MODE_OCR_ALWAYS) ou est appliqué par fichier à la discrétion du convertisseur (EXTRACTION_MODE_HYBRID, la valeur par défaut).
Quel modèle utiliser ? Comparez l’intelligence, le prix par token et les fournisseurs dans le comparatif LLM, puis choisissez le modèle adapté.
Options de fusion (merge)
merge est facultatif et désactivé par défaut — définissez merge.enabled pour l’activer. Tous les autres champs merge.* sont ignorés tant que enabled vaut false/est omis.
| Champ | Type | Requis | Description |
|---|---|---|---|
merge.enabled | bool | non | active la fusion côté serveur pour ce job |
merge.scope | enum | non | MERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (par défaut) | MERGE_SCOPE_JOB ; une entrée merged[] par fichier, ou une pour tous les fichiers |
merge.conflictPolicy | enum | non | MERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (par défaut) | MERGE_CONFLICT_POLICY_MAJORITY |
merge.dedupeBy | string[] | non | noms de champs JSON identifiant un enregistrement unique, pour la déduplication de contenu entre fichiers ; JSON + responseSchema uniquement ; 32 noms de champs au maximum — au-delà, la requête est rejetée avec 400 à la création du job ; vide/omis → déduplication technique uniquement |
merge.format | enum | non | MERGE_FORMAT_UNSPECIFIED | MERGE_FORMAT_AUTO (par défaut) | MERGE_FORMAT_JSON | MERGE_FORMAT_XML | MERGE_FORMAT_HTML | MERGE_FORMAT_MARKDOWN | MERGE_FORMAT_TEXT ; remplace la détection automatique du format par fichier |
Voir « Fusion des résultats en chunks » dans la documentation du Job API pour le parcours complet (portée, résolution des conflits, les deux types de déduplication).
Options de consensus (consensus)
consensus est facultatif et désactivé par défaut. Lorsqu’il est défini, consensus.mode doit être une valeur reconnue et le job doit définir responseSchema — un job avec consensus.mode défini mais sans responseSchema est rejeté avec 400 à la création.
| Champ | Type | Requis | Description |
|---|---|---|---|
consensus.mode | enum | non | CONSENSUS_MODE_UNSPECIFIED (désactivé, par défaut) | CONSENSUS_MODE_TWO_RUNS | CONSENSUS_MODE_THREE_RUNS | CONSENSUS_MODE_FIVE_RUNS ; une valeur de chaîne non reconnue est silencieusement ignorée par le gateway — le job s’exécute sans consensus, sans 400 |
Voir « Exécutions de consensus » dans la documentation du Job API pour le parcours complet (modes, lecture de minAgreement/disagreements, limites honnêtement énoncées).
Idempotence
Envoyez idempotencyKey pour rendre POST /v1/jobs sûr à réessayer. Un nouvel envoi avec la
même clé et des paramètres de requête identiques renvoie le job d’origine (aucun
doublon n’est créé). La même clé avec des paramètres différents est rejetée avec
400 "idempotency key reused with different request parameters". La clé fait au
plus 255 caractères ; générez un UUID frais par création logique.
GET /v1/jobs/{id}/result — résultat
Renvoie { "result": { "job": …, "ocr": [...], "llm": [...] } }. Exemple (abrégé) :
{
"result": {
"job": {
"id": "15b07304-…", "accountId": "…", "title": "Invoice #42",
"metadata": {}, "status": "JOB_STATUS_COMPLETE", "error": "",
"sourceUrls": ["https://example.com/invoice.pdf"],
"files": ["https://…/files/15b07304-…/invoice.pdf"],
"prompts": ["…"], "neural": { "type": "NEURAL_CLIENT_TYPE_XIAOMI", "model": "mimo-v2-flash", "chunkBudgetTokens": 240000 },
"created": "2026-06-17T16:57:49Z", "updated": "2026-06-17T16:58:05Z"
},
"ocr": [
{ "jobId": "15b07304-…", "file": "https://…/invoice.pdf",
"status": "JOB_OCR_STATUS_DONE", "content": "<p><b>…</b></p>",
"outputFormat": "html", "model": "pdf_fitz", "error": "", "duration": "149508116",
"created": "…", "updated": "…" }
],
"llm": [
{ "jobId": "15b07304-…", "file": "https://…/invoice.pdf",
"promptIndex": 0, "chunkIndex": 0, "chunkTotal": 1,
"status": "JOB_LLM_STATUS_DONE", "skipReason": "",
"model": "mimo-v2-flash", "content": "{\"total\": 15000}",
"error": "", "duration": "601925111", "created": "…", "updated": "…" }
]
}
}
neural dans la réponse n’inclut pas apiKey ; l’objet Job n’a pas de champ ocr — la clé OCR n’est jamais renvoyée. ocr[].content est le texte reconnu au format ocr[].outputFormat ; le format dépend du type de fichier : PDF et HTML → html, .xml → xml, .txt → plain, tout le reste (y compris les images et les fichiers bureautiques) → markdown. llm[].content est le texte de la réponse du modèle tel quel (votre prompt en détermine la structure ; il n’y a aucune validation côté serveur, sauf si le job a défini responseSchema — auquel cas llm[].schemaValid/schemaErrors rapportent la conformité). Dans l’exemple ci-dessus, les champs vides/à zéro ("", {}, 0) sont affichés par souci d’exhaustivité — protojson les omet, ils peuvent donc être absents d’une réponse réelle.
Champs ocr[] :
| Champ | Type | Description |
|---|---|---|
jobId / file | string | id du job / URL du fichier |
status | enum | JOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
content | string | texte reconnu au format outputFormat |
outputFormat | string | format de content : markdown / html / xml / plain. Détermine le découpage conscient de la structure à l’étape LLM |
model | string | méthode/moteur OCR (par ex. pdf_fitz) |
request / rawData | string | débogage : la requête OCR et la réponse brute |
error | string | erreur de l’étape (vide en cas de succès) |
duration | string | durée, ns (un nombre sous forme de chaîne — protojson renvoie int64 sous forme de chaîne) |
created / updated | string | RFC3339 |
Champs llm[] :
| Champ | Type | Description |
|---|---|---|
jobId / file | string | id du job / URL du fichier |
promptIndex | int | index du prompt (actuellement toujours 0) |
chunkIndex / chunkTotal | int | numéro du chunk / nombre total de chunks (si le texte a été découpé) |
status | enum | JOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
skipReason | string | lorsque SKIPPED : no_prompt / ocr_failed / no_ocr_content / empty_ocr_content |
content | string | réponse du modèle (texte tel quel) |
model | string | la chaîne model issue de la requête |
request / rawData | string | débogage |
error | string | erreur de l’étape (par ex. provider error (category=…, status=400)) |
duration | string | durée, ns (un nombre sous forme de chaîne — protojson renvoie int64 sous forme de chaîne) |
created / updated | string | RFC3339 |
schemaValid | bool | présent uniquement si le job a défini responseSchema et que le fichier n’a pas été découpé (chunkTotal = 1) : indique si content respecte le schéma |
schemaErrors | string[] | violations de conformité lorsque schemaValid vaut false (≤10, ≤512 octets chacune) |
Champs llm[].consensus (présents uniquement si le job a défini consensus.mode ; sur les jobs avec consensus, les lignes ignorées/en échec portent tout de même consensus: null) :
| Champ | Type | Description |
|---|---|---|
k | int | exécutions demandées (2 / 3 / 5) |
runsVotable | int | exécutions ayant produit une réponse JSON analysable et dans le budget, et ayant participé au vote |
minAgreement | number | l’accord par champ le plus faible de la ligne, sous forme de fraction ; 0 est une valeur sentinelle réservée — aucun vote n’a eu lieu (runsVotable < 2) ou le vote a dégradé, pas « 0 % d’accord » |
incomplete | bool | true lorsque runsVotable < k, ou que le vote a dégradé |
disagreements | array | champs sur lesquels les exécutions ne se sont pas pleinement accordées, la plus faible concordance en premier ; par entrée : fieldPath (chemin à points ; "" = racine du document ; peut se répéter entre entrées lorsqu’un chemin porte à la fois un litige de présence et un litige d’élément de tableau — affichez chaque entrée séparément, sans dédupliquer par chemin), variants[] (value — JSON compact, tronqué de façon rune-safe à 512 octets ; runs — combien d’exécutions votables l’ont produite ; included — a gagné le vote / figure dans la réponse retenue, indépendamment de la présence textuelle de sa clé — un null gagnant a included: true même si la clé est omise), variantsDropped (variantes écartées de cette entrée, uniquement le compte) |
disagreementsDropped | int | entrées de divergence écartées de la ligne (uniquement le compte ; ≤100 entrées conservées) |
Champs merged[] (présents uniquement si le job a défini merge.enabled) :
| Champ | Type | Description |
|---|---|---|
file | string | URL du fichier concerné par cette réponse fusionnée ; vide ("") à scope=job |
format | string | format de content : json / xml / html / markdown / text |
content | string | la réponse fusionnée pour tout le document (ou le fichier) |
schemaValid | bool | présent uniquement si le job a défini responseSchema : indique si le content fusionné y est conforme |
conflicts | array | champs ayant divergé entre chunks (≤ 100 entrées, ≤ 10 valeurs en compétition chacune, ≤ 512 octets par valeur) ; chaque entrée : field, values[] en compétition, sources[] (références file + chunk) — values[i] correspond à sources[i] (alignés par index) |
dedupeRemovedTechnical | int | enregistrements supprimés par la déduplication automatique de chevauchement de frontière |
dedupeRemovedContent | int | enregistrements supprimés grâce à vos champs dedupeBy |
mergeIncomplete | bool | true lorsqu’au moins un chunk n’a pas pu être fusionné en toute sécurité et a été ajouté tel quel |
incompleteChunks | array | le ou les chunks n’ayant pas pu être fusionnés ; chaque entrée : file, index de chunk |
Remarque. Une description lisible par machine de l’API Jobs au format OpenAPI 3.0.3 est publiée à l’adresse
/openapi.yaml. Elle est générée à partir des définitions protobuf du service et ne diverge donc pas de l’API ; cette page reste la référence rédigée. Vous pouvez fournir ce fichier à n’importe quel générateur de clients compatible OpenAPI.
POST /v1/jobs/upload — envoi de fichier
Un endpoint HTTP autonome (pas grpc-gateway). Accepte un fichier via multipart/form-data.
Réponse (snake_case — une exception au camelCase général) :
{"url": "https://…/files/…/document.pdf", "name": "document.pdf", "size_bytes": 204800}
Utilisez l’url renvoyée dans sourceUrls lors de la création d’un job.
Le corps d’erreur de cet endpoint est {"code": <int>, "message": "…"}, sans tableau details. La limite de taille du fichier est définie par la configuration (grpc.maxRecvMsgBytes ; 20 MiB dans le déploiement actuel), et non par une valeur par défaut du code.
Versionnage
Le chemin /v1 est stable. Les changements incompatibles sont livrés sous un nouveau chemin (/v2). Les changements additifs (nouveaux champs et endpoints facultatifs) ne rompent pas la compatibilité et sont annoncés dans le Changelog.
Ressources pour les développeurs
- Spécification OpenAPIOpenAPI 3.0.3, générée à partir des définitions du service. Convient à n'importe quel générateur de clients.