Medical API (Deutsch)¶
Kompakte deutsche Fassung — die englische Dokumentation beginnt bei der Übersicht.
Die Medical API macht aus klinischem Freitext strukturierte Daten. Sie hat als ICD-10-Kodierung angefangen und kann heute drei Dinge:
| Endpoint | Zweck |
|---|---|
POST /api/deep-research |
Freitext → ICD-10-GM-Codes, in drei Stufen gestreamt |
POST /api/documents/extract |
Zuweisung oder Arztbrief → strukturiertes, belegtes JSON |
POST /api/llm/generate |
Freie Anweisung auf einem Dokumenttext → Freitext-Antwort |
Katalog für die Kodierung ist ICD-10-GM 2026 mit ergänzten Synonymen — "Blinddarm" oder "Kreuzbandriss" funktionieren also, nicht nur die offizielle Katalogsprache.
Basis-URL und Auth¶
| Umgebung | Basis-URL |
|---|---|
| Sandbox | https://medical-api.dev.unomed.ch |
| Produktion | https://medical-api.unomed.ch |
Die bisherigen Hostnamen icd-10.unomed.ch und icd-10.dev.unomed.ch laufen unverändert weiter und bedienen dieselbe API — bestehende Integrationen müssen nichts ändern. Neue Integrationen nehmen medical-api.
Jeder Call ausser den Health-Endpoints braucht den API-Key als Bearer-Token:
Ohne oder mit falschem Key: 401.
Der Key ist ein langlebiger Integrations-Key und gehört nur auf den Server. Er darf nie in Browser- oder App-Code eingebaut werden — das Frontend spricht mit dem eigenen Backend, das Backend mit dieser API.
Patientendaten¶
/api/documents/extract und /api/llm/generate sind für Dokumente mit Patientendaten gebaut. Deren Inhalte werden weder gecacht noch geloggt — gezählt wird nur ein Request-Zähler pro Key fürs Kontingent. Dasselbe gilt für den Freitext an /api/deep-research.
POST /api/deep-research¶
Der Endpoint für die ICD-Kodierung. Der Server hält die Verbindung offen und streamt drei Ergebnis-Stufen nacheinander via Server-Sent Events:
- Instant — Semantic + Keyword-Search, deutlich unter einer Sekunde. Liefert nur sichere Treffer (
high/medium, max. 10). - Smart — LLM zieht Keywords aus dem Freitext und wählt Codes, wenige Sekunden.
- Deep — LLM-Recherche im Katalog mit konsolidierter Empfehlung, 10–30 s.
Stufe 1 kann sofort angezeigt und durch Stufe 2 und 3 ersetzt bzw. ergänzt werden, sobald sie ankommen. Wenn der User weiternavigiert, wird die Verbindung einfach geschlossen.
Request¶
query ist Pflicht (1–2000 Zeichen). Ein reiner ICD-Code als query (z.B. "M17.9") wird als direkter Lookup behandelt und kommt über den gleichen Stream zurück: Stufe 1 enthält dann den exakten Katalog-Treffer (high) plus benachbarte Katalog-Codes (medium); Stufe 2 und 3 listen die Nachbarn als low mit Begründung.
rerank (optional, Default aus) aktiviert ein Legacy-Reranking für Stufe 1. In dem Modus kommen Stufe-1-Codes mit confidence: "score" und numerischem score (0–100) statt der kategorialen Confidence. Nur setzen, wenn ihr vom alten Format abhängt.
Response — Event-Stream¶
Jede Zeile ist ein SSE-Event der Form data: {...}\n\n. Es gibt drei type-Werte:
status — Fortschritt, für Spinner/Statuszeile:
Wenn während des Streams etwas schiefgeht, kommt {"type":"status","status":"error","message":"..."} — kein HTTP-Fehler, sondern ein Status-Event. Danach ist der Request beendet.
partial — Ergebnisse von Stufe 1 und 2 (layer: 1 oder 2):
{
"type": "partial",
"layer": 1,
"summary": "",
"codes": [
{
"code": "S83.53",
"description": "Riss des vorderen Kreuzbandes",
"confidence": "high",
"reasoning": "",
"score": 0
}
]
}
result — Ergebnis von Stufe 3, danach ist der Stream zu Ende:
{
"type": "result",
"layer": 3,
"summary": "Kurze Begründung der Empfehlung",
"codes": [
{
"code": "S83.53",
"description": "Riss des vorderen Kreuzbandes",
"confidence": "high",
"reasoning": "Warum dieser Code passt",
"score": null
}
]
}
Code-Felder¶
code,description: ICD-10-GM-Code + offizieller Katalogtext.confidence:"high"/"medium"bei Stufe 1 (schwächere Treffer werden nicht zurückgegeben),"high"/"medium"/"low"bei Stufe 2 und 3. Mitrerank: truenutzt Stufe 1 stattdessen"score"(Legacy).score: Legacy-Feld — Ähnlichkeit 0–100 nur wennconfidence"score"ist; sonst0(Stufe 1) bzw.null.reasoning: kurze Begründung, nur Stufe 2 und 3 (Stufe 1 lässt es leer).
Beispiel¶
Läuft server-seitig (Node 18+); von dort werden die Stufen ans eigene Frontend weitergereicht, z.B. über einen eigenen SSE-Endpoint:
const response = await fetch('https://medical-api.dev.unomed.ch/api/deep-research', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({ query: 'Kreuzbandriss rechts' }),
});
if (!response.ok) {
// 401 / 422 / 503 kommen als normale HTTP-Fehler, nicht als Stream
throw new Error(`ICD API ${response.status}: ${await response.text()}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const event = JSON.parse(line.slice(6));
if (event.type === 'partial') forwardTier(event.layer, event.codes);
if (event.type === 'result') forwardTier(3, event.codes);
if (event.type === 'status' && event.status === 'error') forwardError(event.message);
}
}
Der Client-Timeout sollte mindestens 120 Sekunden betragen — Stufe 3 kann lang sein.
POST /api/documents/extract¶
Nimmt den Klartext eines medizinischen Dokuments und gibt ein strukturiertes JSON-Objekt zurück — Patient, Versicherung, Absender, Empfänger, Diagnosen, Medikation. Synchron, kein Streaming; ein typisches Dokument braucht 2–5 Sekunden.
Request¶
document_type ist "zuweisung" (Anmeldung/Überweisung an eine Institution) oder "arztbrief" (Entlass-/Verlaufsbrief, Befund) und bestimmt das Schema. text ist das Dokument als Klartext, 1–20 000 Zeichen; PDFs und Scans müssen auf eurer Seite in Text umgewandelt werden (OCR-Text funktioniert).
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" },
"fragestellung": "Indikation zur Knie-TP rechts?",
"diagnosen": ["Gonarthrose rechts", "Arterielle Hypertonie"],
"medikation": ["Lisinopril 10mg 1-0-0"],
"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 enthält immer alle Top-Level-Keys. Was nicht ausdrücklich im Dokument steht, ist null bzw. eine leere Liste. Datumsangaben sind ISO-normalisiert (YYYY-MM-DD).
Beide Typen teilen dokument, patient, versicherung, absender, empfaenger, aufenthalt, fragestellung, diagnosen, medikation. Dazu kommt bei zuweisung kostengutsprache und dringlichkeit, bei arztbrief eingriffe und procedere.
Enum-Werte: patient.geschlecht m/w · versicherung.klasse allgemein/halbprivat/privat · aufenthalt.art stationaer/ambulant · dringlichkeit notfall/dringlich/elektiv · kostengutsprache liegt_vor/beantragt/wird_eingeholt. Sie bleiben null, solange das Dokument sie nicht ausdrücklich nennt — eine "Privatklinik" macht die Versicherungsklasse nicht privat, ein Wunschtermin macht eine Zuweisung nicht dringlich.
Grounding — warum nichts erfunden werden kann¶
Nach der Modell-Antwort prüft ein deterministischer Validator jeden Wert gegen das Quelldokument. Was er dort nicht belegen kann, wird auf null gesetzt und in removed_by_grounding aufgeführt:
Wörtliche Felder (Namen, Adressen, Nummern, Daten, Diagnosen, Medikamente) brauchen einen Beleg im Dokument — Daten in allen gängigen Schreibweisen, Nummern über die Ziffernfolge, Freitext über Substring oder ≥ 60 % Token-Überdeckung. Enum-Felder brauchen ein explizites Schlüsselwort.
Garantiert ist damit: ein erfundener Wert erreicht euch nicht. Nicht abgedeckt ist die Fehlzuordnung — ein Wert, der im Dokument steht, aber im falschen Feld landet (z.B. die Praxisadresse als Patientenadresse), besteht den Beleg-Check. Dafür braucht es einen menschlichen Review-Schritt auf den relevanten Feldern und eine Anzeige des Quelldokuments daneben.
Ein nicht-leeres removed_by_grounding ist ein brauchbares Review-Signal: das Modell hat etwas behauptet, was das Dokument nicht hergibt.
POST /api/llm/generate¶
Allzweck-Endpoint: Dokumenttext plus Anweisung rein, Antwort des Modells als Text zurück. Für Aufgaben, für die es noch keinen eigenen Endpoint gibt — Brief zusammenfassen, patientenverständlich umschreiben, eine bestimmte Passage herausziehen.
Wenn ihr ein strukturiertes Ergebnis braucht, nehmt POST /api/documents/extract — der ist schemagebunden und belegt-geprüft. Dieser Endpoint liefert Freitext ohne solche Garantien.
Request¶
{
"text": "Zuweisung vom 12.08.2026 ...",
"prompt": "Fasse die Fragestellung dieser Zuweisung in einem Satz zusammen."
}
text ist das Dokument (1–20 000 Zeichen), prompt die Anweisung (1–5 000 Zeichen). prompt geht als System-Prompt rein, text als User-Message.
Response¶
{
"output": "Der Hausarzt möchte klären, ob eine Kniegelenksprothese rechts indiziert ist.",
"model": "eu.anthropic.claude-haiku-4-5-...",
"usage": { "tokens_in": 207, "tokens_out": 56 },
"processing_time_ms": 1397.92
}
output ist Klartext; Modelle setzen gern Markdown-Überschriften davor — wenn ihr einen nackten Satz wollt, schreibt das in den prompt. Die Ausgabe wird nicht validiert: es prüft niemand, ob die Antwort durch text gedeckt ist. Also als Entwurf für einen Menschen behandeln, nicht als Fakt.
Health¶
Ohne Auth erreichbar:
GET /api/health— Basis-Verfügbarkeit + geladene Katalog-Grösse.GET /api/llm/health— Verfügbarkeit des LLM-Backends. Betrifft Stufe 2 und 3 der Deep Research sowie/api/documents/extractund/api/llm/generate. Wenn die als down zurückkommt, läuft Stufe 1 der Deep Research trotzdem — die anderen beiden Endpoints nicht.
Fehlercodes¶
401— API-Key fehlt oder ist ungültig.422— Request-Validierung fehlgeschlagen (z.B.queryleer oder zu lang, unbekannterdocument_type).429— Rate-Limit oder Tageskontingent des Keys erreicht; derRetry-After-Header sagt, wann es weitergeht.502— LLM-Call fehlgeschlagen oder ohne brauchbares Ergebnis. Nur auf den LLM-Endpoints; einmal wiederholen, danach als Störung behandeln.503— Service noch nicht bereit oder LLM-Backend nicht konfiguriert.
Auf den dokumentverarbeitenden Endpoints sind die Fehlermeldungen bewusst generisch: Provider-Fehlertexte können Teile des Requests zitieren, und der kann Patientendaten enthalten.