POST /api/documents/extract¶
Takes the plain text of a medical document and returns a structured JSON object — patient, insurance, sender, recipient, diagnoses, medication and so on. Synchronous, no streaming; a typical document takes 2–5 seconds.
Request¶
{
"document_type": "zuweisung",
"text": "Praxis Dr. med. Anna Beispiel, Bahnhofstrasse 4, 8001 Zürich\n\nZuweisung vom 12.08.2026\n..."
}
document_type—"zuweisung"(referral/registration to an institution) or"arztbrief"(discharge/progress letter, findings). Determines the extraction schema.text— the document as plain text, 1–20 000 characters. PDFs and scans have to be converted to text on your side (OCR output is fine, see below).
Response¶
{
"document_type": "zuweisung",
"data": {
"dokument": { "typ": "zuweisung", "datum": "2026-08-12" },
"patient": {
"nachname": "Muster",
"vorname": "Max",
"geburtsdatum": "1971-02-03",
"geschlecht": null,
"strasse": "Seestrasse 12",
"plz": "8802",
"ort": "Kilchberg",
"versichertennummer": null,
"fallnummer": null
},
"versicherung": { "name": "Helsana", "klasse": "allgemein" },
"absender": {
"name": "Anna Beispiel",
"institution": "Praxis Dr. med. Anna Beispiel",
"fachrichtung": "Allgemeine Innere Medizin",
"ort": "Zürich"
},
"empfaenger": { "name": null, "institution": null, "ort": null },
"aufenthalt": { "von": null, "bis": null, "art": null },
"fragestellung": "Indikation zur Knie-TP rechts?",
"diagnosen": ["Gonarthrose rechts", "Arterielle Hypertonie"],
"medikation": ["Lisinopril 10mg 1-0-0", "Ibuprofen 400mg bei Bedarf"],
"kostengutsprache": null,
"dringlichkeit": "elektiv"
},
"removed_by_grounding": [],
"model": "eu.anthropic.claude-haiku-4-5-...",
"usage": { "tokens_in": 2149, "tokens_out": 452 },
"processing_time_ms": 2815.98
}
data— the extracted object, always with all top-level keys present. Anything not stated in the document isnull(or an empty list). Field names are German, values are taken from the document.removed_by_grounding— fields the validator dropped, see below. Empty list in the normal case.model,usage,processing_time_ms— which model answered, token counts, server-side duration.
Dates are normalised to ISO YYYY-MM-DD. Diagnoses and medication come back close to the document wording, one entry each.
Fields per document type¶
Both types share dokument, patient, versicherung, absender, empfaenger, aufenthalt, fragestellung, diagnosen, medikation.
zuweisung |
arztbrief |
|
|---|---|---|
| additional fields | kostengutsprache, dringlichkeit |
eingriffe, procedere |
Enum values: patient.geschlecht m/w · versicherung.klasse allgemein/halbprivat/privat · aufenthalt.art stationaer/ambulant · dringlichkeit notfall/dringlich/elektiv · kostengutsprache liegt_vor/beantragt/wird_eingeholt. Each of them is null unless the document says so explicitly — a clinic called "Privatklinik" does not make the insurance class private, and a requested appointment date does not make a referral urgent.
Grounding — why values cannot be invented¶
After the model has answered, a deterministic validator checks every value against the source document. Whatever it cannot find is set to null and listed in removed_by_grounding:
- Literal fields (names, addresses, numbers, dates, diagnoses, medication) need a match in the document. Dates are checked in all common notations, numbers by their digit sequence, free text by substring or ≥ 60 % token overlap.
- Enum fields need an explicit keyword in the document.
What this guarantees: an invented value never reaches you. What it does not catch: a value that is in the document but was put in the wrong field — a practice address landed in the patient address, for instance. Both values are provably in the document, so the check passes. Plan for a human review step on the fields that matter, and show the source document next to them.
A non-empty removed_by_grounding is a useful review signal: it means the model claimed something the document does not support.
OCR and scans¶
Text from OCR works. Obvious character-level OCR errors are corrected, everything else is taken as found. The weaker the OCR, the more fields end up null — which is the intended failure mode.
Example¶
curl -X POST https://medical-api.dev.unomed.ch/api/documents/extract \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"document_type":"zuweisung","text":"Zuweisung vom 12.08.2026 ..."}'
Errors¶
401 key missing/invalid · 422 validation failed (e.g. unknown document_type, text empty or > 20 000 characters) · 429 rate limit or daily quota reached · 502 the model did not return a usable extraction · 503 service not configured. See Errors & Health.
Error messages are deliberately generic — provider errors can quote parts of the request, which would mean patient data in an error string.
POST /api/documents/text¶
Takes a file — PDF, JPEG or PNG — and returns its text. Use it in front of /api/documents/extract when you only have the attachment, not the text. Synchronous, no streaming.
The decision is made per page, and the response tells you what ran:
- A page with a real text layer (≥ 40 visible characters, or filled form fields) is returned as is — milliseconds, no model involved.
- A page without one (scan, photo, image) is rendered at 130 dpi and transcribed verbatim by the vision model (line structure kept, checkboxes as
[x]/[ ], nothing summarised, corrected or translated). Scan pages are sent in batches of three; at most 10 scan pages per document. methodis"text"(no scan pages — does not count against the daily quota),"vision"(no text pages) or"mixed". Any document with at least one scan page counts one request against the quota.
Filled form fields (AcroForm) are listed after the page text in a --- Formularfelder --- block, one line per filled field (name: value; checkboxes only when ticked, as [x] <value>). Names are the short field name (lastName, not the full XFA path); the full path appears only when two filled fields share a short name. Images larger than 4000 px or 3.5 MB are downscaled and re-encoded as JPEG before transcription; images above 40 megapixels are rejected with 422 (Bild zu gross …) before decoding. PDF pages with an oversized page box are rendered at a reduced resolution; if even 40 dpi would exceed the cap, the page is rejected with 422 (Seite zu gross …).
Request¶
content_type—application/pdf,image/jpegorimage/png.image/tiffis not supported yet (the vision models read only JPEG and PNG); convert on your side or send a PDF.data— the file, Base64-encoded. At most 25 MB decoded.filename— optional, informational only.
Response¶
{
"text": "--- Seite 1 ---\nZuweisung zur stationären Behandlung\n...\n\n--- Seite 2 ---\n[x] stationär [ ] ambulant\nPatient: Hans Muster, geb. 03.05.1960\n...\n\n--- Formularfelder ---\nNachname: Muster\nstationaer: [x] Ja",
"pages": 2,
"pages_transcribed": 1,
"method": "mixed",
"model": "eu.anthropic.claude-haiku-4-5-...",
"processing_time_ms": 6210.4
}
text— pages in document order; multi-page documents carry--- Seite n ---markers. If a document has more than 10 scan pages, the first 10 are transcribed and the text ends with[weitere Seiten nicht abgeschrieben].pages— total pages of the document;pages_transcribed— how many of them went through the vision model.model— which model transcribed; empty string when no model ran.
Vision output is a transcription, not OCR ground truth: the model reads what it sees and marks what it cannot read as [unleserlich]. Feed the result into /api/documents/extract — its grounding check makes sure only values that actually appear in this text end up in the structured data.
Example¶
curl -X POST https://medical-api.dev.unomed.ch/api/documents/text \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d "{\"content_type\":\"application/pdf\",\"data\":\"$(base64 -w0 zuweisung.pdf)\"}"
Errors¶
401 key missing/invalid · 413 file larger than 25 MB · 415 unsupported content_type (including image/tiff) · 422 the file cannot be read as PDF/image, yields no text at all, or the transcription was cut off by the model's output limit (Abschrift abgeschnitten — Dokument zu lang) · 429 rate limit or daily quota reached (scan pages only) · 502 the model did not return a transcription · 503 no model configured or PDF renderer unavailable (text-layer PDFs still work).
The file content is not cached, logged or persisted — the rendered page images live in a temporary directory that is deleted right after the model call.