FHIR Server (+)

<< Klicken Sie, um das Inhaltsverzeichnis anzuzeigen >>

Navigation:  Schnittstellen >

FHIR Server (+)

EMMA erweitert RheDAT um einen FHIR-R4-Server, über den externe Systeme (Klinik-Middleware, Statistik-Software, Studien-Datenbanken, Patienten-Apps) lesend und schreibend auf den Patientenbestand zugreifen können. Der Zugriff ist OAuth2-geschützt; pro zugreifendem System wird ein eigener Client mit individuellem Secret eingerichtet. Sie finden die Einstellungen unter Administration|Schnittstellen.

clip1109

1. Client-Liste — alle aktuell eingerichteten FHIR-Clients. Jede Zelle ist direkt editierbar; Änderungen werden ohne Bestätigung sofort gespeichert. Spalten:

Client-ID — eindeutiger technischer Name, den der Client beim Login (client_id) verwendet. Konvention: Kleinbuchstaben, Bindestriche, sprechend („klinik-meinhof", „rhdv-statistik").

AktivJa: Client kann sich anmelden. Nein: Login wird abgelehnt (HTTP 403). Die Zeile bleibt erhalten, der Zugriff ist nur temporär ausgesetzt.

AnonymJa: alle Patient-Resourcen, die dieser Client abruft, werden ohne Klartextnamen, Geburtsdatum und externe Identifier (KIS-ID, PVS-ID etc.) ausgeliefert. Pseudonyme (interne Subject-ID, Studien-Patient-IDs) bleiben enthalten, damit Datensätze referenzierbar bleiben. Nein: Patientendaten kommen mit allen Klartextangaben.

Einschränken auf StudieKeine Einschränkung: Client darf den gesamten Patientenbestand sehen („Statistiker-Modus"). Oder Auswahl einer konkreten Studie: Client sieht ausschließlich die enrollten Subjects dieser Studie, und auch in der Patient-Resource erscheint nur der Studien-Identifier dieser einen Studie.

Consent — relevant nur bei aktivierter Studien-Restriktion. Ja: Aus den enrollten Subjects der Studie werden zusätzlich nur die ausgeliefert, deren Einverständnis (consent_flag) gesetzt ist. Nein: alle enrollten Subjects der Studie, der aktuelle Consent-Status wird über separate Consent-Resourcen mitgeteilt. Hartes Ausschluss-Kriterium ist immer refuse_flag — wer aktiv widersprochen hat, wird unter keinen Umständen exportiert.

Erlaubte ItemTypes (inbound) — Whitelist der ItemType-IDs, die dieser Client per POST/PUT schreiben darf. Default ist leer = kein Schreibrecht. Per Klick öffnet sich ein Auswahldialog mit allen dokumentierten ItemTypes (alphabetisch, mit Suche und Bereichsangabe in Klammern). Reine Berechnungs-Felder (BMI etc.) erscheinen nicht in der Liste, weil sie der Server selbst berechnet und nicht direkt überschrieben werden dürfen. Die Anzeige in der Zelle zeigt eine Kurzform „N Items: Titel1, Titel2, Titel3, …".

Erlaubte ItemTypes (outbound) — Gegenstück zur inbound-Whitelist: legt fest, welche ItemType-IDs dieser Client lesen bzw. über $bundle exportieren darf. Das ist eine harte Grenze: nur freigegebene Werte verlassen den Server, alles andere wird schon serverseitig aus dem Bundle entfernt; ein $bundle?type=<id> auf einen nicht freigegebenen Typ liefert 403. Der Auswahldialog bietet zwei zusätzliche Sammel-Einträge: Alles (alle Daten inkl. Medikation) und Medikation (die gesamte Medikationshistorie, die an keinem einzelnen ItemType hängt). Neue wie bestehende Clients sind mit Alles vorbelegt — schränken Sie bewusst ein, wenn ein Client nur bestimmte Felder sehen soll. Ist die Liste leer, wird kein klinischer Item-Wert exportiert (die Patient-Stammdaten unterliegen weiterhin der Studie- und Anonym-Einstellung, nicht dieser Liste).

Redirect-URLs (Auth-Code) — steuert, ob dieser Client sich zusätzlich über den SMART Authorization-Code-Flow mit PKCE anmelden darf. Leer: ausschließlich Client-Credentials (wie bisher). Sind eine oder mehrere Redirect-URLs hinterlegt (eine pro Zeile), ist der Authorization-Code-Flow für diesen Client freigeschaltet — beschränkt auf genau diese Adressen. Per Klick öffnet sich ein Editor zum Eintragen der URLs. Benötigt wird das für Datenintegrations-Werkzeuge, die zwingend diesen Flow verlangen (z. B. REDCap). Die Zelle zeigt „(nur Client-Credentials)" bzw. „Auth-Code: N URLs".

clip1107

2. Hinzufügen — Legt einen neuen Client an. Sie werden zur Eingabe einer Client-ID aufgefordert; diese wird als client_id verwendet. Anschließend generiert EMIL ein 48-Zeichen-Zufalls-Secret und zeigt es einmalig in einem Dialog zum Kopieren. Bewahren Sie das Secret sicher auf — im Klartext wird es nie wieder angezeigt, der Server kennt nur den SHA-3-Hash.

clip1106

3. Neues Secret — Erzeugt für den in der Tabelle ausgewählten Client ein neues Secret. Das vorhergehende Secret wird unmittelbar ungültig. Anwendung z. B. bei Verlust oder turnusmäßiger Erneuerung.

4. Löschen — Entfernt den ausgewählten Client unwiderruflich nach Bestätigung. Bereits erteilte Tokens bleiben bis zu ihrer Ablaufzeit (Default eine Stunde) noch gültig.

Audit-Trail. Jeder FHIR-Client wird im EMIL-Audit-Trail wie ein interner Benutzer behandelt: jede Schreibaktion erscheint als „FHIR: <Client-ID>" in den Historien-Einträgen (Subject – History) und in den Datenbank-Audit-Views. Bei der Rückverfolgung von Änderungen sieht der Admin auf einen Blick, welcher Client wann was geschrieben hat.

Schnelleinstieg für Entwickler

Alle Beispiele unten sind generische HTTP-Aufrufe. Sie funktionieren mit jedem HTTP-Werkzeug: curl auf der Kommandozeile, Postman, Insomnia, HTTPie oder jeder Sprache mit HTTP-Client. Die Beispiele zeigen den ausgeführten Request inkl. ggf. Body; ersetzen Sie RheDAT-Server:8443 durch Ihren tatsächlichen RheDAT-Host plus Port und <token> durch den von Schritt 1 erhaltenen Bearer-Token.  

Schritt 1: Anmeldung (OAuth2)

Der Server bietet zwei OAuth2-Anmeldeverfahren an. Client-Credentials (Variante A) ist der einfache Server-zu-Server-Weg und für die meisten Anbindungen richtig. Der Authorization-Code-Flow mit PKCE (Variante B) ist für Werkzeuge gedacht, die ihn zwingend verlangen (z. B. REDCap); er ist nur aktiv, wenn beim Client Redirect-URLs hinterlegt sind. Beide Verfahren führen zum selben Bearer-Token mit identischen Zugriffsrechten.

Variante A — Client-Credentials. Vor jedem Datenzugriff besorgt sich der Client einen Bearer-Token vom Token-Endpoint:

POST https://RheDAT-Server:8443/fhir/token

Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=klinik-meinhof&client_secret=<plain-secret>

Antwort:

{

"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",

"token_type": "Bearer",

"expires_in": 3600

}

Der Token wird in den Folgeanfragen als HTTP-Header mitgegeben:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Tokens sind Default eine Stunde gültig und werden vom Server selbst signiert (HMAC-SHA-256); eine Online-Verifikation durch Drittsysteme ist nicht erforderlich. Bei Ablauf antwortet der Server mit 401 und WWW-Authenticate: Bearer error="invalid_token" — einfach einen neuen Token holen.

Hinweis — Änderungen in der Konfiguration: Aktiv, Anonym, Studie und Consent sind im Token selbst eingebrannt — dafür ist nach Änderung ein neuer Token nötig; dasselbe gilt für Erlaubte ItemTypes (outbound). Erlaubte ItemTypes (inbound) dagegen wird bei jedem Schreibvorgang frisch aus der Datenbank gelesen und greift sofort, auch mit altem Token.

Variante B — Authorization-Code-Flow mit PKCE. Manche Werkzeuge (z. B. REDCap) verlangen zwingend diesen Flow. Er ist nur verfügbar, wenn beim Client mindestens eine Redirect-URL hinterlegt ist (siehe Client-Liste oben). Die Zugriffsrechte (Studie, Anonym, Consent, erlaubte ItemTypes) gelten unverändert — der Flow ändert nur die Art der Anmeldung, nicht die Berechtigung.

B1 — Autorisierung im Browser. Das anfragende Werkzeug erzeugt ein PKCE-Paar (ein zufälliges code_verifier und daraus code_challenge = base64url(SHA256(code_verifier))) und leitet den Browser des Anwenders auf den Authorize-Endpoint:

GET https://RheDAT-Server:8443/fhir/authorize

?response_type=code

&client_id=klinik-meinhof

&redirect_uri=https://tool.example.org/callback

&scope=offline_access

&state=xyz123

&code_challenge=<base64url-hash>

&code_challenge_method=S256

Der Server prüft die Anfrage und leitet den Browser standardmäßig ohne weitere Eingabe zurück auf die redirect_uri und hängt den einmaligen Autorisierungs-Code an — der Client weist sich erst beim anschließenden Token-Tausch per Secret und PKCE aus. So ist eine unbeaufsichtigte Verbindung möglich. Ist am Server [FHIR]AUTHCONSENT=1 gesetzt, erscheint stattdessen eine Bestätigungsseite, auf der das Client-Secret einzugeben ist. Rücksprung:

https://tool.example.org/callback?code=<code>&state=xyz123

B2 — Code gegen Token tauschen. Das Werkzeug löst den Code server-seitig (ohne Browser) am Token-Endpoint ein und weist per code_verifier nach, dass es der ursprüngliche Anfrager ist:

POST https://RheDAT-Server:8443/fhir/token

Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&code_verifier=<verifier>

&redirect_uri=https://tool.example.org/callback

&client_id=klinik-meinhof&client_secret=<plain-secret>

Antwort:

{

"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",

"token_type": "Bearer",

"expires_in": 3600,

"refresh_token": "8Kf3c1...",

"scope": "offline_access"

}

Der access_token wird danach wie in Variante A als Authorization: Bearer … mitgegeben. Ein refresh_token wird nur ausgestellt, wenn der Scope offline_access angefordert wurde — damit kann das Werkzeug ohne erneute Browser-Anmeldung neue Access-Tokens holen (z. B. für geplante Hintergrund-Abrufe). Ob dieser Scope mitgeschickt wird, entscheidet das anfragende Werkzeug; in REDCap muss dazu in der CDIS-/FHIR-Endpunkt-Konfiguration die persistente Verbindung bzw. der Refresh-Token aktiviert sein — sonst bleibt es bei sitzungsgebundenen Tokens ohne automatische Erneuerung.

B3 — Token erneuern (optional). Solange der Refresh-Token gültig ist, holt sich das Werkzeug damit einen frischen Access-Token — ohne den Browser-Schritt zu wiederholen:

POST https://RheDAT-Server:8443/fhir/token

grant_type=refresh_token&refresh_token=<refresh_token>

&client_id=klinik-meinhof&client_secret=<plain-secret>

Die Antwort enthält einen neuen Access- und Refresh-Token; der bisherige Refresh-Token wird dabei ungültig (Rotation).

Wichtig. Die redirect_uri muss bei jedem Aufruf exakt einer der beim Client hinterlegten Redirect-URLs entsprechen — andernfalls lehnt der Server ab und leitet nicht weiter (Schutz vor Umleitungs-Angriffen). Als PKCE-Verfahren wird ausschließlich S256 akzeptiert. Der Autorisierungs-Code ist einmalig und nur wenige Minuten gültig.

Server-Einstellungen (config.ini, Abschnitt [FHIR]). TOKENTTL — Gültigkeit der Access-Tokens in Sekunden (Standard 3600). REFRESHTTL — Gültigkeit der Refresh-Tokens (Standard 30 Tage). REFRESHENABLED=0 — schaltet den Refresh-Token-Mechanismus serverweit ab. AUTHCONSENT=1 — verlangt am Authorize-Endpoint eine interaktive Bestätigung mit Secret-Eingabe (Standard: aus, damit unbeaufsichtigte Anbindungen wie REDCap ohne wiederholte Secret-Eingabe funktionieren). Die unterstützten Verfahren und Endpunkte kündigt der Server auch unter GET /fhir/.well-known/smart-configuration an (Authorize- und Token-URL, Grant-Types, code_challenge_methods_supported).

Schritt 2: Was kann der Server? — das Manifest

Der Endpunkt /fhir/metadata liefert eine maschinenlesbare Beschreibung aller verfügbaren Endpunkte, Suchparameter und Operationen — in FHIR-Sprache heißt das ein CapabilityStatement. Wer noch nie mit einem FHIR-Server gesprochen hat, sollte hier anfangen:

GET https://RheDAT-Server:8443/fhir/metadata

Accept: application/fhir+json

Kein Auth-Header nötig — das Manifest ist öffentlich, damit Clients die Schnittstelle entdecken können, bevor sie sich anmelden.

Auszug aus einer typischen Antwort:

{

"resourceType": "CapabilityStatement",

"status": "active",

"fhirVersion": "4.0.1",

"format": ["json"],

"software": { "name": "EMIL FHIR Server", "version": "8.40.0" },

"implementation":{ "description": "EMIL FHIR Endpoint (Build 8.40.0)",

"url": "https://RheDAT-Server:8443/fhir" },

"rest": [{

"mode": "server",

"security": {

"cors": true,

"service": [{ "coding": [{ "code": "OAuth", "display": "OAuth2 client_credentials" }] }],

"extension": [{ "url": "...oauth-uris",

"extension": [{ "url": "token",

"valueUri": "https://RheDAT-Server:8443/fhir/token" }] }]

},

"resource": [

{ "type": "Patient",

"interaction": [{ "code": "read" }, { "code": "search-type" }],

"searchParam": [{ "name": "identifier", "type": "token" },

{ "name": "family", "type": "string" },

{ "name": "given", "type": "string" },

{ "name": "name", "type": "string" }]

},

{ "type": "Observation",

"interaction": [{ "code": "create" }, { "code": "update" }]

},

{ "type": "QuestionnaireResponse",

"interaction": [{ "code": "create" }, { "code": "update" }]

},

{ "type": "Questionnaire", "interaction": [{ "code": "read" }] },

{ "type": "CodeSystem", "interaction": [{ "code": "read" }] }

],

"operation": [{ "name": "bundle", "definition": "...OperationDefinition/system-bundle" }]

}]

}

Was Sie daraus lesen können:

Token-URLrest[0].security.extension[].extension[?(url=='token')].valueUri. Hier holen Sie sich Schritt 1.

Welche Resourcen lese-/schreibbar sind — jeder resource[]-Eintrag mit seinen interaction[]-Codes: read, search-type, create, update.

Welche Such-Parameter es für eine Resource gibt — resource[].searchParam[].

System-Operationen wie $bundleoperation[].

Server-Versionsoftware.version. Bei Support-Anfragen immer mit angeben.

Jeder Eintrag trägt zudem eine documentation-Property mit ausführlicher Erklärung (Mapping-Regeln, Whitelist-Verhalten, Default-Werte etc.) — den Inhalt dieser Hilfe finden Sie dort in Kurzform direkt am Server.

SMART-Discovery. Alternativ liefert GET /fhir/.well-known/smart-configuration eine kompaktere Variante speziell für OAuth-Clients (Token-URL, unterstützte Grant-Types). Standardisiert nach „SMART on FHIR".

Schritt 3: Was bedeuten die internen IDs? — CodeSystems

EMIL-Resourcen verweisen über numerische IDs auf interne Konzepte: ein ItemType ist z. B. 10038 = Körpergewicht, eine Studie ist 1850 = irgendwas, ein Medikament ist 74211 = Methotrexat. Die Bedeutung dieser IDs wird über vier CodeSystem-Resourcen aufgelöst:

GET /fhir/CodeSystem/itemtype # ItemType-Wörterbuch (Felder)

GET /fhir/CodeSystem/study # Studienkatalog

GET /fhir/CodeSystem/drug # Medikamentenkatalog

GET /fhir/CodeSystem/stop-reason # Absetzgründe (1..10)

GET /fhir/CodeSystem/image-category # Bild-Kategorien (IMAGE_CAT)

Alle CodeSystems unterstützen Paging via _count (Default 1000) und _offset (Default 0). Bei geschnittenen Antworten liefert der Server zusätzlich meta.tag mit code="partial" und einen Link-Header mit rel="next"/"prev"/"first"/"last". Die Gesamtanzahl steht immer in der Top-Level-Property count (unabhängig vom Slice).

CodeSystem/itemtype — Felder-Wörterbuch

Das wichtigste CodeSystem: hier finden Sie für jeden EMIL-ItemType (also jedes Feld, das in einer Patientenakte vorkommen kann) Titel, Datentyp und ggf. die Auswahlwerte. Beispiel-Aufruf und gekürzte Antwort:

GET /fhir/CodeSystem/itemtype?_count=3

Authorization: Bearer <token>

{

"resourceType": "CodeSystem",

"id": "itemtype",

"url": "https://emil.itc-ms.de/fhir/sid/itemtype",

"name": "EMILItemType",

"status": "active",

"content": "complete",

"count": 2483,

"property": [

{ "code": "area", "type": "code", "description": "L=laboratory, F=fixed/master data, V=visits" },

{ "code": "unit", "type": "string", "description": "Display unit (UCUM-aligned where applicable)" },

{ "code": "variable", "type": "string", "description": "Internal variable name used in EMIL formulas" },

{ "code": "kind", "type": "code", "description": "Item kind. Determines value cardinality..." },

{ "code": "enabled", "type": "boolean","description": "Only set on child concepts (coded values)..." }

],

"concept": [

{ "code": "10038",

"display": "Körpergewicht",

"property": [ { "code": "area", "valueCode": "V" },

{ "code": "unit", "valueString": "kg" },

{ "code": "variable", "valueString": "weight" },

{ "code": "kind", "valueCode": "number" } ]

},

{ "code": "10042",

"display": "BMI",

"property": [ { "code": "area", "valueCode": "V" },

{ "code": "unit", "valueString": "kg/m2" },

{ "code": "kind", "valueCode": "calc" } ]

},

{ "code": "12345",

"display": "Allergien",

"property": [ { "code": "area", "valueCode": "F" },

{ "code": "kind", "valueCode": "mlist" } ],

"concept": [

{ "code": "12345/POLLEN", "display": "Pollen" },

{ "code": "12345/PEANUT", "display": "Erdnüsse" },

{ "code": "12345/PENICILLIN", "display": "Penicillin",

"property": [ { "code": "enabled", "valueBoolean": false } ] }

]

}

]

}

So lesen Sie das:

concept[].code ist die ItemType-ID. Damit bauen Sie in Observation/QuestionnaireResponse die code.coding[].code-Felder.

property/kind sagt, was für ein Feldtyp das ist und wie der Wert auszusehen hat: number/numberuObservation.valueQuantity mit der Einheit aus property/unit. slistObservation.valueCodeableConcept mit genau einem coding[]. mlistObservation.valueCodeableConcept mit einem coding[] pro gewählter Option. text und Varianten → valueString. date/datefuz/datefuzm (+ u-Varianten) → valueString mit dem Datum als ISO-String. form/calcform/custform/custhform → eigene QuestionnaireResponse statt Observation. calc → berechnetes Feld, nicht direkt schreibbar. Der Server berechnet es selbst aus seinen Quell-Items.

concept[].concept[] — verschachtelte Sub-Concepts. Nur bei kind=slist/mlist: die zulässigen Auswahlwerte. Der Sub-Code hat die Form <typeid>/<rawcode>genau diese Form erscheint später in der Observation. Historische Werte (im EMIL als ausgeblendet markiert) tragen property/enabled = false.

property/area: L = Labor, F = Stammdaten (einmalig pro Patient), V = Verlauf (pro Visit). Bestimmt, in welche Art von Encounter der Wert geschrieben werden muss (Stammdaten landen automatisch in der Fixed-Visit).

property/unit: die EMIL-Stamm-Einheit. Bei Inbound müssen Sie den Wert in dieser Einheit liefern; eine andere Einheit führt zu einem 400er-Fehler.

CodeSystem/study — Studienkatalog

GET /fhir/CodeSystem/study

Authorization: Bearer <token>

{

"resourceType": "CodeSystem",

"id": "study",

"url": "https://emil.itc-ms.de/fhir/sid/study",

"name": "EMILStudy",

"status": "active",

"count": 47,

"concept": [

{ "code": "1850", "display": "RhDV Erwachsene 2024" },

{ "code": "1851", "display": "RABBIT" },

{ "code": "1852", "display": "Kids-RhDV" }

]

}

Die code-Werte sind die internen Studien-IDs, die Sie z. B. in $bundle?study=<id> oder in der Konfiguration des Clients (Einschränken auf Studie) verwenden. Studien-restricted Clients sehen nur Studien, in denen ihre Patienten enrollt sind.

CodeSystem/drug — Medikamentenkatalog

GET /fhir/CodeSystem/drug?_count=5&_offset=0

Authorization: Bearer <token>

{

"resourceType": "CodeSystem",

"id": "drug",

"url": "https://emil.itc-ms.de/fhir/sid/drug",

"name": "EMILDrug",

"status": "active",

"count": 3214,

"concept": [

{ "code": "-1", "display": "Glukokortikoide" },

{ "code": "74211", "display": "Methotrexat" },

{ "code": "74212", "display": "Sulfasalazin" },

{ "code": "74213", "display": "Leflunomid" }

]

}

Wird referenziert von MedicationStatement.medicationCodeableConcept. Der Sonderfall -1 „Glukokortikoide" deckt die strukturierte Kortison-Dokumentation ab (s. Abschnitt MedicationStatement).

CodeSystem/stop-reason — Absetzgründe

GET /fhir/CodeSystem/stop-reason

Authorization: Bearer <token>

{

"resourceType": "CodeSystem",

"id": "stop-reason",

"url": "https://emil.itc-ms.de/fhir/sid/stop-reason",

"status": "active",

"count": 10,

"concept": [

{ "code": "1", "display": "Remission" },

{ "code": "2", "display": "Nebenwirkung" },

{ "code": "3", "display": "Unzureichende Wirkung" },

{ "code": "4", "display": "Kontraindikation" },

{ "code": "5", "display": "Patientenwunsch / Kinderwunsch / Schwangerschaft" },

{ "code": "6", "display": "Compliance" },

{ "code": "7", "display": "Kosten" },

{ "code": "8", "display": "Sonstiges" },

{ "code": "9", "display": "Wirkverlust" },

{ "code": "10", "display": "Fehlendes Ansprechen" }

]

}

Wird referenziert von MedicationStatement.statusReason[].coding[] bei abgesetzten Medikationen.

CodeSystem/image-category — Bild-Kategorien

Die in EMIL gepflegten Bild-Kategorien (Tabelle IMAGE_CAT), auf die Media.modality verweist.

GET /fhir/CodeSystem/image-category

Authorization: Bearer <token>

{

"resourceType": "CodeSystem",

"id": "image-category",

"url": "https://emil.itc-ms.de/fhir/sid/image-category",

"status": "active",

"count": 6,

"concept": [

{ "code": "1", "display": "Befund" },

{ "code": "3", "display": "Röntgen" }

]

}

Lesen

Patient suchen

Bevor Sie etwas zu einem Patienten schreiben können, müssen Sie dessen interne EMIL-Subject-ID kennen. Diese ermittelt man über die Patient-Suche, mit beliebigen Identifiern aus den verknüpften Systemen:

GET /fhir/Patient?identifier=K12345

GET /fhir/Patient?identifier=https://emil.itc-ms.de/fhir/sid/kis|K12345

GET /fhir/Patient?family=mustermann

GET /fhir/Patient?name=schum&given=micha&_count=20

Die Antwort ist ein Bundle vom Typ searchset:

{

"resourceType": "Bundle",

"type": "searchset",

"total": 1,

"entry": [{

"resource": {

"resourceType": "Patient",

"id": "42",

"identifier": [

{ "system": "https://emil.itc-ms.de/fhir/sid/subject", "value": "42" },

{ "system": "https://emil.itc-ms.de/fhir/sid/kis", "value": "K12345" }

],

"name": [{ "use": "official", "family": "Mustermann", "given": ["Max"] }],

"birthDate": "1965-03-12",

"gender": "male"

}

}]

}

Die id (hier 42) ist die EMIL-Subject-ID, die Sie später in Schreibvorgängen als subject.reference: "Patient/42" verwenden. Wenn Ihr Client studien-restricted ist oder das Anonym-Flag gesetzt hat, sehen Sie nur die jeweils erlaubten Daten.

Identifier-Suche akzeptiert mit oder ohne system|-Präfix; intern probiert der Server alle bekannten Quellen durch (interne ID, KIS/PVS/OTHER/PAPER, alle Studien-Pseudonyme).

Patient einzeln lesen

GET /fhir/Patient/42

Authorization: Bearer <token>

Liefert dieselbe Patient-Resource wie im Such-Bundle, aber direkt — nützlich, wenn die ID bereits bekannt ist.

Questionnaire-Definition lesen

Pro Form-ItemType (Score/Custom-Form) gibt es eine schmale Questionnaire-Resource, die per Coding-Link auf den Dictionary-Eintrag im CodeSystem/itemtype verweist. Die Feldsemantik (linkIds, Skalen, Wertelisten) wird zentral in der EMIL-Dictionary-Dokumentation gepflegt:

GET /fhir/Questionnaire/52007

Authorization: Bearer <token>

Patientenbilder lesen (Media / Binary)

Bilder aus der Patientenakte (eingescannte Befunde, Fotos; EMIL-Tabelle IMAGE) werden als FHIR-Media-Resourcen bereitgestellt. Media liefert nur die Metadaten (Datum, Kategorie, Titel) und verweist über content.url auf den eigentlichen Bild-Download (Binary). Die Bytes werden also erst geladen, wenn der Consumer den Link gezielt abruft — eine Galerie-Übersicht bleibt damit schlank. Archivierte Bilder werden nie ausgeliefert.

GET /fhir/Media?patient=42

Authorization: Bearer <token>

Optional einschränkbar über &category=<cat-id> (IMAGE_CAT, siehe CodeSystem/image-category) und &date=ge2024-01-01 / &date=le2024-12-31; Paging via _count (Default 50) und _offset. Die Antwort ist ein searchset-Bundle; ein einzelnes Bild liefert GET /fhir/Media/<image-id>.

{

"resourceType": "Media",

"id": "1007",

"status": "completed",

"type": { "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/media-type", "code": "image" }] },

"modality": { "coding": [{ "system": "https://emil.itc-ms.de/fhir/sid/image-category", "code": "3", "display": "Röntgen" }] },

"subject": { "reference": "Patient/42" },

"createdDateTime": "2024-03-15",

"content": {

"contentType": "image/jpeg",

"url": "https://RheDAT-Server:8443/fhir/Binary/1007",

"title": "Thorax p.a."

}

}

Bild-Bytes laden (Binary)

Die in content.url genannte URL liefert die rohen Bild-Bytes:

GET /fhir/Binary/1007

Authorization: Bearer <token>

Antwort: 200 OK mit Content-Type: image/jpeg und dem JPEG im Body. Studien-restricted Clients sehen nur Bilder von Subjects ihrer Studie; ein fremdes Subject ergibt 404. Fehlt die Bilddatei trotz Datenbankeintrag, antwortet der Server ebenfalls mit 404.

Anonyme Clients: Bei gesetztem Anonym-Flag bleiben die Media-Metadaten sichtbar, der Byte-Abruf über Binary wird aber mit 403 abgelehnt — Bilder sind potenziell identifizierend.

Bundle-Export einer Kohorte ($bundle)

Der Haupt-Endpunkt für Massen-Exporte. Liefert pro Subject die vollständigen Items als Observation (für Lab/Visit-Werte) bzw. QuestionnaireResponse (für Form-Items), gruppiert über Encounter-Resourcen pro Visit. Zu jedem emittierten Encounter zusätzlich ein MedicationStatement-Snapshot pro Präparat. Die Medikation kommt als MedicationStatement in zwei Ausprägungen (vollständige Historie und Status quo pro Encounter, s. u.). Auf der ersten Seite zusätzlich ResearchStudy-Resourcen und je Subject ResearchSubject. Paging durch _count (Default 50 Subjects pro Seite) und _offset.

Outbound-Whitelist. Welche Item-Werte tatsächlich im Bundle landen, begrenzt zusätzlich die pro Client gepflegte Spalte Erlaubte ItemTypes (outbound) (siehe Client-Liste oben). Sie wirkt als harte Grenze im Server: nur freigegebene Observation/QuestionnaireResponse/MedicationStatement werden emittiert, alles andere wird noch vor dem Versand entfernt. Ein type=-Filter auf einen nicht freigegebenen Typ wird mit 403 abgelehnt. Der Sammel-Eintrag Medikation gibt gezielt die Medikationshistorie frei (dann liefert der Server den vollständigen history-Event-Stream; die encounter-status-Snapshots erscheinen nur, wenn zusätzlich mindestens ein Observation-Typ freigegeben ist, da sie an einem Encounter hängen). Ist die Liste leer, enthält das Bundle nur das Gerüst (Patient, ResearchSubject, Consent) ohne klinische Werte.

MedicationStatement — Medikation

Die Medikation sollte immer über die MedicationStatement-Resourcen ausgewertet werden — nicht über die Observations. Die einzelnen Medikamenten-Felder erscheinen technisch zwar auch als Observation, sind dort aber roh und schwer nutzbar. In den MedicationStatements bereitet der Server die Therapie sauber auf (Wirkstoff, Dosis, Zeitraum, Absetzgrund; Quelle: die rheumatologische Medikationshistorie).

Der Server liefert MedicationStatements in zwei Ausprägungen, unterscheidbar über meta.tag (System https://emil.itc-ms.de/fhir/CodeSystem/medstatement-source):

tag.code="history" — die vollständige Medikationshistorie als Verlauf: jede Ansetzung, jede Absetzung (mit Grund) und jede Dosisänderung als eigenes MedicationStatement, über alle Visits hinweg und so weit zurück wie dokumentiert. Das ist 1:1 die Medikationshistorie, die der Anwender in der Akte (Festdaten) sieht. Kein context.

tag.code="encounter-status" — der Status quo je Encounter: pro Visit für jedes Präparat im damaligen Therapieplan ein MedicationStatement. context referenziert den Encounter.

Wichtig: Die beiden Mengen überschneiden sich inhaltlich. Ein Konsument muss sich für genau eine tag.code-Menge entscheiden und über den Tag filtern — werden beide ungefiltert ausgewertet, wird dieselbe Therapie doppelt gezählt. Faustregel: für den Therapieverlauf / die Anamnese history, für eine visit-bezogene Auswertung „was war zum Zeitpunkt X aktiv" encounter-status.

Felder in beiden Ausprägungen:

status = active bei laufender Anwendung, stopped bei Absetzung.

medicationCodeableConcept mit Coding aus dem drug-CodeSystem; id=-1 ist der Sonderfall „Glukokortikoide".

dateAsserted = Datum, zu dem dieser Stand gilt (bei history das Event-Datum, bei encounter-status das State-Datum zum Visit).

effectivePeriod.start = Therapiebeginn (bei history der Gesamtbeginn der Therapie, auch für laufende Präparate), effectivePeriod.end = Absetzdatum bei Absetzungen.

dosage[].text = klartext-Dosis + Applikationsart (nur bei aktiven Präparaten).

statusReason[] bei Absetzungen: alle gesetzten Gründe als coding aus dem stop-reason-CodeSystem (Mehrfachnennung möglich). Eingegebene Freitexte zu Nebenwirkung/Kontraindikation/Sonstiges überschreiben das display.

Beispiel 1 — Voller Kohorten-Export einer Studie

GET /fhir/$bundle?study=1850

Authorization: Bearer <token>

Liefert alle enrolled Subjects der Studie 1850 mit allen ihren Items.

Beispiel 2 — Eingeschränkt auf bestimmte ItemTypes

GET /fhir/$bundle?study=1850&type=52007,52014&from=2024-01-01

Nur die Items dieser zwei TypeIDs aus Visits ab 2024-01-01. Ist einer der angeforderten Typen nicht in der Erlaubte ItemTypes (outbound)-Whitelist des Clients freigegeben, antwortet der Server mit 403.

Beispiel 3 — Mit Consent-Resourcen

GET /fhir/$bundle?study=1850&consent=1

Wie Beispiel 1, zusätzlich pro Subject eine Consent-Resource mit dem aktuellen Status (active/rejected/inactive).

Beispiel 4 — Einzelne Patienten gezielt

GET /fhir/$bundle?subject=42,17,8

Drei konkret benannte Patienten. Nur erlaubt für unrestricted Clients (Statistiker-Modus).

Beispiel 5 — Paging einer großen Kohorte

GET /fhir/$bundle?study=1850&_count=20&_offset=0

GET /fhir/$bundle?study=1850&_count=20&_offset=20

Der HTTP-Header Link: rel="next" und Bundle.link[] liefern die nächste/vorige Seite vorgefertigt.

Schreiben (Inbound)

Der FHIR-Server akzeptiert eingehende Daten für Observation (Einzelwerte) und QuestionnaireResponse (Formulare). Drei wichtige Eigenschaften vorweg:

1.Patienten werden nicht angelegt. Stammdaten werden ausschließlich in EMIL gepflegt. POST /Patient und PUT /Patient/<id> antworten beide mit 405 Method Not Allowed. Bitte zuerst per GET /Patient?identifier=... die interne ID ermitteln.

2.Whitelist pro Client. Geschrieben werden dürfen nur ItemType-IDs, die in der Spalte Erlaubte ItemTypes des Clients freigegeben sind. Default ist leer = kein Schreibrecht.

3.Idempotent. Ein POST /Observation mit (Subject, Visit-Datum, ItemType), zu dem schon ein Item existiert, überschreibt das bestehende. Mehrfaches Senden desselben Bodys ist sicher.

Observation schreiben

Komplettes Schritt-für-Schritt-Beispiel: Körpergewicht (TypeID 10038)

Angenommen, Sie wollen für einen Patienten an einem bestimmten Datum das Körpergewicht eintragen. Vorgehen:

Schritt A — ItemType nachschlagen

GET /fhir/CodeSystem/itemtype/?_count=2000

Im Ergebnis nach „Körpergewicht" suchen (oder den ItemType direkt in der EMIL-Dictionary-Doku nachsehen):

{ "code": "10038",

"display": "Körpergewicht",

"property": [

{ "code": "area", "valueCode": "V" },

{ "code": "unit", "valueString": "kg" },

{ "code": "variable", "valueString": "weight" },

{ "code": "kind", "valueCode": "number" }

]

}

Das sagt Ihnen: ItemType 10038, Area „V" (Visit), Einheit kg, Datentyp number. Der Wert muss also als valueQuantity mit Einheit kg kommen.

Schritt B — ItemType in der EMIL-Schnittstellen-Konfig freischalten

In der GUI: Einstellungen → Schnittstellen → FHIR. Beim betreffenden Client in der Spalte Erlaubte ItemTypes klicken, im Picker „Körpergewicht" auswählen, OK. Wirkt sofort, auch ohne neuen Token.

Schritt C — Patient-ID besorgen

GET /fhir/Patient?identifier=K12345

Authorization: Bearer <token>

→ aus der Antwort die id entnehmen, z. B. 42.

Schritt D — Observation senden

POST /fhir/Observation

Authorization: Bearer <token>

Content-Type: application/fhir+json

{

"resourceType": "Observation",

"status": "final",

"subject": { "reference": "Patient/42" },

"code": {

"coding": [{

"system": "https://emil.itc-ms.de/fhir/sid/itemtype",

"code": "10038"

}]

},

"effectiveDateTime": "2026-05-17",

"valueQuantity": {

"value": 72.5,

"unit": "kg",

"system": "http://unitsofmeasure.org"

}

}

Antwort: 201 Created, Header Location: .../Observation/<itemid> und Body:

{

"resourceType": "OperationOutcome",

"issue": [{

"severity": "information",

"code": "informational",

"diagnostics": "Observation gespeichert: Patient/42, Encounter/142862, Observation/2247369."

}]

}

Die EMIL-Item-ID (hier 2247369) können Sie für spätere Updates (per PUT) merken. Der Encounter (142862) wurde entweder an dem Datum gefunden oder neu angelegt. Sofort danach erscheint der Wert im EMIL-Client; im Audit-Trail steht „FHIR: <Client-ID>" als Urheber.

Visit-Auflösung

Wenn keine encounter.reference angegeben ist, sucht der Server eine vorhandene Visit am effectiveDateTime-Datum für das Subject (passender Area, also V für normale Visits, L für Labor, F für Stammdaten). Wenn keine existiert, wird sie automatisch angelegt. Mehrere Observations zum selben Datum landen damit in derselben Visit.

Alternativ können Sie eine bestehende Visit gezielt referenzieren (sofern Sie deren ID kennen, z. B. aus einem vorherigen Bundle-Read):

"encounter": { "reference": "Encounter/142862" }

Coded Value (SLIST/MLIST)

Für ItemTypes mit kind=slist oder kind=mlist: Body verwendet valueCodeableConcept mit einem oder mehreren coding[]-Einträgen. Die Codes haben die Form <typeid>/<rawcode> wie im CodeSystem dokumentiert (das $-Präfix aus der EMIL-Datenbank wird NICHT mitgeschickt).

Beispiel für ein Multi-Select „Allergien" (TypeID 12345) mit zwei gewählten Codes:

POST /fhir/Observation

Authorization: Bearer <token>

Content-Type: application/fhir+json

{

"resourceType": "Observation",

"status": "final",

"subject": { "reference": "Patient/42" },

"code": {

"coding": [{

"system": "https://emil.itc-ms.de/fhir/sid/itemtype",

"code": "12345"

}]

},

"effectiveDateTime": "2026-05-17",

"valueCodeableConcept": {

"coding": [

{ "system": "https://emil.itc-ms.de/fhir/sid/itemtype", "code": "12345/POLLEN" },

{ "system": "https://emil.itc-ms.de/fhir/sid/itemtype", "code": "12345/PEANUT" }

]

}

}

Für kind=slist (Einfachauswahl): genau ein coding[]-Eintrag.

Freitext

POST /fhir/Observation

Authorization: Bearer <token>

Content-Type: application/fhir+json

{

"resourceType": "Observation",

"status": "final",

"subject": { "reference": "Patient/42" },

"code": { "coding": [{ "system": "https://emil.itc-ms.de/fhir/sid/itemtype", "code": "20007" }] },

"effectiveDateTime": "2026-05-17",

"valueString": "Patient klagt über Müdigkeit nach Belastung."

}

Update (PUT)

Den Wert eines bereits geschriebenen Items ändern:

PUT /fhir/Observation/2247369

Authorization: Bearer <token>

Content-Type: application/fhir+json

{ ... gleicher Body wie oben, mit geändertem value ... }

200 OK. Bei nicht existierender ID: 404 Not Found (kein Update-as-Create).

POST oder PUT? Für normale Schreibvorgänge reicht POST — er ist idempotent (gleicher Body → gleicher Endzustand). PUT brauchen Sie nur, wenn Sie explizit ein bestimmtes Item per ID referenzieren wollen.

QuestionnaireResponse schreiben

Für Form-ItemTypes (kind form/calcform/custform/custhform): jedes Subitem wird ein item[]-Eintrag mit linkId + answer[0].value[x]. Schritt-für-Schritt analog zu Observation:

1.ItemType-ID nachschlagen (CodeSystem/itemtype, kind-Property prüfen).

2.In der Schnittstellen-Konfig freischalten.

3.Patient-ID besorgen.

4.POST schicken.

Einfaches Formular (z. B. HAQ-Score, TypeID 52007)

POST /fhir/QuestionnaireResponse

Authorization: Bearer <token>

Content-Type: application/fhir+json

{

"resourceType": "QuestionnaireResponse",

"status": "completed",

"questionnaire": "Questionnaire/52007",

"subject": { "reference": "Patient/42" },

"authored": "2026-05-17",

"item": [

{ "linkId": "anziehen", "answer": [{ "valueInteger": 1 }] },

{ "linkId": "aufstehen", "answer": [{ "valueInteger": 0 }] },

{ "linkId": "essen", "answer": [{ "valueInteger": 2 }] },

{ "linkId": "memo", "answer": [{ "valueString": "leichter Tag" }] }

]

}

Bei kind=calcform berechnet der Server den Score (vor #255 im EMIL-Storage) selbstständig — ein optional mitgesendetes linkId="score" wird beim nächsten Recalc überschrieben.

Multiform: mehrere Einträge in einem Item

Für Form-Items, in denen mehrere strukturierte Einträge nebeneinander stehen (z. B. Medikamentenliste): top-level item[]-Einträge mit linkId="row" und verschachteltem item[] pro Zeile.

POST /fhir/QuestionnaireResponse

Authorization: Bearer <token>

Content-Type: application/fhir+json

{

"resourceType": "QuestionnaireResponse",

"status": "completed",

"questionnaire": "Questionnaire/52126",

"subject": { "reference": "Patient/42" },

"authored": "2026-05-17",

"item": [

{ "linkId": "row", "item": [

{ "linkId": "drug", "answer": [{ "valueString": "Methotrexat" }] },

{ "linkId": "dose", "answer": [{ "valueString": "15" }] },

{ "linkId": "unit", "answer": [{ "valueString": "mg" }] },

{ "linkId": "applic", "answer": [{ "valueString": "SC" }] }

]},

{ "linkId": "row", "item": [

{ "linkId": "drug", "answer": [{ "valueString": "Folsäure" }] },

{ "linkId": "dose", "answer": [{ "valueString": "5" }] },

{ "linkId": "unit", "answer": [{ "valueString": "mg" }] }

]}

]

}

Checkboxen

Checkboxen werden im EMIL-Storage nur emittiert, wenn sie angekreuzt sind. Beim Schreiben:

{ "linkId": "cbprednisolon", "answer": [{ "valueBoolean": true }] }

→ landet als "cbprednisolon": "1" im Storage. false oder Weglassen führt dazu, dass der Eintrag nicht erzeugt wird.

Anonymisierung

Wenn der Client das Anonym-Flag gesetzt hat oder die Anfrage zusätzlich ?anonymize=1 mitgibt, werden alle Patient-Resourcen ohne Klartext-Personendaten ausgeliefert. Pseudonyme (Subject-ID, Studien-Patient-IDs) und alle Items bleiben enthalten — Auswertungen werden so nicht verhindert, der Personenbezug wird aber entfernt.

Für Binary (Bild-Bytes aus der Tabelle IMAGE) gilt zusätzlich: anonyme Clients erhalten die zugehörigen Media-Metadaten, aber nicht die Bilddaten selbst (403), da ein Bild den Personenbezug direkt enthalten kann.

Häufige Fehler

401 „Authorization: Bearer … erforderlich": Kein oder leerer Authorization-Header. Token holen, Header setzen.

401 „Ungültige Credentials": Client-Secret falsch. Notfalls neues Secret über die GUI generieren.

401 „invalid_token" im WWW-Authenticate: Token abgelaufen oder mit falschem Secret signiert. Neuen Token holen.

403 „Client ist deaktiviert": Spalte Aktiv steht auf Nein. In der GUI umstellen.

400 „Client … darf TypeID … nicht schreiben": ItemType nicht in der Erlaubte ItemTypes-Whitelist. Im Picker hinzufügen, dann sofort neu probieren.

400 „berechnetes Feld (kind=calc)": Wert wird vom Server berechnet (z. B. BMI). Schreiben Sie stattdessen die Quell-Items (Größe, Gewicht).

400 „subject.reference muss 'Patient/<id>' sein": Erst GET /Patient?identifier=… machen, die interne ID nehmen.

400 „Subject ist nicht in der studienspezifischen Kohorte": Studien-restricted Client versucht, einen Subject außerhalb seiner Studie zu beschreiben.

400 „Unit … passt nicht zur erwarteten Einheit …": Der Wert muss in der Stamm-Einheit des ItemTypes geliefert werden (siehe property/unit im CodeSystem). Keine automatische Konversion.

404 „Patient/X existiert nicht": Subject-ID falsch — Lookup wiederholen.

404 bei PUT auf Observation/X: Item-ID existiert nicht. Bei Bedarf mit POST neu anlegen (idempotent).

405 bei POST /Patient: Erwartet — Patient-Create ist bewusst gesperrt. Suche nutzen.

403 bei GET /Binary/X: Anonymer Client — Bild-Bytes sind gesperrt (Metadaten über Media bleiben abrufbar).

404 bei GET /Media/X oder /Binary/X: Bild existiert nicht, ist archiviert, liegt außerhalb des Studien-Scopes, oder die Bilddatei fehlt im Dateisystem.

503 „FHIR-Server ist ausgeschaltet": Master-Schalter in der GUI nicht gesetzt. Bis zu 30 Sekunden Cache.