Guide de l'API FHIR : interactions REST, recherche, Bundles et authentification pour une intégration FHIR R4
Une référence pratique de l'API FHIR pour les ingénieurs d'intégration : interactions RESTful avec exemples curl, recherche et pagination, Bundles transaction, écritures conditionnelles, erreurs OperationOutcome, SMART Backend Services et Subscriptions, vérifiée sur R4, R5 et le ballot R6.
Ala Ben Aicha

Réponse directe
Une API FHIR est une interface REST qui expose des ressources de santé typées : Patient, Encounter, Observation, etc. On lit et on écrit les ressources en HTTP classique sur [base]/[type]/[id], on interroge avec des paramètres de recherche, on regroupe les écritures dans un Bundle, et on découvre ce que le serveur prend réellement en charge dans son CapabilityStatement, à l'adresse GET [base]/metadata. La plupart des API FHIR en production tournent en FHIR R4 (4.0.1) et sont protégées par OAuth 2.0 via SMART on FHIR.
Ce qu'est FHIR, et ce que l'on entend par « API FHIR »
FHIR (Fast Healthcare Interoperability Resources) est le standard HL7 qui modélise les données de santé sous forme de ressources. Chaque type de ressource a une structure définie, des représentations JSON et XML et ses propres paramètres de recherche. L'API RESTful n'est qu'un des paradigmes d'échange de la spécification (les autres sont les documents, les messages et les services), mais c'est celui que la plupart des équipes désignent par « API FHIR ». Pour situer FHIR par rapport aux messages HL7 v2 et aux documents CDA, commencez par HL7 et FHIR : les différences.
Une intégration FHIR exige toujours un workflow clinique convenu, un guide d'implémentation, des identifiants stables et un modèle d'autorisation. Un serveur qui accepte un JSON valide n'est qu'un point de départ.
Choisir la version de FHIR avant d'écrire du code
| Release | Version | Publication | Statut | À retenir quand |
|---|---|---|---|---|
| R4 | 4.0.1 | 30 oct. 2019 | Premier contenu normatif, plus du contenu à l'essai | Choix par défaut en production et pour la plupart des guides nationaux |
| R4B | 4.3.0 | 28 mai 2022 | Modifications du seul contenu à l'essai ; contenu normatif R4 inchangé | Un guide d'implémentation précis repose dessus |
| R5 | 5.0.0 | 26 mars 2023 | Usage à l'essai (STU) | Vous avez besoin des Subscriptions par topic en natif ou d'un guide R5, et vos partenaires gèrent R5 |
| R6 | 6.0.0 (pas encore publiée) | Deuxième ballot complet en juillet 2026 | Projet | À suivre ; HL7 vise le statut normatif pour la plupart des ressources du socle |
Les dates proviennent du répertoire des versions FHIR. En R4, Patient, Observation, Bundle, CapabilityStatement et OperationOutcome sont normatives, tandis qu'Encounter et Questionnaire restent à l'essai : vérifiez la maturité de chaque ressource dont vous dépendez. Aux États-Unis, le critère de certification ONC §170.315(g)(10) impose FHIR R4.0.1 avec US Core et SMART App Launch 2.0.0, ce qui explique en partie pourquoi R4 reste le choix prudent. La page des versions FHIR détaille les relations entre R4, R4B et R5.
Découvrir le serveur : CapabilityStatement et configuration SMART
Tout serveur conforme publie un CapabilityStatement. Lisez-le avant de concevoir le client.
curl -s https://fhir.example.org/r4/metadata \
-H "Accept: application/fhir+json"
Vérifiez fhirVersion, puis, pour chaque ressource, les codes interaction pris en charge, les entrées searchParam, les supportedProfile et les indicateurs conditionalCreate et conditionalUpdate. Les opérations globales figurent sous operation. Les points d'accès OAuth sont annoncés à [base]/.well-known/smart-configuration. Mettez le résultat en cache, mais relisez-le après chaque montée de version de l'éditeur : un paramètre de recherche absent coûte moins cher à découvrir ici que dans les logs de production.
Les interactions RESTful
| Interaction | Requête HTTP | Remarques |
|---|---|---|
| read | GET [base]/Patient/demo-p001 |
Renvoie la version courante et un ETag |
| vread | GET [base]/Patient/demo-p001/_history/2 |
Une version précise |
| update | PUT [base]/Patient/demo-p001 |
Remplace la ressource entière ; envoyez If-Match pour éviter les mises à jour perdues |
| patch | PATCH [base]/Patient/demo-p001 |
JSON Patch, XML Patch ou FHIRPath Patch en R4 ; prise en charge variable |
| delete | DELETE [base]/Patient/demo-p001 |
Une lecture ultérieure renvoie 410 Gone sur les serveurs qui conservent l'historique |
| create | POST [base]/Patient |
Le serveur attribue l'id et renvoie Location |
| search | GET [base]/Patient?identifier=... |
Renvoie un Bundle searchset |
| history | GET [base]/Patient/demo-p001/_history |
Existe aussi au niveau du type et du système |
| capabilities | GET [base]/metadata |
Le CapabilityStatement |
| batch / transaction | POST [base] |
Le corps est un Bundle |
Lectures et historique en pratique :
BASE=https://fhir.example.org/r4
curl -s "$BASE/Patient/demo-p001" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer $TOKEN"
curl -s "$BASE/Patient/demo-p001/_history/2" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer $TOKEN"
curl -s "$BASE/Patient/demo-p001/_history" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer $TOKEN"
Écritures, avec contrôle de version et mise à jour partielle :
curl -s -X PUT "$BASE/Patient/demo-p001" \
-H "Content-Type: application/fhir+json" \
-H 'If-Match: W/"2"' \
-H "Prefer: return=representation" \
-H "Authorization: Bearer $TOKEN" \
-d @patient-demo-p001.json
curl -s -X PATCH "$BASE/Patient/demo-p001" \
-H "Content-Type: application/json-patch+json" \
-H "Authorization: Bearer $TOKEN" \
-d '[{"op":"replace","path":"/birthDate","value":"1990-01-02"}]'
Le PUT échoue en 412 si quelqu'un a modifié la ressource après la version 2 ; le PATCH modifie un seul élément sans renvoyer toute la ressource. Prefer: return=minimal supprime le corps de la réponse, ce qui compte sur les flux à fort volume.
Recherche : paramètres, _include, _revinclude et pagination
Les paramètres de recherche sont définis ressource par ressource. Les paramètres de type token prennent la forme system|code, les dates acceptent des préfixes comme ge et le, et des modificateurs comme :exact et :missing affinent la correspondance. Les trois requêtes ci-dessous renvoient les HbA1c d'un patient depuis janvier (les plus récentes d'abord, 50 par page), un patient retrouvé par identifiant métier avec toutes les Observation qui pointent vers lui, et les venues depuis le 1er octobre avec leurs patients. Laissez curl encoder les valeurs :
curl -s -G "$BASE/Observation" \
--data-urlencode "subject=Patient/demo-p001" \
--data-urlencode "code=http://loinc.org|4548-4" \
--data-urlencode "date=ge2026-01-01" \
--data-urlencode "_sort=-date" \
--data-urlencode "_count=50" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer $TOKEN"
curl -s -G "$BASE/Patient" \
--data-urlencode "identifier=https://example.org/demo-hospital/patients|DEMO-P001" \
--data-urlencode "_revinclude=Observation:subject" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer $TOKEN"
curl -s -G "$BASE/Encounter" \
--data-urlencode "date=ge2026-10-01" \
--data-urlencode "_include=Encounter:subject" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer $TOKEN"
_include ajoute les ressources vers lesquelles pointent les résultats ; _revinclude ajoute celles qui pointent vers les résultats. Les deux coûtent du temps serveur, et beaucoup de serveurs les plafonnent ou les refusent sur de gros volumes. Pour les requêtes qui ne doivent pas apparaître dans les journaux d'accès, utilisez POST [base]/Observation/_search avec un corps encodé en formulaire, comme le décrit la spécification de la recherche.
La réponse est un Bundle searchset :
{
"resourceType": "Bundle",
"type": "searchset",
"total": 132,
"link": [
{ "relation": "self", "url": "https://fhir.example.org/r4/Observation?subject=Patient/demo-p001&_count=50" },
{ "relation": "next", "url": "https://fhir.example.org/r4/Observation?page-token=opaque-value-from-server" }
],
"entry": [
{
"fullUrl": "https://fhir.example.org/r4/Observation/obs-001",
"resource": { "resourceType": "Observation", "id": "obs-001" },
"search": { "mode": "match" }
}
]
}
Quelques règles de pagination qui évitent de vrais bugs : suivez le lien next tel quel au lieu de reconstruire la requête ; attendez-vous à recevoir moins d'entrées que _count ; ne comptez pas sur la présence de total, coûteux à calculer (_total=accurate le demande explicitement) ; et distinguez les valeurs search.mode match et include avant de compter les résultats.
Les ressources Observation et Patient
Observation porte les résultats de biologie, les constantes, les scores et bien d'autres mesures. En R4, seuls status et code sont obligatoires, mais tout guide d'implémentation réel en exige davantage :
{
"resourceType": "Observation",
"status": "final",
"category": [{ "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory" }] }],
"code": { "coding": [{ "system": "http://loinc.org", "code": "4548-4", "display": "Hemoglobin A1c/Hemoglobin.total in Blood" }] },
"subject": { "reference": "Patient/demo-p001" },
"effectiveDateTime": "2026-10-02T08:30:00+02:00",
"valueQuantity": { "value": 6.1, "unit": "%", "system": "http://unitsofmeasure.org", "code": "%" }
}
La ressource Patient contient l'état civil et les identifiants. Deux notions à ne pas confondre : Patient.id est l'identifiant technique local attribué par le serveur, tandis que Patient.identifier contient les identifiants métier (IPP, identifiant national) avec un URI system par autorité d'attribution. Quand une admission HL7 v2 devient du FHIR, PID-3 alimente Patient.identifier et PV1-19 Encounter.identifier, dans des espaces de noms distincts. Le guide de mapping ADT détaille les choix propres à chaque événement, la référence des types de messages HL7 montre les segments source, et l'exemple synthétique ADT/FHIR proposé sur cette page fournit un petit fichier pour vos tests.
Bundles : searchset, transaction et batch
| Type de Bundle | Émis par | Comportement |
|---|---|---|
searchset |
Serveur | Résultats de recherche, avec liens de pagination |
transaction |
Client | Toutes les entrées réussissent, ou aucune |
batch |
Client | Chaque entrée est traitée indépendamment, sans référence entre entrées |
transaction-response / batch-response |
Serveur | Un statut par entrée |
history |
Serveur | Versions issues d'une interaction history |
document / message |
Les deux | Documents FHIR et messagerie FHIR |
collection |
Les deux | Simple ensemble de ressources |
Une petite transaction qui crée un Patient seulement s'il n'existe pas encore, puis crée ou met à jour sa venue (Encounter) par identifiant métier :
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:5b2f6c1e-0d6a-4f1e-9a51-2a1f0c0e7a01",
"resource": {
"resourceType": "Patient",
"identifier": [{ "system": "https://example.org/demo-hospital/patients", "value": "DEMO-P001" }],
"name": [{ "family": "DOE", "given": ["JANE"] }],
"birthDate": "1990-01-01"
},
"request": {
"method": "POST",
"url": "Patient",
"ifNoneExist": "identifier=https://example.org/demo-hospital/patients|DEMO-P001"
}
},
{
"fullUrl": "urn:uuid:9c1d7e2a-3b4f-4c5d-8e6f-7a8b9c0d1e02",
"resource": {
"resourceType": "Encounter",
"identifier": [{ "system": "https://example.org/demo-hospital/visits", "value": "DEMO-V001" }],
"status": "in-progress",
"class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "IMP" },
"subject": { "reference": "urn:uuid:5b2f6c1e-0d6a-4f1e-9a51-2a1f0c0e7a01" },
"period": { "start": "2026-10-05T10:00:00+01:00" }
},
"request": {
"method": "PUT",
"url": "Encounter?identifier=https://example.org/demo-hospital/visits|DEMO-V001"
}
}
]
}
Envoyez-la avec POST [base] et Content-Type: application/fhir+json. Le serveur traite d'abord les DELETE, puis les POST, puis les PUT/PATCH, puis les GET, et réécrit les références urn:uuid avec les id qu'il attribue. R4 ne précise pas tous les cas limites d'une référence vers une création conditionnelle qui a trouvé une ressource existante : testez ce chemin sur le serveur cible. Vérifiez dans le CapabilityStatement les interactions transaction et batch avant de compter sur l'une ou l'autre ; la page de la ressource Bundle donne les règles complètes.
Création et mise à jour conditionnelles pour des écritures idempotentes
Les moteurs d'interface rejouent les messages. Sans écriture conditionnelle, une admission rejouée crée un second Patient. La spécification HTTP propose deux outils, encore marqués « à l'essai » en R4 :
curl -s -X POST "$BASE/Patient" \
-H "Content-Type: application/fhir+json" \
-H "If-None-Exist: identifier=https://example.org/demo-hospital/patients|DEMO-P001" \
-H "Authorization: Bearer $TOKEN" \
-d @patient-demo-p001.json
| Correspondances | Création conditionnelle (If-None-Exist) |
Mise à jour conditionnelle (PUT [base]/Patient?identifier=...) |
|---|---|---|
| Aucune | Crée la ressource | Crée la ressource |
| Une | Renvoie 200 OK, ne crée rien |
Met à jour la ressource trouvée |
| Plusieurs | 412 Precondition Failed |
412 Precondition Failed |
Un 412 sur correspondances multiples signale un problème de qualité des données, en général des doublons de patients (identitovigilance). Envoyez-le vers une file de traitement ; ne le rejouez pas en boucle.
Erreurs, OperationOutcome et $validate
Une requête en échec renvoie un statut HTTP et, en général, un OperationOutcome :
| Statut | Signification habituelle |
|---|---|
| 400 | Contenu illisible ou structurellement invalide |
| 401 / 403 | Jeton absent, ou jeton sans le scope nécessaire |
| 404 / 410 | Ressource inconnue, ou supprimée |
| 412 | Conflit de version ou correspondance conditionnelle ambiguë |
| 422 | FHIR valide mais contraire à un profil ou à une règle métier |
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "required",
"diagnostics": "Observation.status: minimum required = 1, but only found 0",
"expression": ["Observation.status"]
}
]
}
Journalisez issue.code et issue.expression, pas seulement le statut HTTP. Avant l'envoi, validez par rapport aux profils du destinataire. Les serveurs qui prennent en charge $validate acceptent la ressource directement dans le corps de la requête (POST [base]/Observation/$validate) ; pour désigner un profil, envoyez plutôt une ressource Parameters, car la forme « corps brut » n'accepte aucun paramètre d'URL. En intégration continue, le HL7 FHIR Validator chargé avec les packages du guide d'implémentation détecte la plupart des erreurs de profil avant qu'un partenaire ne les voie.
Autorisation : SMART on FHIR et Backend Services
FHIR n'est pas un protocole de sécurité : une ressource valide ne donne aucun droit d'accès. La plupart des API FHIR s'appuient sur le cadre SMART App Launch (la version publiée actuelle est la 2.2.0). Deux flux couvrent l'essentiel des intégrations :
- Les applications utilisées par une personne emploient le flux authorization code avec PKCE, lancées depuis le DPI ou en mode autonome. Le guide de lancement d'applications SMART on FHIR le décrit pas à pas.
- Les intégrations de système à système utilisent SMART Backend Services : le grant OAuth client credentials, authentifié par un JWT signé (
private_key_jwt) plutôt que par un secret partagé.
curl -s -X POST https://auth.example.org/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=system/Patient.rs system/Observation.rs" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$SIGNED_JWT"
L'en-tête du JWT porte alg (RS384 ou ES384), kid et typ ; ses claims sont iss et sub (tous deux égaux à votre client_id), aud (l'URL du point d'accès token), exp (cinq minutes au plus dans le futur) et un jti unique. Enregistrez la clé publique sous forme de JWKS, de préférence par URL pour pouvoir faire tourner les clés. Les scopes SMART v2 comme system/Observation.rs remplacent la forme v1 system/Observation.read ; demandez au serveur quelle syntaxe il accepte.
Pour Epic, distinguez l'accès patient, le lancement SMART côté clinicien et les Backend Services, et confirmez les API prises en charge par l'établissement ainsi que les conditions d'accès en production ; le guide Epic pour les startups sépare ces parcours. Pour Oracle Health Millennium, appuyez-vous sur la documentation actuelle de la plateforme et sur les points d'accès de l'organisation cible. Le comportement du bac à sable ne garantit pas la disponibilité en production.
Subscriptions : R4, le backport R5 et R5
Il existe trois modèles, et les serveurs n'implémentent pas tous les mêmes :
- La Subscription R4 repose sur des critères : le client enregistre une expression de recherche et un canal (
rest-hook,websocket,email,smsoumessage). Elle est à l'essai et sa prise en charge est inégale. - L'IG Subscriptions R5 Backport (1.1.0, STU 1.1, janvier 2023, toujours la publication en vigueur) apporte les abonnements par topic à R4 et R4B. Le client s'abonne à un SubscriptionTopic via son URL canonique. Les notifications arrivent dans un Bundle
historydont la première entrée est une ressource Parameters portant le statut de l'abonnement, et la charge utile peut êtreempty,id-onlyoufull-resource. - Les Subscriptions R5 font de SubscriptionTopic, Subscription et SubscriptionStatus des ressources à part entière, avec un type de Bundle
subscription-notificationet des notifications de handshake et de heartbeat.
Quel que soit le modèle, traitez les notifications comme des indices. Gardez une recherche de rattrapage (par exemple sur _lastUpdated) pour les événements manqués ou désordonnés, et vérifiez la politique de nouvelles tentatives du serveur.
Questionnaire et QuestionnaireResponse
Questionnaire définit un formulaire : des items avec un linkId, un type, des options de réponse ou des jeux de valeurs, et des conditions enableWhen. QuestionnaireResponse stocke les réponses et renvoie au formulaire par son URL canonique questionnaire. On les recherche avec GET [base]/QuestionnaireResponse?subject=Patient/demo-p001&questionnaire=.... Pour le pré-remplissage, l'extraction vers des Observation et les formulaires adaptatifs, voyez Structured Data Capture (STU 4, construit sur R4).
Pour les extractions à l'échelle d'une population, Bulk Data ($export) produit des fichiers NDJSON de manière asynchrone au lieu de paginer des recherches.
Check-list d'intégration
- Convenez du guide d'implémentation, de la version FHIR et des systèmes d'identifiants avant d'écrire le mapping.
- Lisez le CapabilityStatement et
.well-known/smart-configuration; listez les interactions, paramètres de recherche et profils dont vous dépendez. - Utilisez la création ou la mise à jour conditionnelle sur les identifiants métier, et envoyez
If-Matchlors des mises à jour. - Suivez les liens de pagination tels quels et traitez à part les entrées
include. - Validez par rapport aux profils du destinataire en intégration continue, et journalisez le détail des OperationOutcome en production.
- Testez les doublons, mises à jour, suppressions, 412 et expirations de jeton, pas seulement le cas nominal.
- Choisissez le serveur en connaissance de cause ; le comparatif des serveurs FHIR aide à ce choix.
Si vous souhaitez un regard extérieur sur la conception d'une API FHIR ou d'une interface HL7 v2 vers FHIR, consultez le service d'intégration HL7 et FHIR ou parlons de votre projet.