Overview
FHIR (Fast Healthcare Interoperability Resources) R4 is a REST-based standard for exchanging health data as JSON or XML resources, and this guide shows you how to read, search, create, update, and delete Patient and Observation resources with plain HTTP requests.
By the end you will be able to:
- Retrieve a patient by ID and search by name, birth date, and gender.
- Create a patient and record a heart-rate Observation using standard LOINC and UCUM codes.
- Read an OperationOutcome to diagnose a failed request.
This guide is written for developers who know HTTP and JSON. No healthcare background is required. The examples use curl against a public test server, and the same requests work against any FHIR R4 server, such as a vendor sandbox, once you add that server's authorization header.
Before you begin
You need curl and an internet connection. No account or API key is required for the public test server.
- Tool: curl 7.x or later. Check with
curl --version. Postman works too; the requests translate directly. - Test server: the public HAPI FHIR R4 server at
https://hapi.fhir.org/baseR4. - Data rule: the test server is open to everyone, and its contents can change or be removed at any time. Never submit real patient information. Use fictional names only.
Set the base URL once so the examples stay short.
export FHIR_BASE=https://hapi.fhir.org/baseR4
On Windows PowerShell, use $env:FHIR_BASE = "https://hapi.fhir.org/baseR4" and run curl.exe instead of curl.
Confirm the server is reachable and speaks R4 by requesting its capability statement:
curl -s -H "Accept: application/fhir+json" "$FHIR_BASE/metadata" | grep -o '"fhirVersion" *: *"[^"]*"'
A working connection prints a line like "fhirVersion": "4.0.1". FHIR R4 is version 4.0.1.
Core concepts
Every FHIR request maps one of five REST interactions onto a resource type such as Patient or Observation.
| Interaction | Request | Success status |
|---|---|---|
| Read | GET [base]/Patient/{id} |
200 OK |
| Search | GET [base]/Patient?{parameters} |
200 OK, returns a Bundle |
| Create | POST [base]/Patient |
201 Created |
| Update | PUT [base]/Patient/{id} |
200 OK |
| Delete | DELETE [base]/Patient/{id} |
200 OK or 204 No Content |
Four terms appear throughout this guide:
- Resource: a JSON object whose
resourceTypefield names its kind, such asPatient. Every stored resource has a server-assignedid. - Bundle: a container resource. Search results come back as a Bundle of type
searchset, with oneentryper match. - Reference: a pointer from one resource to another, written as
{"reference": "Patient/4523"}. - Coding: a code plus the code system that defines it, for example LOINC code
8867-4for heart rate.
Send Accept: application/fhir+json on every request so the server returns JSON. On requests that carry a body (create and update), also send Content-Type: application/fhir+json.
In FHIR JSON, elements that can repeat, such as name and given, are always arrays, even when they hold one value.
Task 1: Read a Patient by ID
A read request returns one resource when you know its ID. To find a valid ID on the shared test server, ask for one patient with only the id element, then copy the id from the first entry.
curl -s -H "Accept: application/fhir+json" "$FHIR_BASE/Patient?_count=1&_elements=id"
Then read that patient. Replace 4523 with your ID.
curl -s -H "Accept: application/fhir+json" "$FHIR_BASE/Patient/4523"
The response is the Patient resource (abbreviated here to the most common elements). The server also adds meta.source and a generated text narrative, which are left out here. IDs and timestamps are examples; yours will differ. The behavior in this guide was checked against the HAPI test server on October 23, 2026.
{
"resourceType": "Patient",
"id": "4523",
"meta": {
"versionId": "1",
"lastUpdated": "2026-10-22T12:30:00.000+00:00"
},
"name": [
{ "family": "Quickstart", "given": ["Alex"] }
],
"gender": "other",
"birthDate": "1990-06-15"
}
meta.versionIdincreases each time the resource is updated, which lets you detect concurrent edits.nameis an array of HumanName objects, and each holds afamilystring and agivenarray.birthDateuses the formatYYYY-MM-DD. Servers may omit any element that has no value.
If the ID does not exist, the server returns status 404 and an OperationOutcome describing the problem. See the troubleshooting section for how to read it.
Task 2: Search for patients
A search request filters a resource type with query parameters and returns the matches in a Bundle. This example finds up to five female patients with the family name Smith. Quote the URL so your shell does not treat & as a command separator.
curl -s -H "Accept: application/fhir+json" \
"$FHIR_BASE/Patient?family=Smith&gender=female&_count=5"
The response is a Bundle (abbreviated):
{
"resourceType": "Bundle",
"type": "searchset",
"link": [
{ "relation": "self", "url": "https://hapi.fhir.org/baseR4/Patient?family=Smith&gender=female&_count=5" },
{ "relation": "next", "url": "https://hapi.fhir.org/baseR4?_getpages=...&_getpagesoffset=5&_count=5" }
],
"entry": [
{
"fullUrl": "https://hapi.fhir.org/baseR4/Patient/4523",
"resource": { "resourceType": "Patient", "id": "4523" },
"search": { "mode": "match" }
}
]
}
Each match is in entry[].resource. A search with no matches still returns status 200 with a Bundle that has no entry array, so check for that rather than expecting an error.
Common Patient search parameters
| Parameter | Example | Matches |
|---|---|---|
family |
family=Smith |
Family names that start with the value, ignoring case |
given |
given=Alex |
Given names that start with the value |
name |
name=smith |
Any part of any name |
gender |
gender=female |
Exact code: male, female, other, or unknown |
birthdate |
birthdate=ge1980-01-01 |
Dates, with prefixes eq, ne, gt, lt, ge, le |
Repeat a parameter to set a range, for example birthdate=ge1980-01-01&birthdate=lt1990-01-01. To search by a business identifier, pass the code system and value separated by a vertical bar, such as identifier=http://hospital.example.org/mrn%7C12345. The %7C is a URL-encoded vertical bar.
Paging and trimming results
- Page size:
_count=5asks for five results per page. Servers may enforce their own maximum. - Next page: follow the
urlof thelinkwhoserelationisnext. Stop when there is nonextlink. Do not build page URLs yourself. - Smaller responses:
_summary=truereturns only each resource's summary elements, and_elements=id,namereturns only the elements you list, plusresourceTypeandmeta. - Order:
_sort=familysorts ascending, and_sort=-birthdatesorts descending.
Task 3: Create a Patient
A create request sends a new resource in the request body, and the server assigns its ID. Save this fictional patient as patient.json. Do not include an id.
{
"resourceType": "Patient",
"name": [
{ "use": "official", "family": "Quickstart", "given": ["Alex"] }
],
"gender": "other",
"birthDate": "1990-06-15"
}
Send it with POST. The -i flag prints the response headers, and Prefer: return=representation asks the server to return the stored resource in the response body.
curl -s -i -X POST "$FHIR_BASE/Patient" \
-H "Content-Type: application/fhir+json" \
-H "Accept: application/fhir+json" \
-H "Prefer: return=representation" \
-d @patient.json
A successful create returns status 201 Created and a Location header with the new resource's URL:
HTTP/1.1 201 Created
Location: https://hapi.fhir.org/baseR4/Patient/4523/_history/1
The number after Patient/ is the new ID. Note it, because the next tasks use it. The body contains your resource plus the server-generated id and meta elements, plus a generated text narrative.
Two gotchas cause most create failures. A missing Content-Type header returns status 400 on the HAPI test server (the FHIR specification also allows 415). A body whose resourceType does not match the URL, such as an Observation posted to /Patient, returns status 400 on the HAPI test server (other servers may use 422).
Task 4: Record and retrieve a heart-rate Observation
An Observation stores a single measurement, such as a vital sign, and points to the patient it belongs to. This one records a heart rate of 72 beats per minute. Save it as observation.json, replacing 4523 with the ID from Task 3.
{
"resourceType": "Observation",
"status": "final",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs",
"display": "Vital Signs"
}
]
}
],
"code": {
"coding": [
{ "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" }
]
},
"subject": { "reference": "Patient/4523" },
"effectiveDateTime": "2026-10-22T08:30:00-04:00",
"valueQuantity": {
"value": 72,
"unit": "beats/minute",
"system": "http://unitsofmeasure.org",
"code": "/min"
}
}
| Element | Purpose |
|---|---|
status |
Required. Where the result stands. final means complete and verified. |
code |
Required. What was measured, here a LOINC code. LOINC defines the codes for lab tests and clinical measurements. |
subject |
A reference to the patient the measurement belongs to. |
effectiveDateTime |
When the measurement was taken, with a time zone offset. |
valueQuantity |
The result: a number plus a unit. The system and code use UCUM, the standard unit code system, and /min means per minute. |
Post it the same way as the patient:
curl -s -i -X POST "$FHIR_BASE/Observation" \
-H "Content-Type: application/fhir+json" \
-H "Accept: application/fhir+json" \
-d @observation.json
A 201 Created response means the Observation is stored. To retrieve the patient's heart-rate readings, newest first, search by subject and code. The %7C separates the code system from the code.
curl -s -H "Accept: application/fhir+json" \
"$FHIR_BASE/Observation?subject=Patient/4523&code=http://loinc.org%7C8867-4&_sort=-date"
The result is a searchset Bundle, like the patient search in Task 2. Each entry[].resource is an Observation. If the Bundle is empty, check that subject uses the exact ID from Task 3.
Task 5: Update and delete a resource
An update replaces the entire stored resource, so the request body must contain every element you want to keep, plus the id. Any element you leave out is removed. This version of patient.json adds an email address:
{
"resourceType": "Patient",
"id": "4523",
"name": [
{ "use": "official", "family": "Quickstart", "given": ["Alex"] }
],
"gender": "other",
"birthDate": "1990-06-15",
"telecom": [
{ "system": "email", "value": "alex.quickstart@example.com" }
]
}
curl -s -X PUT "$FHIR_BASE/Patient/4523" \
-H "Content-Type: application/fhir+json" \
-H "Accept: application/fhir+json" \
-d @patient.json
A successful update returns status 200, and meta.versionId increases to 2. The id in the body must match the id in the URL, or the server rejects the request with status 400. To list every stored version, request GET $FHIR_BASE/Patient/4523/_history.
To prevent overwriting someone else's change, add the header If-Match: W/"1" with the version you last read. If the stored version is different, the server refuses the update, with status 412 Precondition Failed under the FHIR specification, although the HAPI test server returned 409 Conflict.
Delete
Remove the Observation first and then the patient, because servers can refuse to delete a resource that other resources still reference, usually with status 409.
curl -s -i -X DELETE "$FHIR_BASE/Observation/{observation-id}"
curl -s -i -X DELETE "$FHIR_BASE/Patient/4523"
A successful delete returns status 200 or 204, depending on the server. Reading a deleted resource afterward typically returns status 410 Gone. Because the test server is shared, delete the test data you create when you finish.
Errors and troubleshooting
When a request fails, a FHIR server returns an HTTP error status and, usually, an OperationOutcome resource that explains what went wrong. Read issue[].diagnostics first. Exact wording varies by server.
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "processing",
"diagnostics": "HAPI-2001: Resource Patient/does-not-exist is not known"
}
]
}
| Status | Usual cause | What to check |
|---|---|---|
| 400 | Malformed JSON, an unknown search parameter, or an ID mismatch on update | Validate the JSON and check parameter spelling |
| 401 | Missing or expired credentials | Add the Authorization header (not needed on the public test server) |
| 403 | Valid credentials without permission | Check the scopes or roles granted to your client |
| 404 | Unknown resource type, ID, or base URL | Check the path and the ID |
| 409 | Version conflict or a resource that is still referenced | Re-read the resource, or delete the referencing resources first |
| 410 | The resource was deleted | Search for it again or create a new one |
| 412 | The If-Match version does not match |
Re-read the resource and retry with the new version |
| 415 | Unsupported media type | Send Content-Type: application/fhir+json |
| 422 | The body is valid JSON but fails server validation | Read each issue[].diagnostics for the failing element |
Common problems
- You get HTML instead of JSON. Add the
Accept: application/fhir+jsonheader, or append_format=jsonto the URL. - curl reports a bad URL, or only part of your search runs. The URL has an unquoted
&. Wrap the whole URL in double quotes. - A search returns an empty Bundle. Search values are matched against stored data, so check spelling and the exact code values, such as
femalerather thanF. - A create works but the next read returns 404. The shared test server can be reset. Create the resource again.
Quick reference and next steps
Production FHIR servers add authorization, most often SMART on FHIR, which is OAuth 2.0 with healthcare scopes, so the next step is learning how your target server issues access tokens.
| Task | Request |
|---|---|
| Check server version | GET [base]/metadata |
| Read a patient | GET [base]/Patient/{id} |
| Search patients | GET [base]/Patient?family=Smith&_count=5 |
| Create a patient | POST [base]/Patient with a JSON body |
| Record a vital sign | POST [base]/Observation with a JSON body |
| List a patient's heart rates | GET [base]/Observation?subject=Patient/{id}&code=http://loinc.org%7C8867-4&_sort=-date |
| Update | PUT [base]/Patient/{id} with the full resource |
| Delete | DELETE [base]/Patient/{id} |
Other topics worth exploring: transaction Bundles for saving several resources in one request, the _include search parameter for returning related resources together, and implementation guides such as US Core that add profiles for U.S. healthcare data.