Dokumentenverarbeitung (Job API)
Job-Lebenszyklus
JOB_STATUS_NEW → JOB_STATUS_FILE_PROCESSING → JOB_STATUS_OCR → JOB_STATUS_LLM → JOB_STATUS_COMPLETE | JOB_STATUS_PARTIAL | JOB_STATUS_FAILED
- JOB_STATUS_NEW — der Job wurde erstellt und in die Warteschlange gestellt.
- JOB_STATUS_FILE_PROCESSING — Dateien werden heruntergeladen, Archive entpackt und Formate in eine verarbeitbare Form normalisiert. Kann direkt in
JOB_STATUS_FAILEDübergehen, wenn alle Sources unerreichbar sind oder ein Datei-Limit überschritten wird (Grund imerror-Feld des Jobs). - JOB_STATUS_OCR — Texterkennung für jede Datei.
- JOB_STATUS_LLM — der erkannte Text wird mit Ihren Prompts an das Modell geschickt.
- JOB_STATUS_COMPLETE — keine Fehler in der OCR- oder LLM-Stufe.
- JOB_STATUS_PARTIAL — mindestens eine erfolgreiche Modellantwort (LLM), aber auch mindestens ein Fehler in der OCR- oder LLM-Stufe (prüfe die Fehler auf Dateiebene im Ergebnis).
- JOB_STATUS_FAILED — Fehler haben verhindert, dass irgendeine Datei eine erfolgreiche Modellantwort erreicht: entweder ein Fehler beim Herunterladen/Entpacken der Dateien (Grund im Job-
error-Feld;ocr[]/llm[]sind leer) oder keine Datei erreichte ein erfolgreiches Ergebnis in der OCR- oder LLM-Stufe.
Die Verarbeitung ist asynchron: Fragen Sie den Job-Status über GET /v1/jobs/{id} ab, bis der Job einen terminalen Status erreicht.
Dateien hochladen
Sie können die Source eines Jobs auf zwei Arten angeben:
- Öffentliche URL — übergeben Sie die URL beim Erstellen des Jobs in
sourceUrls. hotdoc lädt die Datei herunter (Download-Timeout: 30 s, bis zu 3 Weiterleitungen). - Direkter Upload — laden Sie die Datei hoch und verwenden Sie dann die zurückgegebene URL in
sourceUrls:POST /v1/jobs/upload(multipart/form-data) — lädt eine einzelne Datei über HTTP hoch. Das ist ein eigenständiger HTTP-Endpunkt (kein grpc-gateway). Die Antwort ist JSON in snake_case:{"url": "…", "name": "…", "size_bytes": 12345}. Der Fehler-Body dieses Endpunkts ist{"code": <int>, "message": "…"}, ohnedetails-Array. Das Größenlimit wird per Konfiguration gesetzt (grpc.maxRecvMsgBytes; 20 MiB im aktuellen Deployment), nicht durch einen Code-Standardwert.- gRPC-Methode
Upload(Client-Streaming) — ein Streaming-Upload (Limit pro Nachricht: 20 MiB).
Prompts und Datenextraktion
Die Datenextraktion wird durch Text-Prompts gesteuert, nicht durch ein Schema. Im Feld prompts übergeben Sie ein Array von Anweisungen. In der LLM-Stufe wird der erkannte Text jeder Datei genommen, bei Bedarf in Chunks aufgeteilt und zusammen mit Ihrem Prompt an das Modell geschickt. Die Antwort des Modells wird als Text je Datei zurückgegeben (und je Chunk, falls die Datei aufgeteilt wurde).
Die Aufteilung in Chunks ist strukturbewusst: Der Text wird entlang der Strukturgrenzen seines Formats geschnitten (Markdown, HTML, XML oder plain) — Tabellen werden nicht mitten in einer Zeile zerrissen (und passt eine Tabelle nicht als Ganzes, wird ihre Kopfzeile in jedem Chunk wiederholt), und der Kontext der Abschnittsüberschriften bleibt erhalten. Die Chunk-Größe in Tokens wird durch neural.chunkBudgetTokens gesetzt (siehe „Modell anbinden”); ist sie nicht gesetzt, gilt der konservative Standardwert des Dienstes. Jeder Chunk ist ein separater Modellaufruf mit einer vollständigen Kopie des Prompts und einer eigenen llm[]-Zeile (chunkIndex / chunkTotal). Das Zusammensetzen der Antwort aus den Chunks in der Reihenfolge von chunkIndex erfolgt auf Ihrer Seite — der Dienst fügt die Chunks nicht zu einem einzigen Ergebnis zusammen.
Um strukturierte Daten zu erhalten, fragen Sie sie direkt im Prompt an — zum Beispiel: „Gib das Ergebnis als JSON mit den folgenden Feldern zurück: …”. Ihr Prompt und das von Ihnen gewählte Modell bestimmen die Gültigkeit und Form des JSON; hotdoc erzwingt und validiert kein Schema.
Prompt-Tipps:
- listen Sie die benötigten Felder explizit und eindeutig auf;
- geben Sie das Ausgabeformat direkt im Prompt-Text an;
- behalten Sie das Größenlimit für Prompts im Blick — 64 KiB (siehe „Limits”).
Hinweis. Die Platzhalter <…> in den folgenden Vorlagen markieren Stellen, die Sie selbst ausfüllen müssen: hotdoc ersetzt sie nicht automatisch — der Prompt wird genau so an das Modell geschickt, wie er geschrieben ist. Ihr Prompt und das von Ihnen gewählte Modell bestimmen die Ausgabestruktur; es gibt keine serverseitige Schemavalidierung.
Die Prompt-Qualität bestimmt Ihre Ergebnisse. Wie gut die Extraktion funktioniert, hängt ebenso sehr von Ihrem Prompt ab wie vom gewählten Modell — oft sogar mehr. Ein vager Prompt liefert vage Ausgaben selbst auf einem Spitzenmodell, während ein präziser, gut strukturierter Prompt selbst von kleineren, günstigeren Modellen zuverlässige Ergebnisse erzielt. Behandeln Sie die folgende Vorlage als Ausgangspunkt, nicht als fertigen Prompt: Nehmen Sie sie, beschreiben Sie Ihren Dokumenttyp, Ihre Aufgabe und die genaue Ausgabe, die Sie brauchen, und lassen Sie dann ein leistungsfähiges Modell daraus einen auf Ihren Fall zugeschnittenen Prompt erstellen — wobei diese Struktur beibehalten und die Regeln, Sonderfälle und die Ausgabevalidierung für Ihre Daten geschärft werden. Der Meta-Prompt dafür steht am Ende dieses Abschnitts.
Beispiel: Felder aus Rechnung / Bestellung / Beleg extrahieren
Text von prompts[0]:
TASK
Extract structured fields from a single procurement/accounting document and return them strictly as JSON.
IMPORTANT
Your answer must contain ONLY JSON. Do not add any comments, explanations, or surrounding text before or after the JSON.
1. INPUT
The recognized text of a single document follows the "---" marker below (the OCR output is HTML). It is the only source. The document may be an <type: invoice / purchase order / receipt> and may contain stamps, signatures, and multi-row line-item tables. The text may be truncated or split into chunks — work only with the text you are given and never assume content you cannot see.
2. OUTPUT JSON FORMAT
Return a single JSON object matching this schema (the inline comments are explanatory — do not include them in the output):
{
"doc_type": "string", // one of: invoice | purchase_order | receipt | unknown
"number": "string",
"date": "string", // ISO 8601: YYYY-MM-DD
"supplier": {
"name": "string",
"tax_id": "string" // e.g. US EIN or EU VAT ID, as printed
},
"items": [
{ "name": "string", "qty": number, "price": number, "amount": number }
],
"total": number,
"currency": "string" // ISO 4217, e.g. USD, EUR
}
3. EXTRACTION RULES
- doc_type: classify from the title, headers, and content. If it is none of the listed types, set "unknown" and still fill any fields you can.
- number / date: the document's own number and issue date. Convert the date to ISO 8601 (YYYY-MM-DD).
- supplier: the selling/issuing party, not the buyer. tax_id: the supplier's tax identifier, exactly as printed.
- items: one object per line item, in document order. Keep "name" exactly as written, including specifications and units that identify the item.
- qty / price / amount / total: return as JSON numbers — strip thousands separators and currency symbols, use a dot as the decimal separator ("1,200.50" -> 1200.5).
- currency: ISO 4217 code. If only a symbol is present, map it ("$" -> "USD", "€" -> "EUR"). If it cannot be determined, use null.
4. PROCESSING REQUIREMENTS
- Use ONLY the provided document text. Do not add external knowledge or infer values that are not present.
- Do NOT guess, complete, or reformat values beyond the normalization explicitly required above.
- Field not found -> null for scalars (including supplier sub-fields), [] for "items". Never drop a schema key.
- Analyze the entire document, including tables and appendices. A reference to an external attachment is not a line item.
- Be literal and deterministic: the same input must always produce the same output.
5. RESPONSE FORMAT
- Return ONLY the valid JSON object described above.
- No markdown, no code fences, no text before or after the JSON.
REMEMBER
Your answer must start with "{" and end with "}". Nothing else. If the document is not one of the expected types, return the schema with "doc_type": "unknown" and whatever fields you could extract.
JSON-Envelope (primär — auf dem verifizierten Xiaomi mimo-v2-flash):
{
"sourceUrls": ["<YOUR_FILE_URL>"],
"title": "Invoice <number>",
"prompts": ["<THE ENTIRE TEMPLATE ABOVE, AS A SINGLE STRING>"],
"ocr": { "provider": "NEURAL_CLIENT_TYPE_MISTRAL", "model": "mistral-ocr-latest", "providerKey": "<YOUR_KEY>" },
"neural": {
"type": "NEURAL_CLIENT_TYPE_XIAOMI",
"model": "mimo-v2-flash",
"apiKey": "<YOUR_PROVIDER_KEY>",
"reasoningEffort": "low"
}
}
Header: Authorization: Bearer <YOUR_HOTDOC_KEY>.
| Anfragefeld | Was es ist | „Variable” |
|---|---|---|
Authorization | Ihr hotdoc API-Schlüssel | API-Zugriffsschlüssel |
sourceUrls[] | Datei-URLs | Dokument-Links |
prompts[0] | die gesamte Vorlage als ein einzelner String | strukturierter Prompt |
neural.type / neural.model | Provider und Modell | Modell |
neural.apiKey | Provider-Schlüssel (BYOK) | Modell-Schlüssel |
neural.reasoningEffort | optional minimal/low/medium/high | Reasoning-Tiefe |
ocr.provider | OCR-Provider-Enum (z. B. NEURAL_CLIENT_TYPE_MISTRAL) | OCR-Provider |
ocr.model | OCR-Modellbezeichner; erforderlich | OCR-Modell |
ocr.providerKey | Provider-Schlüssel für OCR (BYOK); nur als Eingabe entgegengenommen, niemals zurückgegeben | OCR-Schlüssel |
Weitere Beispiele (kurz). Gleiche Mechanik — nur der Prompt-Text und die erwartete Antwortform in llm[].content unterscheiden sich:
- Klassifikation. Prompt: „Bestimme den Dokumenttyp: Rechnung / Vertrag / Beleg / Brief / Sonstiges. Gib ein einzelnes Wort aus der Liste zurück, ohne Erklärung.” Antwort: ein einzelnes Wort (z. B.
contract). - Vertragskonditionen. Prompt: „Extrahiere: Parteien, Gegenstand, Betrag, Laufzeit und Kündigungsbedingungen. Gib JSON gemäß dem Schema {parties[], subject, amount, term, termination} zurück. Feld nicht gefunden → null.” Antwort: JSON gemäß dem Schema.
- Zusammenfassung. Prompt: „Fasse das Dokument in 3–5 Sätzen zusammen. Keine Aufzählungslisten.” Antwort: Fließtext.
Eigenen Prompt erstellen (Meta-Prompt)
Der schnellste Weg zu einem hochwertigen Prompt ist, ein leistungsfähiges Modell ihn für Sie schreiben zu lassen. Geben Sie ihm den folgenden Meta-Prompt: Fügen Sie unser Beispiel als Referenzstruktur ein, die benötigte Ausgabeform (JSON/CSV/Markdown) sowie eine Beschreibung Ihres Kontexts und Ihrer Aufgabe — und Sie bekommen einen einsatzbereiten hotdoc-Prompt zurück. Füllen Sie die eingeklammerten Blöcke aus; den Rest erledigt das Modell.
You are a senior prompt engineer. Build a production-grade extraction prompt that will be sent to a document-processing model through the hotdoc API. The model receives the OCR'd text (HTML) of a single document and must return data in a strict, machine-parseable format.
WHAT I'M GIVING YOU
1) REFERENCE PROMPT — the structure and style to follow. Preserve its section anatomy.
<<<REFERENCE_PROMPT
[paste the hotdoc example prompt here]
REFERENCE_PROMPT
2) TARGET OUTPUT — the exact shape I need back: a JSON schema/sample, CSV columns, or Markdown layout.
<<<TARGET_OUTPUT
[paste your desired JSON / CSV / Markdown here]
TARGET_OUTPUT
3) DOMAIN & CONTEXT — what these documents are, where they come from, and their quirks (languages, layouts, stamps, tables, common OCR errors).
<<<CONTEXT
[describe your documents and domain]
CONTEXT
4) TASK — exactly what to extract or produce, plus the business rules, definitions, and edge cases that matter.
<<<TASK
[describe the task and rules]
TASK
5) OUTPUT FORMAT — one of: JSON | CSV | Markdown. Default: JSON.
<<<FORMAT
JSON
FORMAT
HOW TO BUILD THE PROMPT
1. Study the domain and task deeply before writing. Infer the edge cases a careful human reviewer would catch — ambiguous fields, duplicates, ranges, units, missing data, multi-row tables, appendices — and address each one explicitly.
2. Keep the REFERENCE PROMPT's anatomy: a one-line TASK, an IMPORTANT "only the target format" rule, then numbered sections (INPUT, OUTPUT FORMAT, EXTRACTION/PROCESSING RULES field by field, PROCESSING REQUIREMENTS, RESPONSE FORMAT), and a final REMEMBER reinforcement.
3. Make the output contract unambiguous for the chosen format:
- JSON: give the full schema with types and nullability, mark required vs optional keys, forbid any text/markdown/code fences outside the JSON, and require the answer to start with "{" (or "[") and end with "}" (or "]").
- CSV: fix the exact column order and header row, the delimiter, the quoting/escaping rule, and how empty values are written; one record per row, no prose.
- Markdown: fix the exact headings/table columns and forbid any content outside that layout.
4. Pin the data discipline: use only the provided document text; never invent, guess, or reformat beyond the normalization you explicitly define; specify number, date, and unit normalization; define how "not found" is represented (null / empty / skipped) and how duplicates are handled; preserve source values verbatim where identity matters.
5. Account for hotdoc specifics: the model sees one document's OCR'd HTML, possibly truncated or split into chunks; do not rely on any temperature setting — enforce determinism through wording ("be literal and deterministic"); the prompt is sent verbatim, so resolve every "<placeholder>" yourself.
6. Self-check before finishing: re-read the TARGET OUTPUT and confirm the prompt forces exactly that shape, that every field has a rule, and that a small, cheap model could follow it without guessing.
OUTPUT
Return ONLY the finished prompt, ready to paste into hotdoc's "prompts" array — no explanation, no preamble, no code fences.
Modell anbinden (BYOK)
Die LLM-Stufe läuft mit Ihrem Provider-Schlüssel. Die Konfiguration wird beim Erstellen eines Jobs im neural-Objekt übergeben:
| Feld | Erforderlich | Beschreibung |
|---|---|---|
type | ja | Provider (siehe Liste unten) |
model | ja | Modellbezeichner; wird unverändert an den Provider übergeben |
apiKey | ja | Ihr Provider-Schlüssel; nur als Eingabe entgegengenommen, niemals in Antworten zurückgegeben |
reasoningEffort | nein | Hinweis zur Reasoning-Tiefe; erlaubte Werte: minimal, low, medium, high (leer = aus). Ein ungültiger Wert → 400-Fehler. Ob er beachtet wird, hängt vom Provider/Modell ab. |
chunkBudgetTokens | nein | Token-Budget für einen einzelnen Modellaufruf: deckt sowohl den Prompt als auch den Dokumenttext in einem Chunk ab. 0/nicht gesetzt → der konservative Standardwert des Dienstes. Bereich: 16000–2000000; ein Wert außerhalb des Bereichs → 400. Setzen Sie ihn auf das Kontextfenster Ihres Modells — nur Sie kennen es. Die Antwort gibt immer das tatsächlich wirksame Budget zurück (auch wenn Sie sich auf den Standardwert verlassen): Der Dienst reserviert einen kleinen Spielraum für Chat-Template-Tokens, sodass der zurückgegebene Wert etwas niedriger ist als der von Ihnen gesetzte. |
Unterstützte Provider:
| Provider | neural.type-Wert |
|---|---|
| OpenAI | NEURAL_CLIENT_TYPE_OPENAI |
| Anthropic (Claude) | NEURAL_CLIENT_TYPE_CLAUDE |
| xAI (Grok) | NEURAL_CLIENT_TYPE_GROK |
| Together | NEURAL_CLIENT_TYPE_TOGETHER |
| DeepSeek | NEURAL_CLIENT_TYPE_DEEPSEEK |
| Xiaomi | NEURAL_CLIENT_TYPE_XIAOMI |
| Mistral | NEURAL_CLIENT_TYPE_MISTRAL |
| OpenRouter | NEURAL_CLIENT_TYPE_OPENROUTER |
Über NEURAL_CLIENT_TYPE_OPENROUTER erhalten Sie Modelle vieler Anbieter, die keine direkte Integration haben.
Sie zahlen den Provider direkt zu dessen Tarif — hotdoc erhebt keinen Aufschlag auf Tokens oder OCR.
Idempotente Wiederholungen
Um die Job-Erstellung sicher zu wiederholen, generieren Sie einen idempotencyKey (eine UUID) und verwenden Sie ihn
über die Wiederholungen derselben Anfrage hinweg:
POST /v1/jobs
{ "sourceUrls": ["..."], "ocr": { ... }, "neural": { ... }, "idempotencyKey": "3f1c…" }
Eine Wiederholung mit demselben Schlüssel und identischen Parametern gibt den ursprünglichen Job zurück;
das Ändern eines beliebigen Parameters unter demselben Schlüssel gibt 400 zurück. Verwenden Sie einen neuen Schlüssel für einen
wirklich neuen Job.
Webhooks
Webhooks ersparen Ihnen das Polling: Der Dienst sendet ein POST an Ihren Endpunkt, sobald der Job abgeschlossen ist. Das ist völlig optional — wenn Sie die Felder nicht setzen, ändert sich das API-Verhalten nicht.
Einrichtung
Übergeben Sie beim Erstellen eines Jobs (POST /v1/jobs) eines oder beide optionalen Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
webhookUrl | string | Absolute URL Ihres Endpunkts (http:// oder https://). Wird nur als Eingabe entgegengenommen — in Antworten niemals zurückgegeben. |
webhookSecret | string | Optionaler Secret für die HMAC-Signatur. Wird nur als Eingabe entgegengenommen — in Antworten niemals zurückgegeben; verschlüsselt gespeichert und zusammen mit dem Job gelöscht (siehe „Sicherheit und Daten”). |
Beispiel:
{
"sourceUrls": ["https://example.com/invoice.pdf"],
"prompts": ["Extract the total."],
"ocr": { "provider": "NEURAL_CLIENT_TYPE_MISTRAL", "model": "mistral-ocr-latest", "providerKey": "..." },
"neural": { "type": "NEURAL_CLIENT_TYPE_XIAOMI", "model": "mimo-v2-flash", "apiKey": "..." },
"webhookUrl": "https://your-service.example.com/hooks/hotdoc",
"webhookSecret": "my-secret-value"
}
Wann der Webhook auslöst
Einmal pro Job — beim ersten Übergang in einen terminalen Status (JOB_STATUS_COMPLETE, JOB_STATUS_PARTIAL oder JOB_STATUS_FAILED). Die Zustellung selbst ist at-least-once (bei Fehlern sind Duplikate möglich); siehe „Zustellungsgarantien”. Der Webhook trägt nicht das Verarbeitungsergebnis; er signalisiert nur die Fertigstellung. Das vollständige Ergebnis holen Sie wie gewohnt mit GET /v1/jobs/{id}/result.
Anfrage-Body
Der Dienst sendet ein POST an Ihre webhookUrl mit Content-Type: application/json und einem JSON-Body:
{
"job_id": "15b07304-...",
"account_id": "a1b2c3d4-...",
"status": "complete",
"finished_at": "2026-06-24T12:34:56Z"
}
| Feld | Typ | Beschreibung |
|---|---|---|
job_id | string | Job-UUID |
account_id | string (Konto-Identifikator) | Konto-Identifikator |
status | string | Einer von: complete, partial, failed |
finished_at | string | Abschlusszeit des Jobs im RFC3339-Format (UTC) |
Signaturprüfung
Wenn webhookSecret gesetzt ist, enthält jede Anfrage zwei zusätzliche Header:
| Header | Beispielwert | Beschreibung |
|---|---|---|
X-Hotdoc-Timestamp | 1750765200 | Unix-Zeit der Zustellung (Sekunden) |
X-Hotdoc-Signature | sha256=a3f4... | HMAC-SHA256-Signatur |
Signaturalgorithmus:
signature = "sha256=" + hex( hmac_sha256(secret, "<timestamp>.<body>") )
wobei <timestamp> die String-Form der Unix-Zeit aus X-Hotdoc-Timestamp ist, <body> der rohe Anfrage-Body (Bytes wie empfangen) und . das Trennzeichen. hex ist kleingeschrieben; secret wird als UTF-8-Bytes verwendet.
So prüfen Sie auf Ihrer Seite:
- Extrahieren Sie den Wert von
X-Hotdoc-Timestamp. - Berechnen Sie
hmac_sha256(secret, "<X-Hotdoc-Timestamp value from step 1>.<raw request body>"), kodieren Sie als kleingeschriebenes hex und stellen Siesha256=voran. - Vergleichen Sie das Ergebnis mit
X-Hotdoc-Signatureüber einen konstant-zeitigen Vergleich (hmac.Equal/crypto/subtle.ConstantTimeCompareoder gleichwertig). - Weisen Sie die Anfrage ab, wenn der Zeitstempel zu alt ist (empfohlene Toleranz: 5 Minuten).
Wenn webhookSecret nicht gesetzt ist, werden die Header X-Hotdoc-Timestamp und X-Hotdoc-Signature nicht gesendet.
Wiederholungsrichtlinie
Wenn Ihr Endpunkt nicht erreichbar ist oder einen Fehler zurückgibt, wiederholt der Dienst die Zustellung nach folgendem Zeitplan:
| Versuch | Verzögerung bis zum nächsten |
|---|---|
| 1 → 2 | 1 Minute |
| 2 → 3 | 5 Minuten |
| 3 → 4 | 15 Minuten |
| 4 → 5 | 30 Minuten |
| 5 → 6 | 30 Minuten |
| 6 | — (letzter; danach wird die Zustellung als fehlgeschlagen markiert) |
Insgesamt: bis zu 6 Versuche.
Dauerhafte Fehler (4xx außer 408/429, unbrauchbare URL, SSRF-Blockierung) werden nicht wiederholt: Die Zustellung wird sofort als fehlgeschlagen markiert. Eine 2xx-Antwort gilt als Erfolg.
Zustellungsgarantien
Die Zustellung ist at-least-once: In den meisten Fällen erhält Ihr Endpunkt genau einen Aufruf, aber Wiederholungen können bei Fehlern zu doppelter Zustellung führen. Deduplizieren Sie Events auf Ihrer Seite anhand von job_id.