API-Referenz
Basis-URL: https://api.hotdoc.io. Anfrage- und Antwort-Bodies sind JSON; Feldnamen sind in camelCase. Ausnahmen: POST /v1/jobs/upload (Antwort) und der Webhook-Anfrage-Body — beide verwenden snake_case.
Verarbeitungs-Endpunkte
| Methode | Endpunkt | Zweck |
|---|---|---|
POST | /v1/jobs | Job erstellen |
GET | /v1/jobs/{id} | Job-Status und Dateiliste (ohne Erkennungsergebnisse) |
GET | /v1/jobs/{id}/result | vollständiges Ergebnis: erkannter Text und Modellantworten je Datei |
GET | /v1/jobs | Konto-Jobs auflisten (Paginierung: pageSize, pageToken, Filter statusEq) |
POST | /v1/jobs/upload | eine einzelne Datei hochladen (multipart/form-data) |
POST /v1/jobs — Job erstellen
Anfrage-Body:
| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
sourceUrls | string[] | ja | zu verarbeitende Datei-URLs |
prompts | string[] | nein | Modellanweisungen (nur der erste Prompt wird ausgeführt); ohne Prompts wird das LLM nicht aufgerufen |
neural | object | ja | Modellkonfiguration (siehe „Modell anbinden”) |
ocr | object | ja | OCR-Provider-Konfiguration (BYOK); immer erforderlich (siehe „OCR-Konfiguration”) |
extractionMode | enum | nein | EXTRACTION_MODE_UNSPECIFIED | EXTRACTION_MODE_HYBRID | EXTRACTION_MODE_OCR_ALWAYS; nicht gesetzt / UNSPECIFIED erbt den Konto-Standard (der wiederum standardmäßig HYBRID ist) |
responseSchema | string | nein | optionales JSON Schema (Roh-String), das die JSON-Antwort des Modells einschränkt; in sich geschlossen (nur interne #/$defs), ≤128 KiB, Tiefe ≤64, ≤10000 Knoten; ein ungültiges Schema wird bei der Erstellung mit 400 abgelehnt (siehe „Strukturierte Ausgabe” in der Job-API-Dokumentation) |
merge | object | nein | optionales serverseitiges Zusammenführen der Antworten je Chunk zu einem Gesamtergebnis für das ganze Dokument; standardmäßig deaktiviert; siehe „Merge-Optionen” weiter unten und „Zusammenführen von Chunk-Ergebnissen” in der Job-API-Dokumentation |
consensus | object | nein | optionale Konsens-Abstimmung über k Durchläufe je Chunk; setzt responseSchema voraus (sonst 400); standardmäßig deaktiviert; siehe „Konsens-Optionen” weiter unten und „Konsens-Durchläufe” in der Job-API-Dokumentation |
title | string | nein | beliebiger Job-Name |
metadata | map<string,string> | nein | beliebige String-Schlüssel-Wert-Paare |
webhookUrl | string | nein | absoluter http/https-Endpunkt, der bei Job-Abschluss benachrichtigt wird (siehe „Webhooks”) |
webhookSecret | string | nein | optionaler HMAC-Secret zum Signieren von Webhook-Anfragen; nur als Eingabe entgegengenommen, niemals in Antworten zurückgegeben |
idempotencyKey | string | nein | Client-Schlüssel für sichere Wiederholungen beim Erstellen; max. 255 Zeichen (siehe „Idempotenz”) |
neural und ocr sind immer erforderlich, auch wenn prompts leer ist. Bei leerem prompts wird das Modell nicht aufgerufen: Jede Datei wird mit JOB_LLM_STATUS_SKIPPED und skipReason=no_prompt markiert, und bei erfolgreicher OCR endet der Job als JOB_STATUS_COMPLETE.
Die Antwort ist der erstellte Job im Status JOB_STATUS_NEW; responseSchema wird auf diesem Objekt zurückgespiegelt (leer, falls nicht gesetzt); consensus wird als befülltes Objekt zurückgegeben — mode: "CONSENSUS_MODE_UNSPECIFIED", falls nicht gesetzt, niemals als fehlendes oder null-Feld.
OCR-Konfiguration
ocr wählt das Erkennungs-Backend (OCR) pro Anfrage (BYOK):
| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
ocr.provider | enum | ja | einer von NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI |
ocr.model | string | ja | die Modell-ID des Providers, z. B. mistral-ocr-latest (Mistral) oder das Vision-Modell des gewählten Providers |
ocr.providerKey | string | ja | BYOK-Schlüssel für den OCR-Provider; nur als Eingabe, niemals zurückgegeben |
Mistral ist das typische OCR-Backend (mistral-ocr-latest); andere Provider führen
Vision-Chat-OCR aus. Der Schlüssel wird verschlüsselt gespeichert und zusammen mit dem Job gelöscht.
Hinweis. Der Extraktionsmodus ist ein Feld auf oberster Ebene, extractionMode, nicht Teil von ocr: Es bestimmt, ob OCR immer läuft (EXTRACTION_MODE_OCR_ALWAYS) oder je Datei nach Ermessen des Konverters angewendet wird (EXTRACTION_MODE_HYBRID, der Standard).
Welches Modell verwenden? Vergleichen Sie Intelligenz, Preis pro Token und Anbieter im LLM-Vergleich und wählen Sie das passende Modell.
Merge-Optionen (merge)
merge ist optional und standardmäßig deaktiviert — setzen Sie merge.enabled, um es zu aktivieren. Solange enabled false/nicht gesetzt ist, werden alle übrigen merge.*-Felder ignoriert.
| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
merge.enabled | bool | nein | aktiviert das serverseitige Zusammenführen für diesen Job |
merge.scope | enum | nein | MERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (Standard) | MERGE_SCOPE_JOB; ein merged[]-Eintrag je Datei, oder einer über alle Dateien |
merge.conflictPolicy | enum | nein | MERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (Standard) | MERGE_CONFLICT_POLICY_MAJORITY |
merge.dedupeBy | string[] | nein | JSON-Feldnamen, die einen eindeutigen Datensatz identifizieren, für die dateiübergreifende inhaltliche Deduplizierung; nur JSON + responseSchema; höchstens 32 Feldnamen — mehr werden bei der Job-Erstellung mit 400 abgelehnt; leer/nicht gesetzt → nur technische Deduplizierung |
merge.format | enum | nein | MERGE_FORMAT_UNSPECIFIED | MERGE_FORMAT_AUTO (Standard) | MERGE_FORMAT_JSON | MERGE_FORMAT_XML | MERGE_FORMAT_HTML | MERGE_FORMAT_MARKDOWN | MERGE_FORMAT_TEXT; überschreibt die automatische Formaterkennung je Datei |
Den vollständigen Durchgang (Umfang, Konfliktbehandlung, die zwei Arten der Deduplizierung) finden Sie unter „Zusammenführen von Chunk-Ergebnissen” in der Job-API-Dokumentation.
Konsens-Optionen (consensus)
consensus ist optional und standardmäßig deaktiviert. Ist es gesetzt, muss consensus.mode ein erkannter Wert sein und der Job muss responseSchema gesetzt haben — ein Job mit gesetztem consensus.mode, aber ohne responseSchema, wird bei der Erstellung mit 400 abgelehnt.
| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
consensus.mode | enum | nein | CONSENSUS_MODE_UNSPECIFIED (deaktiviert, Standard) | CONSENSUS_MODE_TWO_RUNS | CONSENSUS_MODE_THREE_RUNS | CONSENSUS_MODE_FIVE_RUNS; ein nicht erkannter String-Wert wird vom Gateway stillschweigend ignoriert — der Job läuft ohne Konsens, nicht mit 400 |
Den vollständigen Durchgang (Modi, Lesen von minAgreement/disagreements, ehrliche Einschränkungen) finden Sie unter „Konsens-Durchläufe” in der Job-API-Dokumentation.
Idempotenz
Senden Sie idempotencyKey, um POST /v1/jobs sicher wiederholbar zu machen. Eine Wiederholung mit
demselben Schlüssel und identischen Anfrageparametern gibt den ursprünglichen Job zurück (es wird kein
Duplikat erstellt). Derselbe Schlüssel mit anderen Parametern wird mit
400 "idempotency key reused with different request parameters" abgelehnt. Der Schlüssel ist
maximal 255 Zeichen lang; generieren Sie eine frische UUID pro logischer Erstellung.
GET /v1/jobs/{id}/result — Ergebnis
Gibt { "result": { "job": …, "ocr": [...], "llm": [...] } } zurück. Beispiel (gekürzt):
{
"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 in der Antwort enthält kein apiKey; das Job-Objekt hat kein ocr-Feld — der OCR-Schlüssel wird niemals zurückgegeben. ocr[].content ist der erkannte Text im Format ocr[].outputFormat; das Format hängt vom Dateityp ab: PDF und HTML → html, .xml → xml, .txt → plain, alles andere (einschließlich Bildern und Office-Dateien) → markdown. llm[].content ist der Antworttext des Modells unverändert (Ihr Prompt bestimmt die Struktur; es gibt keine serverseitige Validierung, es sei denn, der Job hat responseSchema gesetzt — dann melden llm[].schemaValid/schemaErrors die Konformität). Im obigen Beispiel werden leere/Null-Felder ("", {}, 0) der Vollständigkeit halber gezeigt — protojson lässt sie weg, sodass sie in einer echten Antwort fehlen können.
ocr[]-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
jobId / file | string | Job-ID / Datei-URL |
status | enum | JOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
content | string | erkannter Text im Format outputFormat |
outputFormat | string | Format von content: markdown / html / xml / plain. Steuert das strukturbewusste Chunking in der LLM-Stufe |
model | string | OCR-Methode/-Engine (z. B. pdf_fitz) |
request / rawData | string | Debug: die OCR-Anfrage und die Rohantwort |
error | string | Stufenfehler (leer bei Erfolg) |
duration | string | Dauer, ns (eine Zahl als String — protojson gibt int64 als String zurück) |
created / updated | string | RFC3339 |
llm[]-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
jobId / file | string | Job-ID / Datei-URL |
promptIndex | int | Prompt-Index (aktuell immer 0) |
chunkIndex / chunkTotal | int | Chunk-Nummer / Gesamtzahl der Chunks (falls der Text aufgeteilt wurde) |
status | enum | JOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
skipReason | string | bei SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content |
content | string | Modellantwort (Text unverändert) |
model | string | der model-String aus der Anfrage |
request / rawData | string | Debug |
error | string | Stufenfehler (z. B. provider error (category=…, status=400)) |
duration | string | Dauer, ns (eine Zahl als String — protojson gibt int64 als String zurück) |
created / updated | string | RFC3339 |
schemaValid | bool | nur vorhanden, wenn der Job responseSchema gesetzt hat und die Datei nicht aufgeteilt wurde (chunkTotal = 1): ob content dem Schema entspricht |
schemaErrors | string[] | Konformitätsverstöße, wenn schemaValid false ist (≤10, je ≤512 Bytes) |
llm[].consensus-Felder (nur vorhanden, wenn der Job consensus.mode gesetzt hat; bei Konsens-Jobs tragen übersprungene/fehlgeschlagene Zeilen weiterhin consensus: null):
| Feld | Typ | Beschreibung |
|---|---|---|
k | int | angeforderte Durchläufe (2 / 3 / 5) |
runsVotable | int | Durchläufe, die eine parsbare, budgetkonforme JSON-Antwort geliefert haben und an der Abstimmung teilnahmen |
minAgreement | number | niedrigste Feld-Übereinstimmung in dieser Zeile, als Bruchteil; 0 ist ein reservierter Sentinel-Wert — keine Abstimmung fand statt (runsVotable < 2) oder die Abstimmung ist degradiert, nicht „0 % Übereinstimmung” |
incomplete | bool | true, wenn runsVotable < k, oder die Abstimmung degradiert ist |
disagreements | array | Felder, bei denen die Durchläufe nicht vollständig übereinstimmten, mit der niedrigsten Übereinstimmung zuerst; je Eintrag: fieldPath (Punkt-Pfad; "" = Dokumentwurzel; kann sich über Einträge hinweg wiederholen, wenn ein Pfad sowohl einen Präsenz-Konflikt als auch einen Array-Element-Konflikt hat — jeden Eintrag separat rendern, nicht nach Pfad deduplizieren), variants[] (value — kompaktes JSON, rune-safe auf 512 Bytes gekürzt; runs — wie viele abstimmungsfähige Durchläufe diesen Wert lieferten; included — hat die Abstimmung gewonnen / ist in der konsentierten Antwort vorhanden, unabhängig davon, ob sein Schlüssel textuell vorhanden ist — ein gewinnendes null hat included: true, auch wenn der Schlüssel weggelassen wird), variantsDropped (aus diesem Eintrag gekappte Varianten, nur Zähler) |
disagreementsDropped | int | aus der Zeile gekappte Abweichungs-Einträge (nur Zähler; ≤100 Einträge bleiben erhalten) |
merged[]-Felder (nur vorhanden, wenn der Job merge.enabled gesetzt hat):
| Feld | Typ | Beschreibung |
|---|---|---|
file | string | Datei-URL, auf die sich diese zusammengeführte Antwort bezieht; leer ("") bei scope=job |
format | string | Format von content: json / xml / html / markdown / text |
content | string | die zusammengeführte Antwort für das gesamte Dokument (oder die Datei) |
schemaValid | bool | nur vorhanden, wenn der Job responseSchema gesetzt hat: ob der zusammengeführte content diesem entspricht |
conflicts | array | Felder, die zwischen Chunks voneinander abwichen (≤ 100 Einträge, ≤ 10 konkurrierende Werte je Eintrag, ≤ 512 Byte je Wert); je Eintrag: field, konkurrierende values[], sources[] (Referenzen aus file + chunk) — values[i] entspricht sources[i] (indexgleich) |
dedupeRemovedTechnical | int | Datensätze, entfernt durch automatische Grenzüberlappungs-Deduplizierung |
dedupeRemovedContent | int | Datensätze, entfernt durch Ihre dedupeBy-Felder |
mergeIncomplete | bool | true, wenn mindestens ein Chunk nicht sicher zusammengeführt werden konnte und stattdessen unverändert angehängt wurde |
incompleteChunks | array | die Chunks, die nicht zusammengeführt werden konnten; je Eintrag: file, chunk-Index |
Hinweis. Eine maschinenlesbare OpenAPI-3.0.3-Beschreibung der Jobs-API ist unter
/openapi.yamlveröffentlicht. Sie wird aus den Protobuf-Definitionen des Dienstes generiert und weicht daher nicht von der API ab; diese Seite bleibt die ausformulierte Referenz. Übergeben Sie die Datei einem beliebigen OpenAPI-fähigen Client-Generator.
POST /v1/jobs/upload — Datei hochladen
Ein eigenständiger HTTP-Endpunkt (kein grpc-gateway). Nimmt eine Datei über multipart/form-data entgegen.
Antwort (snake_case — eine Ausnahme zum allgemeinen camelCase):
{"url": "https://…/files/…/document.pdf", "name": "document.pdf", "size_bytes": 204800}
Verwenden Sie die zurückgegebene url beim Erstellen eines Jobs in sourceUrls.
Der Fehler-Body dieses Endpunkts ist {"code": <int>, "message": "…"}, ohne details-Array. Das Dateigrößenlimit wird per Konfiguration gesetzt (grpc.maxRecvMsgBytes; 20 MiB im aktuellen Deployment), nicht durch einen Code-Standardwert.
Versionierung
Der /v1-Pfad ist stabil. Rückwärtsinkompatible Änderungen erscheinen unter einem neuen Pfad (/v2). Additive Änderungen (neue optionale Felder und Endpunkte) brechen die Kompatibilität nicht und werden im Changelog angekündigt.
Ressourcen für Entwickler
- OpenAPI-SpezifikationOpenAPI 3.0.3, aus den Dienstdefinitionen generiert. Für jeden Client-Generator geeignet.