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 »)
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.

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.

Quel modèle utiliser ? Comparez l’intelligence, le prix par token et les fournisseurs dans le comparatif LLM, puis choisissez le modèle adapté.

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). 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

Remarque. Une spécification lisible par machine (OpenAPI/Swagger) n’est pas encore générée ; cette référence est maintenue à la main. OpenAPI est prévu comme une tâche distincte.

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.