Intégration des systèmes de santéMis à jour -11 minutes de lecture

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

Guide de l'API FHIR : interactions REST, recherche, Bundles et authentification pour une intégration FHIR R4

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, sms ou message). 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 history dont la première entrée est une ressource Parameters portant le statut de l'abonnement, et la charge utile peut être empty, id-only ou full-resource.
  • Les Subscriptions R5 font de SubscriptionTopic, Subscription et SubscriptionStatus des ressources à part entière, avec un type de Bundle subscription-notification et 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

  1. Convenez du guide d'implémentation, de la version FHIR et des systèmes d'identifiants avant d'écrire le mapping.
  2. Lisez le CapabilityStatement et .well-known/smart-configuration ; listez les interactions, paramètres de recherche et profils dont vous dépendez.
  3. Utilisez la création ou la mise à jour conditionnelle sur les identifiants métier, et envoyez If-Match lors des mises à jour.
  4. Suivez les liens de pagination tels quels et traitez à part les entrées include.
  5. Validez par rapport aux profils du destinataire en intégration continue, et journalisez le détail des OperationOutcome en production.
  6. Testez les doublons, mises à jour, suppressions, 412 et expirations de jeton, pas seulement le cas nominal.
  7. 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.

FHIRAPI FHIRHL7API RESTSMART on FHIRIntégrationInteropérabilité

Lectures et services associés

Poursuivons la conversation

Vous avez des questions sur ce sujet ? J'aimerais avoir de vos nouvelles.