FHIR RESTful API: how FHIR uses HTTP to exchange healthcare data

The FHIR RESTful API is a resource-oriented API over HTTPS that uses JSON as the default format for web and mobile applications. Every interaction — read, search, create, update, delete — maps to a standard HTTP method (GET, POST, PUT, DELETE) on instance-level, type-level, whole-system, or named operation endpoints prefixed with $.

This page is for web and mobile developers who already know REST but have never written FHIR code, and who want to understand why healthcare APIs are different and how to integrate them correctly into Vietnamese hospital systems under the current legal framework (Law 91/2025/QH15, Decree 356/2025/NĐ-CP, Decree 137/2024/NĐ-CP).

TL;DR

  • Five core interactions (read, search, create, update, delete) plus vread, history, patch, Bundle transaction, Operations, and Bulk Data cover every healthcare use case.
  • Resources are identified by the syntax [base]/[type]/[id]; type-level for search and create; whole-system for transactions and $export.
  • Standard FHIR R4 formats: application/fhir+json and application/fhir+xml are both normative; this page recommends JSON for web and mobile.
  • Security defaults: HTTPS is mandatory, authentication via SMART on FHIR (OAuth 2.0 + OpenID Connect), and AuditEvent logging for every access.
  • Vietnam's legal framework (Personal Data Protection Law 91/2025/QH15, Decree 356/2025/NĐ-CP, Decree 137/2024/NĐ-CP) imposes obligations to protect personal data and apply digital signatures; FHIR + SMART is a sensible way to implement them.

1. FHIR endpoint anatomy

FHIR is resource-oriented REST. Every URL is built from a service base URL combined with a resource type, a resource identifier, a version history segment, and a search query. The general syntax in §3.1 of FHIR R4 is:

[base]/[type]/[id]{/_history/[vid]}{?[search]}

[base] is the server's root URL (for example, https://hapi.fhir.org/baseR4); [type] is the correctly capitalized Resource name (Patient, Encounter, Observation, etc.); [id] is the logical identifier the server assigns; [vid] is the monotonically increasing version number after each update. There are four endpoint levels worth distinguishing:

  • Instance level (/Patient/123): used for read, vread, update, delete, and patch.
  • Type level (/Patient): used for search and create.
  • Whole-system level (/): used for batch/transaction Bundles and server-wide search.
  • Operation ($name): applies at all three levels above, for example /Patient/$match or /Patient/123/$everything.

A practical example against the public HAPI sandbox (note: dev/test only — never send real data):

curl -H "Accept: application/fhir+json" \
  https://hapi.fhir.org/baseR4/Patient/example

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?name=Smith&_count=5"

2. Five core interactions and their extended siblings

The FHIR R4 specification defines five mandatory interactions — read, search, create, update, delete — plus several common extended interactions. The table below maps the HTTP method, sample URL, and use case for each:

Interaction HTTP URL Use case
readGET/Patient/123Read a single Resource by id
vreadGET/Patient/123/_history/2Read a specific historical version
searchGET/Patient?name=NguyễnSearch by parameters
createPOST/PatientCreate new — server assigns the id
updatePUT/Patient/123Replace the entire Resource
patchPATCH/Patient/123Partial update (JSON Patch / FHIRPath Patch)
deleteDELETE/Patient/123Logical resource deletion
historyGET/Patient/123/_historyList the change history
capabilitiesGET/metadataServer capability discovery

A server may choose which interactions it supports; the CapabilityStatement (Section 5) lists exactly which ones for each Resource. In VN Core, we recommend at minimum read, search, create, and update for Patient, Encounter, Condition, Observation, and Coverage to ensure that electronic medical records (under Circular 13/2025/TT-BYT) interoperate with social health insurance (BHYT) records and the personal health record on VNeID, Vietnam's national digital identification app.

3. Search — querying clinical data

Search is FHIR's most complex interaction. Each Resource has a set of standard search parameters (such as name, identifier, birthdate, code, subject) along with several modifiers (:exact, :contains, :missing, :not, :above, :below) and prefixes for numbers and dates (eq, ne, gt, lt, ge, le).

When searching by an identifier with a known system, use the token syntax system|value. In VN Core, the system for the Vietnamese national ID number (CCCD) is http://fhir.hl7.org.vn/core/sid/cccd:

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?birthdate=ge1980-01-01&birthdate=lt1990-01-01"

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Observation?subject=Patient/example&code=http://loinc.org|2093-3"

Two useful reference modes are _include (pull in Resources referenced from the result) and _revinclude (pull in Resources that point back to the result). One important note: both are search parameters and apply only to type-level search, not to instance read:

# Get a Patient along with every Observation that references that Patient
curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?_id=example&_revinclude=Observation:subject"

# Or the other direction: get Observations and pull in the related Patient
curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Observation?subject=Patient/example&_include=Observation:subject"

Chained search lets you traverse a reference to filter the parent Resource. For example, find Observations belonging to a Patient whose name contains "Nguyễn":

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Observation?subject:Patient.name=Nguy%E1%BB%85n"

4. Bundle transaction — atomic multi-operation calls

Bundle is a special Resource that lets you package multiple operations in a single HTTP request sent to the whole-system endpoint. Two common modes: type=batch (each entry is independent — local failures only) and type=transaction (atomic — if any entry fails, the whole thing rolls back). Transactions also support cross-references via fullUrl with a urn:uuid: form and the ifNoneExist header for conditional create.

Here is an example you can run against the HAPI sandbox (save it as bundle.json and post with curl --data-binary @bundle.json):

POST / HTTP/1.1
Host: hapi.fhir.org
Content-Type: application/fhir+json
Accept: application/fhir+json

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:patient-001",
      "resource": {
        "resourceType": "Patient",
        "identifier": [{
          "system": "http://fhir.hl7.org.vn/core/sid/cccd",
          "value": "001234567890"
        }],
        "name": [{ "family": "Nguyễn", "given": ["Văn An"] }],
        "gender": "male",
        "birthDate": "1985-04-12"
      },
      "request": {
        "method": "POST",
        "url": "Patient",
        "ifNoneExist": "identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"
      }
    },
    {
      "resource": {
        "resourceType": "Encounter",
        "status": "in-progress",
        "class": {
          "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
          "code": "AMB",
          "display": "Ambulatory"
        },
        "subject": { "reference": "urn:uuid:patient-001" }
      },
      "request": { "method": "POST", "url": "Encounter" }
    }
  ]
}

The server returns a Bundle of type=transaction-response, where each entry carries a response.status (for example, 201 Created) and a response.location pointing to the newly created Resource. This is an elegant way to push a complete encounter (Patient + Encounter + Condition + Observation) from a hospital information system (HIS) to a FHIR server.

5. CapabilityStatement — server discovery

Every FHIR server must respond at GET [base]/metadata with a CapabilityStatement that describes the FHIR version (4.0.1 for R4), the supported Resources, the list of interactions for each Resource, the implemented search parameters, the available Operations, and the authentication mechanisms. This is a machine-readable document that lets a client know "what this server can do" before calling further.

curl -H "Accept: application/fhir+json" \
  https://hapi.fhir.org/baseR4/metadata | jq '.fhirVersion, .rest[0].resource[].type'

A minimal CapabilityStatement excerpt for VN Core:

{
  "resourceType": "CapabilityStatement",
  "status": "active",
  "date": "2026-05-02",
  "kind": "instance",
  "fhirVersion": "4.0.1",
  "format": ["application/fhir+json", "application/fhir+xml"],
  "rest": [{
    "mode": "server",
    "security": {
      "service": [{
        "coding": [{
          "system": "http://terminology.hl7.org/CodeSystem/restful-security-service",
          "code": "SMART-on-FHIR"
        }]
      }]
    },
    "resource": [{
      "type": "Patient",
      "interaction": [
        { "code": "read" },
        { "code": "search-type" },
        { "code": "create" },
        { "code": "update" }
      ],
      "searchParam": [
        { "name": "identifier", "type": "token" },
        { "name": "name", "type": "string" },
        { "name": "birthdate", "type": "date" }
      ]
    }]
  }]
}

When deploying to a hospital, publish a separate CapabilityStatement for production and staging, clearly stating which VN Core profiles are enforced and which SMART on FHIR scopes are supported. This also makes audits and assessments under Personal Data Protection Law 91/2025/QH15 easier.

6. Operations: $expand, $validate, $everything

Operations are pure RPCs standardized by FHIR, invoked with POST (or GET if read-only) and a name starting with $. A few common Operations:

  • $validate — check whether a Resource is valid against a profile before posting it for real.
  • $expand — expand a ValueSet into a flat list of codes (useful for UI dropdowns).
  • $lookup — look up a code in a CodeSystem to get its display, designations, and properties.
  • $everything — call /Patient/123/$everything to retrieve every Resource related to a patient — handy for summary records.
  • $translate — map codes through a ConceptMap (for example, ICD-10 VN to SNOMED CT VN per Decision 2427/QĐ-BYT, batch 1).

Validating before commit is an important habit for Vietnamese systems, where many VN Core profiles place Must Support constraints on CCCD, ethnicity, and ward/commune:

POST /Patient/$validate?profile=http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient HTTP/1.1
Host: hapi.fhir.org
Content-Type: application/fhir+json

{
  "resourceType": "Patient",
  "identifier": [{
    "system": "http://fhir.hl7.org.vn/core/sid/cccd",
    "value": "001234567890"
  }],
  "name": [{ "family": "Trần", "given": ["Thị Bình"] }],
  "gender": "female",
  "birthDate": "1990-08-21"
}

7. Bulk Data API and asynchronous $export

When you need to export large volumes of data (research cohorts, training data for medical AI, BHYT reconciliation), $export from the Bulk Data Access IG is the standard approach. The asynchronous workflow has three steps:

  1. The client sends the request with the Prefer: respond-async header; the server replies with 202 Accepted and a Content-Location header pointing at a polling URL.
  2. The client polls that URL; when ready, the server returns a JSON manifest listing the NDJSON files for each Resource type.
  3. The client downloads each NDJSON file — every line is a standalone Resource.

A sample request for a Group-level export (the 2026 pilot cohort):

GET /Group/vn-pilot-cohort-2026/$export?_type=Patient,Observation,Condition&_since=2026-01-01T00:00:00Z HTTP/1.1
Host: hapi.fhir.org
Accept: application/fhir+json
Prefer: respond-async
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR...

Legal note

Health data is sensitive personal data under Article 3 of Decree 356/2025/NĐ-CP. Every $export must have a clear legal basis (data subject consent or another lawful ground under Law 91/2025/QH15), an up-to-date DPIA, and complete AuditEvent logging; for cross-border exports, the assessment dossier per Form 09 of Decree 356/2025/NĐ-CP is mandatory.

8. Authentication: SMART on FHIR + OAuth 2.0

SMART on FHIR is an OAuth 2.0 and OpenID Connect profile designed specifically for FHIR. The spec defines three launch models:

  • EHR launch — the app is opened by the EMR within the context of the currently logged-in patient/clinician.
  • Standalone launch — the app (for example, a patient portal) launches independently and the user signs in directly.
  • Backend services — machine-to-machine, using a JWT client assertion instead of a user.

Scopes follow the syntax <context>/<Resource>.<permission>, for example:

patient/Patient.read       # read the current patient's record
patient/Observation.rs     # read + search Observation
user/Encounter.cruds       # full permissions on Encounter
system/*.read              # backend reads everything

The standard PKCE flow: the client calls /authorize, the user signs in and consents to scopes, the server returns an authorization code, and the client exchanges the code for an access token at /token:

POST /oauth2/token HTTP/1.1
Host: auth.example-ehr.vn
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=Aa1Bb2Cc3...&
redirect_uri=https%3A%2F%2Fapp.omihealth.vn%2Fcallback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&
client_id=app-omihealth-vn

The returned token is typically a short-lived JWT plus a refresh token. Every FHIR request must include the header Authorization: Bearer <token> over an HTTPS connection. The SMART discovery configuration lives at .well-known/smart-configuration.

9. Status codes and OperationOutcome

FHIR R4 specifies HTTP status code usage fairly tightly:

  • 200 OK — read/search/update succeeded.
  • 201 Created — create succeeded, with Location and ETag headers.
  • 204 No Content — delete succeeded with no response body.
  • 304 Not Modified — response to a conditional read when the data has not changed.
  • 400 Bad Request — Resource is syntactically invalid.
  • 401 Unauthorized / 403 Forbidden — missing or insufficient access token.
  • 404 Not Found — no matching Resource.
  • 409 Conflict — version conflict (used with If-Match).
  • 410 Gone — the Resource has been logically deleted.
  • 412 Precondition Failed — a conditional header did not match.
  • 422 Unprocessable Entity — Resource is syntactically valid but fails an invariant or profile check.
  • 5xx — server-side error.

FHIR recommends (SHOULD) returning an OperationOutcome in the body for FHIR-level errors (4xx/5xx tied to Resource semantics). Clients must, however, accept the case where the body is empty or non-FHIR at the infrastructure layer (for example, a proxy that returns HTML). A typical OperationOutcome:

{
  "resourceType": "OperationOutcome",
  "issue": [{
    "severity": "error",
    "code": "invariant",
    "details": {
      "text": "Patient.identifier requires the CCCD system per the VNCorePatient profile"
    },
    "diagnostics": "Identifier with system=http://fhir.hl7.org.vn/core/sid/cccd is required.",
    "expression": ["Patient.identifier"]
  }]
}

In VN Core, invariants enforcing CCCD on the VNCorePatient profile return 422 with issue.code = "invariant" and expression pointing at exactly the offending element — convenient for the UI to surface the error to the data entry user.

10. Versioning, conditional requests, paging

FHIR is especially strong in three areas that homegrown APIs often handle sloppily:

  • Optimistic concurrency — every update must include the If-Match: W/"2" header to avoid lost-update conflicts; the server returns 409 if the version has changed.
  • Conditional create / update / delete — use the If-None-Exist header or search parameters in the URL to mean "create only if it does not exist" — ideal for ingesting XML 4210 (Vietnam's BHXH data exchange format) into FHIR.
  • Paging — search returns a Bundle of type=searchset with link.relation = "next"; clients only follow the link instead of constructing the next-page URL themselves. The _count parameter suggests a page size (default recommended: 20).
curl -X PUT \
  -H "Content-Type: application/fhir+json" \
  -H "If-Match: W/\"2\"" \
  --data-binary @patient-v3.json \
  https://hapi.fhir.org/baseR4/Patient/123

curl -X POST \
  -H "Content-Type: application/fhir+json" \
  -H "If-None-Exist: identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890" \
  --data-binary @patient-new.json \
  https://hapi.fhir.org/baseR4/Patient

11. Best practices for the Vietnamese context

FHIR is a technically neutral standard, but real-world deployments must align with the legal framework. Separate legal obligations from the technical choices that fulfill them:

  • Personal data protection: Law 91/2025/QH15 and Decree 356/2025/NĐ-CP impose obligations around security, audit logging, DPIA, DPO, and breach notification within 72 hours. Implement them via mandatory HTTPS on every endpoint, encryption at rest, fine-grained authorization through SMART scopes, AuditEvent logging for each request, and a Resource-level Consent workflow.
  • Electronic transactions and digital signatures: Decree 137/2024/NĐ-CP requires digital signatures on electronic records. In FHIR, use the Provenance Resource with a signature to attach signatures to every important record (e-prescription, lab result, transfer slip).
  • Electronic medical records: Circular 13/2025/TT-BYT sets a 31/12/2026 deadline for healthcare facilities. FHIR REST + VN Core profiles is a sensible way to meet the requirement that records interoperate with VNeID.
  • Performance and reliability: default paging of 20, rate limit of 100 req/min for client apps (looser for backends), cache the CapabilityStatement and $expand results, and use ETags on every update.
  • Testing: use the public HAPI sandbox for dev/test and never send real data; run $validate in CI/CD before merging a new profile.

Recommendation from OmiGroup

When integrating FHIR into a Vietnamese HIS, separate the FHIR Façade layer (HTTPS + SMART + AuditEvent) from the business logic behind it. The façade carries the legal compliance; the business layer focuses on clinical and BHYT rules. For detailed Q&A, contact Phan Mạnh Hùng (HungPM, [email protected]).

References