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

MethodeEndpunktZweck
POST/v1/jobsJob erstellen
GET/v1/jobs/{id}Job-Status und Dateiliste (ohne Erkennungsergebnisse)
GET/v1/jobs/{id}/resultvollständiges Ergebnis: erkannter Text und Modellantworten je Datei
GET/v1/jobsKonto-Jobs auflisten (Paginierung: pageSize, pageToken, Filter statusEq)
POST/v1/jobs/uploadeine einzelne Datei hochladen (multipart/form-data)

POST /v1/jobs — Job erstellen

Anfrage-Body:

FeldTypErforderlichBeschreibung
sourceUrlsstring[]jazu verarbeitende Datei-URLs
promptsstring[]neinModellanweisungen (nur der erste Prompt wird ausgeführt); ohne Prompts wird das LLM nicht aufgerufen
neuralobjectjaModellkonfiguration (siehe „Modell anbinden”)
ocrobjectjaOCR-Provider-Konfiguration (BYOK); immer erforderlich (siehe „OCR-Konfiguration”)
extractionModeenumneinEXTRACTION_MODE_UNSPECIFIED | EXTRACTION_MODE_HYBRID | EXTRACTION_MODE_OCR_ALWAYS; nicht gesetzt / UNSPECIFIED erbt den Konto-Standard (der wiederum standardmäßig HYBRID ist)
responseSchemastringneinoptionales 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)
mergeobjectneinoptionales 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
consensusobjectneinoptionale 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
titlestringneinbeliebiger Job-Name
metadatamap<string,string>neinbeliebige String-Schlüssel-Wert-Paare
webhookUrlstringneinabsoluter http/https-Endpunkt, der bei Job-Abschluss benachrichtigt wird (siehe „Webhooks”)
webhookSecretstringneinoptionaler HMAC-Secret zum Signieren von Webhook-Anfragen; nur als Eingabe entgegengenommen, niemals in Antworten zurückgegeben
idempotencyKeystringneinClient-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):

FeldTypErforderlichBeschreibung
ocr.providerenumjaeiner von NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI
ocr.modelstringjadie Modell-ID des Providers, z. B. mistral-ocr-latest (Mistral) oder das Vision-Modell des gewählten Providers
ocr.providerKeystringjaBYOK-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.

FeldTypErforderlichBeschreibung
merge.enabledboolneinaktiviert das serverseitige Zusammenführen für diesen Job
merge.scopeenumneinMERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (Standard) | MERGE_SCOPE_JOB; ein merged[]-Eintrag je Datei, oder einer über alle Dateien
merge.conflictPolicyenumneinMERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (Standard) | MERGE_CONFLICT_POLICY_MAJORITY
merge.dedupeBystring[]neinJSON-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.formatenumneinMERGE_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.

FeldTypErforderlichBeschreibung
consensus.modeenumneinCONSENSUS_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, .xmlxml, .txtplain, 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:

FeldTypBeschreibung
jobId / filestringJob-ID / Datei-URL
statusenumJOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
contentstringerkannter Text im Format outputFormat
outputFormatstringFormat von content: markdown / html / xml / plain. Steuert das strukturbewusste Chunking in der LLM-Stufe
modelstringOCR-Methode/-Engine (z. B. pdf_fitz)
request / rawDatastringDebug: die OCR-Anfrage und die Rohantwort
errorstringStufenfehler (leer bei Erfolg)
durationstringDauer, ns (eine Zahl als String — protojson gibt int64 als String zurück)
created / updatedstringRFC3339

llm[]-Felder:

FeldTypBeschreibung
jobId / filestringJob-ID / Datei-URL
promptIndexintPrompt-Index (aktuell immer 0)
chunkIndex / chunkTotalintChunk-Nummer / Gesamtzahl der Chunks (falls der Text aufgeteilt wurde)
statusenumJOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
skipReasonstringbei SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content
contentstringModellantwort (Text unverändert)
modelstringder model-String aus der Anfrage
request / rawDatastringDebug
errorstringStufenfehler (z. B. provider error (category=…, status=400))
durationstringDauer, ns (eine Zahl als String — protojson gibt int64 als String zurück)
created / updatedstringRFC3339
schemaValidboolnur vorhanden, wenn der Job responseSchema gesetzt hat und die Datei nicht aufgeteilt wurde (chunkTotal = 1): ob content dem Schema entspricht
schemaErrorsstring[]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):

FeldTypBeschreibung
kintangeforderte Durchläufe (2 / 3 / 5)
runsVotableintDurchläufe, die eine parsbare, budgetkonforme JSON-Antwort geliefert haben und an der Abstimmung teilnahmen
minAgreementnumberniedrigste 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”
incompletebooltrue, wenn runsVotable < k, oder die Abstimmung degradiert ist
disagreementsarrayFelder, 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)
disagreementsDroppedintaus 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):

FeldTypBeschreibung
filestringDatei-URL, auf die sich diese zusammengeführte Antwort bezieht; leer ("") bei scope=job
formatstringFormat von content: json / xml / html / markdown / text
contentstringdie zusammengeführte Antwort für das gesamte Dokument (oder die Datei)
schemaValidboolnur vorhanden, wenn der Job responseSchema gesetzt hat: ob der zusammengeführte content diesem entspricht
conflictsarrayFelder, 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)
dedupeRemovedTechnicalintDatensätze, entfernt durch automatische Grenzüberlappungs-Deduplizierung
dedupeRemovedContentintDatensätze, entfernt durch Ihre dedupeBy-Felder
mergeIncompletebooltrue, wenn mindestens ein Chunk nicht sicher zusammengeführt werden konnte und stattdessen unverändert angehängt wurde
incompleteChunksarraydie 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.yaml verö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.