← Kyle Bush, Technical Writer
Technical writing sample · Developer documentation

FHIR R4 API Quickstart: Patient and Observation Resources

By Kyle Bush · October 2026

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:

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.

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:

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"
}

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

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

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.

Sources