FHIR RESTful API: how FHIR uses HTTP to exchange healthcare data
The FHIR RESTful API is a resource-oriented interaction framework over HTTP; production endpoints carrying health data should be protected with TLS under the deployment security policy. JSON and XML are both standard formats selected through media-type negotiation.
Interactions declared by a server — such as read, search, create, update, or delete — map to HTTP methods and 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
- FHIR defines read, search, create, update, delete, vread, history, patch, Bundle transaction, and Operations; each server supports only the set declared in its
CapabilityStatement. - Resources use
[base]/[type]/[id]; search/create are type-level and transactions are whole-system. Bulk Data defines system-, Patient-, and Group-level$exportscopes. - Standard FHIR R4 formats:
application/fhir+jsonandapplication/fhir+xmlare both normative; this page recommends JSON for web and mobile. - FHIR R4 does not impose one security scheme by default. Deployments must select appropriate TLS, authentication, authorization, and audit controls; SMART on FHIR is a common standardized option where the use case fits.
- Vietnam's legal framework imposes personal-data and electronic-transaction duties; FHIR, SMART,
AuditEvent, andProvenancecan support technical controls but do not establish legal compliance by themselves.
On this page
- FHIR endpoint anatomy
- Five core interactions and their extended siblings
- Search — querying clinical data
- Bundle transaction — atomic multi-operation calls
- CapabilityStatement — server discovery
- Operations: $expand, $validate, $everything…
- Bulk Data API and asynchronous $export
- Authentication: SMART on FHIR + OAuth 2.0
- Status codes and OperationOutcome
- Versioning, conditional requests, paging
- Deployment practices for the Vietnamese context
- Further reading
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 an opaque server-assigned version identifier and must not be assumed to increase numerically. 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/$matchor/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
FHIR R4 defines interactions such as read, search, create, update, and delete, but it does not require every server to support all five for every Resource type. A server advertises its actual interaction set in its CapabilityStatement; the table below maps common interactions to HTTP methods, sample URLs, and use cases:
| Interaction | HTTP | URL | Use case |
|---|---|---|---|
| read | GET | /Patient/123 | Read a single Resource by id |
| vread | GET | /Patient/123/_history/2 | Read a specific historical version |
| search | GET | /Patient?name=Nguyễn | Search by parameters |
| create | POST | /Patient | Create new — server assigns the id |
| update | PUT | /Patient/123 | Replace the entire Resource |
| patch | PATCH | /Patient/123 | Partial update (JSON Patch / FHIRPath Patch) |
| delete | DELETE | /Patient/123 | Logical resource deletion |
| history | GET | /Patient/123/_history | List the change history |
| capabilities | GET | /metadata | Server capability discovery |
A server may choose which interactions it supports; the CapabilityStatement (Section 5) lists them per Resource. The minimum set must come from the actor- and workflow-specific VN Core CapabilityStatement; do not infer one common list for every EMR, BHYT gateway, or citizen application.
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"
A search with no matches still succeeds with HTTP 200 and a Bundle of
type=searchset with no matching entries (commonly total = 0).
HTTP 404 applies to cases such as reading an instance that does not exist, not to
a zero-result search.
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…
A FHIR Operation has a name beginning with $ and may be defined by the base specification, an IG, or an endpoint. Whether it uses GET or POST, its parameters, and its supported scope follow the applicable OperationDefinition and CapabilityStatement. Common examples include:
$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/$everythingto request information related to a patient within the server's implemented scope; do not assume it is the complete legal record or data held by every system.$translate— map codes through a published ConceptMap with explicit direction, version, and equivalence.
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 is a standardized asynchronous option when the endpoint advertises the applicable Bulk Data Access IG version, authorization scopes, and supported parameters. The workflow has three steps:
- The client sends the request with the
Prefer: respond-asyncheader; the server replies with202 Acceptedand aContent-Locationheader pointing at a polling URL. - The client polls that URL; when ready, the server returns a JSON manifest listing the NDJSON files for each Resource type.
- The client downloads each NDJSON file — every line is a standalone Resource.
The Bulk Data IG defines three export initiation scopes:
- System-level:
GET [base]/$export. - Patient-level:
GET [base]/Patient/$exportfor the patient set the client is authorized to access. - Group-level:
GET [base]/Group/[id]/$exportfor Group members.
An illustrative Group-level export request:
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. Before each $export flow,
identify the processing roles, purpose, data scope, recipients, and lawful basis.
Determine whether an impact-assessment record must be prepared or updated,
whether Form 09 applies to an actual cross-border transfer, and which AuditEvent
records are required from the applicable conditions, data flow, threat model, and
audit policy; do not assume every export has the same dossier or audit coverage.
8. Authentication: SMART on FHIR + OAuth 2.0
SMART App Launch is an OAuth 2.0 and OpenID Connect profile for FHIR applications, with two principal launch contexts:
- 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 is a separate machine-to-machine authorization profile in the SMART ecosystem, commonly using a JWT client assertion and system/... scopes; it is not a user-context launch mode.
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
In an authorization-code flow with PKCE, the client calls /authorize, the user authenticates and authorizes the presented scopes, the server returns an authorization code, and the client exchanges it for an access token at /token. This OAuth grant does not itself replace a legal consent decision or a FHIR Consent resource:
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
An access token may be a JWT or an opaque string; lifetime and refresh-token issuance depend on the authorization server and client type. Requests to SMART/OAuth-protected endpoints use Authorization: Bearer <token> over TLS. Clients should read discovery metadata at .well-known/smart-configuration rather than assume token 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
LocationandETagheaders. - 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 — an authentication or authorization failure under the security contract; the cause is not necessarily limited to an access token.
- 404 Not Found — the requested instance does not exist; a zero-match search still returns an empty Bundle with 200.
- 409 Conflict — a state conflict defined by the interaction or API contract; it does not replace the response for a failed
If-Match. - 410 Gone — the Resource has been logically deleted.
- 412 Precondition Failed — a condition did not match, including failed
If-Matchin a version-aware update. - 422 Unprocessable Entity — some servers use it for profile or business-rule failures; clients must not assume every invariant failure uses this status.
- 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"]
}]
}
An invariant in VNCorePatient defines validity; it does not prescribe one
HTTP status. Depending on the interaction and API contract, a server may use 400 or
422 and should return a sufficiently detailed OperationOutcome when appropriate.
A client must not hard-code that every violation has issue.code = "invariant"
or a particular expression value.
10. Versioning, conditional requests, paging
FHIR standardizes three mechanisms that API contracts should state explicitly:
- Optimistic concurrency — when a server declares
versioned-update, the client sendsIf-Match: W/"2"; a version mismatch is handled with HTTP 412 under the specification. - Conditional create / update / delete — when the server declares support, use
If-None-Existor search conditions to prevent duplicates during ingestion. - Paging — search returns a
searchsetBundle with server-provided links; clients followlink.relation = "next"rather than constructing the next URL._countis a requested page size that the server may adjust or ignore.
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. Deployment 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: determine obligations from the processing role, purpose, and risk; use TLS, encryption at rest, fine-grained authorization, and audit logging under an approved policy. SMART scopes,
AuditEvent, andConsentsupport those controls but do not replace a DPIA, access governance, or a lawful basis. - Electronic transactions and digital signatures: where a legal instrument or workflow requires an electronic signature, select the signing scope and verification mechanism accordingly. FHIR can carry signatures through
Provenance.signatureorBundle.signature; legal validity also depends on the certificate, signing policy, and the canonical bytes actually signed. - Electronic medical records: Circular 13/2025/TT-BYT sets different milestones for hospitals and other healthcare facilities. FHIR REST + VN Core is one technical option for interoperability pilots; the Circular does not designate VN Core as the only mechanism.
- Performance and reliability: derive page size, rate limits, and cache policy from load tests and actor-specific SLOs. Publish those limits in the API contract and use ETags according to the server's declared versioning policy.
- Testing: use the public HAPI sandbox for dev/test and never send real data; run
$validatein CI/CD before merging a new profile.
Recommendation from Omi HealthTech
A Vietnamese HIS may separate a FHIR façade from the business layer to manage the API contract and access controls consistently. Compliance remains an end-to-end responsibility across architecture, data, people, and operations; it does not reside in the façade alone. For detailed Q&A, contact [email protected].
References
- FHIR R4 RESTful API §3.1 — hl7.org/fhir/R4/http.html
- FHIR Search — hl7.org/fhir/R4/search.html
- FHIR Bundle — hl7.org/fhir/R4/bundle.html
- FHIR CapabilityStatement — hl7.org/fhir/R4/capabilitystatement.html
- FHIR OperationOutcome — hl7.org/fhir/R4/operationoutcome.html
- SMART App Launch IG — hl7.org/fhir/smart-app-launch
- Bulk Data Access IG — hl7.org/fhir/uv/bulkdata
- HAPI FHIR public sandbox — hapi.fhir.org
- Law 91/2025/QH15 — Personal Data Protection (see legal-corpus, code
L-91-2025) - Decree 356/2025/NĐ-CP — Implementing the Personal Data Protection Law (see legal-corpus, code
ND-356-2025) - Decree 137/2024/NĐ-CP — Electronic Transactions (see legal-corpus, code
ND-137-2024) - Circular 13/2025/TT-BYT — Electronic Medical Records (see legal-corpus, code
TT-13-2025)