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éthodeEndpointObjet
POST/v1/jobscréer un job
GET/v1/jobs/{id}statut du job et liste des fichiers (sans les résultats de reconnaissance)
GET/v1/jobs/{id}/resultrésultat complet : texte reconnu et réponses du modèle par fichier
GET/v1/jobslister les jobs du compte (pagination : pageSize, pageToken, filtre statusEq)
POST/v1/jobs/uploadenvoyer un seul fichier (multipart/form-data)

POST /v1/jobs — créer un job

Corps de la requête :

ChampTypeRequisDescription
sourceUrlsstring[]ouiURL des fichiers à traiter
promptsstring[]noninstructions du modèle (seul le premier prompt s’exécute) ; sans prompts, le LLM n’est pas appelé
neuralobjectouiconfiguration du modèle (voir « Connexion d’un modèle »)
ocrobjectouiconfiguration du fournisseur OCR (BYOK) ; toujours requise (voir « Configuration OCR »)
extractionModeenumnonEXTRACTION_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)
responseSchemastringnonJSON 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)
mergeobjectnonfusion 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
consensusobjectnonvote 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
titlestringnonnom de job arbitraire
metadatamap<string,string>nonpaires clé-valeur de chaînes arbitraires
webhookUrlstringnonendpoint absolu http/https à notifier à la fin du job (voir « Webhooks »)
webhookSecretstringnonsecret HMAC facultatif pour signer les requêtes webhook ; accepté uniquement en entrée, jamais renvoyé dans les réponses
idempotencyKeystringnonclé 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) :

ChampTypeRequisDescription
ocr.providerenumouil’un de NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI
ocr.modelstringouil’identifiant du modèle du fournisseur, par ex. mistral-ocr-latest (Mistral) ou le modèle vision du fournisseur choisi
ocr.providerKeystringouiclé 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.

ChampTypeRequisDescription
merge.enabledboolnonactive la fusion côté serveur pour ce job
merge.scopeenumnonMERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (par défaut) | MERGE_SCOPE_JOB ; une entrée merged[] par fichier, ou une pour tous les fichiers
merge.conflictPolicyenumnonMERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (par défaut) | MERGE_CONFLICT_POLICY_MAJORITY
merge.dedupeBystring[]nonnoms 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.formatenumnonMERGE_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.

ChampTypeRequisDescription
consensus.modeenumnonCONSENSUS_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, .xmlxml, .txtplain, 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[] :

ChampTypeDescription
jobId / filestringid du job / URL du fichier
statusenumJOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
contentstringtexte reconnu au format outputFormat
outputFormatstringformat de content : markdown / html / xml / plain. Détermine le découpage conscient de la structure à l’étape LLM
modelstringméthode/moteur OCR (par ex. pdf_fitz)
request / rawDatastringdébogage : la requête OCR et la réponse brute
errorstringerreur de l’étape (vide en cas de succès)
durationstringdurée, ns (un nombre sous forme de chaîne — protojson renvoie int64 sous forme de chaîne)
created / updatedstringRFC3339

Champs llm[] :

ChampTypeDescription
jobId / filestringid du job / URL du fichier
promptIndexintindex du prompt (actuellement toujours 0)
chunkIndex / chunkTotalintnuméro du chunk / nombre total de chunks (si le texte a été découpé)
statusenumJOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
skipReasonstringlorsque SKIPPED : no_prompt / ocr_failed / no_ocr_content / empty_ocr_content
contentstringréponse du modèle (texte tel quel)
modelstringla chaîne model issue de la requête
request / rawDatastringdébogage
errorstringerreur de l’étape (par ex. provider error (category=…, status=400))
durationstringdurée, ns (un nombre sous forme de chaîne — protojson renvoie int64 sous forme de chaîne)
created / updatedstringRFC3339
schemaValidboolpré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
schemaErrorsstring[]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) :

ChampTypeDescription
kintexécutions demandées (2 / 3 / 5)
runsVotableintexécutions ayant produit une réponse JSON analysable et dans le budget, et ayant participé au vote
minAgreementnumberl’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 »
incompletebooltrue lorsque runsVotable < k, ou que le vote a dégradé
disagreementsarraychamps 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)
disagreementsDroppedintentré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) :

ChampTypeDescription
filestringURL du fichier concerné par cette réponse fusionnée ; vide ("") à scope=job
formatstringformat de content : json / xml / html / markdown / text
contentstringla réponse fusionnée pour tout le document (ou le fichier)
schemaValidboolprésent uniquement si le job a défini responseSchema : indique si le content fusionné y est conforme
conflictsarraychamps 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)
dedupeRemovedTechnicalintenregistrements supprimés par la déduplication automatique de chevauchement de frontière
dedupeRemovedContentintenregistrements supprimés grâce à vos champs dedupeBy
mergeIncompletebooltrue lorsqu’au moins un chunk n’a pas pu être fusionné en toute sécurité et a été ajouté tel quel
incompleteChunksarrayle 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.