sondahub

sondahub / FHIR test server

FHIR test server

A public FHIR R4 server over a synthetic clinic: 130 patients and three years of their care — encounters, vital signs and labs coded in LOINC, problems in SNOMED CT and ICD-10-CM, prescriptions in RxNorm, immunizations in CVX, allergies, procedures, lab reports and appointments. Every R4 search parameter, create, update, JSON Patch and delete with versions, history, transactions, Patient/$everything, $validate and OperationOutcome errors. Writes stick for you; nothing is stored on the server. Free, no key, no real patients.

The clinic

A small community health system: 130 patients, the clinicians who look after them and three years of their care — 10,636 resources in all, the same on every build. Every resource is valid R4 (the server checks its own data the way it checks yours).

TypeResourcesWhat is in it
Patient130People of every age, with an MRN, address, language, GP and clinic; a few deceased.
Practitioner20Family, internal and emergency medicine, pediatrics, cardiology, nurse practitioners and nurses, with NPIs.
PractitionerRole20Who works where: specialty (NUCC), location, hours.
Organization6A community health system, its three clinics, a hospital and its lab.
Location6The clinics, the emergency department, a ward and the lab, with coordinates.
Encounter876Physicals, well-child visits, follow-ups, sick visits, video visits, ED visits and admissions.
Observation6,884Vital signs to the R4 profiles, labs with reference ranges and flags, smoking status, PHQ-9 and GAD-7 scores.
Condition360Problem-list conditions and encounter diagnoses, SNOMED CT with ICD-10-CM beside it.
MedicationRequest285Long-term and short-course prescriptions, RxNorm, with dosage and dispense.
AllergyIntolerance50Food, drug and environmental allergies and intolerances, with reactions.
Immunization377Childhood series, flu, COVID-19, zoster, pneumococcal, Tdap — CVX codes.
Procedure612Screenings, ECGs, spirometry, venipunctures, throat cultures, colonoscopies.
DiagnosticReport365Lipid panels, metabolic panels, blood counts and HbA1c, with their results.
Appointment645Past visits fulfilled, and upcoming ones booked, pending, cancelled or waitlisted.
Base URL
https://api.sondahub.com/fhir
Capabilities
https://api.sondahub.com/fhir/metadata
Version
FHIR R4 (4.0.1)
Format
application/fhir+json
Auth
none

Try it here

The examples on this page share one session: run them in order and each sees what the one before it wrote.
Patients named García (case and accents do not matter)
curl -G https://api.sondahub.com/fhir/Patient \
  --data-urlencode "name=GARCÍA" \
  --data-urlencode "_count=5"
Patient 1’s heart rates, newest first
curl -G https://api.sondahub.com/fhir/Observation \
  --data-urlencode "patient=1" \
  --data-urlencode "code=http://loinc.org|8867-4" \
  --data-urlencode "_sort=-date" \
  --data-urlencode "_count=5"
Blood pressures with a systolic over 160, and whose they are
curl -G https://api.sondahub.com/fhir/Observation \
  --data-urlencode "component-code-value-quantity=http://loinc.org|8480-6\$gt160" \
  --data-urlencode "_include=Observation:patient" \
  --data-urlencode "_count=3"
Patients with type 2 diabetes (reverse chaining)
curl -G https://api.sondahub.com/fhir/Patient \
  --data-urlencode "_has:Condition:patient:code=http://snomed.info/sct|44054006" \
  --data-urlencode "_summary=true" \
  --data-urlencode "_count=5"
Lab reports with their results (_include)
curl -G https://api.sondahub.com/fhir/DiagnosticReport \
  --data-urlencode "patient=1" \
  --data-urlencode "_include=DiagnosticReport:result" \
  --data-urlencode "_count=1"
Everything about patient 1: problems and prescriptions
curl -G https://api.sondahub.com/fhir/Patient/1/$everything \
  --data-urlencode "_type=Patient,Condition,MedicationRequest"
Create a patient
curl -X POST https://api.sondahub.com/fhir/Patient \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "resourceType": "Patient",
  "identifier": [
    {
      "system": "https://example.org/mrn",
      "value": "A-1001"
    }
  ],
  "name": [
    {
      "family": "Doe",
      "given": [
        "Ana"
      ]
    }
  ],
  "gender": "female",
  "birthDate": "1988-03-14",
  "telecom": [
    {
      "system": "email",
      "value": "[email protected]"
    }
  ]
}'
Record a heart rate for them (Patient/131)
curl -X POST https://api.sondahub.com/fhir/Observation \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "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/131"
  },
  "effectiveDateTime": "2026-10-02T09:30:00-04:00",
  "valueQuantity": {
    "value": 72,
    "unit": "beats/minute",
    "system": "http://unitsofmeasure.org",
    "code": "/min"
  }
}'
JSON Patch: add a phone, fix the birth date
curl -X PATCH https://api.sondahub.com/fhir/Patient/131 \
  -H "Content-Type: application/json-patch+json" \
  -H 'If-Match: W/"1"' \
  -d '[
  {
    "op": "add",
    "path": "/telecom/-",
    "value": {
      "system": "phone",
      "value": "+1 614-555-0199",
      "use": "mobile"
    }
  },
  {
    "op": "replace",
    "path": "/birthDate",
    "value": "1988-03-15"
  }
]'
Patient/131’s history
curl https://api.sondahub.com/fhir/Patient/131/_history
A transaction: urn:uuid and conditional references, all or nothing
curl -X POST https://api.sondahub.com/fhir \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:5b1d8f7e-4c1a-4f0e-9a51-2f3c7d9e1a01",
      "resource": {
        "resourceType": "Patient",
        "identifier": [
          {
            "system": "https://example.org/mrn",
            "value": "A-2002"
          }
        ],
        "name": [
          {
            "family": "Okafor",
            "given": [
              "Chidi"
            ]
          }
        ],
        "gender": "male"
      },
      "request": {
        "method": "POST",
        "url": "Patient",
        "ifNoneExist": "identifier=https://example.org/mrn|A-2002"
      }
    },
    {
      "fullUrl": "urn:uuid:5b1d8f7e-4c1a-4f0e-9a51-2f3c7d9e1a02",
      "resource": {
        "resourceType": "Condition",
        "clinicalStatus": {
          "coding": [
            {
              "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
              "code": "active"
            }
          ]
        },
        "verificationStatus": {
          "coding": [
            {
              "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
              "code": "confirmed"
            }
          ]
        },
        "category": [
          {
            "coding": [
              {
                "system": "http://terminology.hl7.org/CodeSystem/condition-category",
                "code": "problem-list-item"
              }
            ]
          }
        ],
        "code": {
          "coding": [
            {
              "system": "http://snomed.info/sct",
              "code": "59621000",
              "display": "Essential hypertension (disorder)"
            }
          ]
        },
        "subject": {
          "reference": "urn:uuid:5b1d8f7e-4c1a-4f0e-9a51-2f3c7d9e1a01"
        },
        "recorder": {
          "reference": "Practitioner?identifier=http://hl7.org/fhir/sid/us-npi|9000079191"
        }
      },
      "request": {
        "method": "POST",
        "url": "Condition"
      }
    },
    {
      "request": {
        "method": "GET",
        "url": "Condition?patient.identifier=https://example.org/mrn|A-2002"
      }
    }
  ]
}'
A resource the server refuses: every problem, with where
curl -X POST https://api.sondahub.com/fhir/Observation \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "resourceType": "Observation",
  "status": "done",
  "code": {
    "text": "Heart rate"
  },
  "subject": {
    "reference": "Patient/999999"
  },
  "effectiveDateTime": "2026-10-02T09:30",
  "valueQuantity": {
    "value": "72",
    "unit": "bpm",
    "code": "/min"
  }
}'

Writes, versions and references

  • Create: POST to the type — 201 with Location: …/Patient/{id}/_history/1, ETag: W/"1" and Last-Modified. If-None-Exist makes it conditional (200 when one matches, 412 when several do).
  • Update: PUT the whole resource, its id matching the URL's — a new version, unless nothing changed. If-Match: W/"n" makes it version-aware (412 when stale). PUT to a new id creates it, if the id is yours to choose: one with a letter or a dash in it (ids of digits only are the server's).
  • Patch: JSON Patch (application/json-patch+json) — add, remove, replace, move, copy and test.
  • Delete: 200 with an OperationOutcome (204 with Prefer: return=minimal); reads after it answer 410 Gone, and the delete is a version in the history. A resource something still refers to is not deleted (409), unless _cascade=delete.
  • Conditional update, patch and delete: PUT /fhir/Patient?identifier=… and so on — one match or a 412.
  • Checked. Every element known and of its type, cardinality, primitive formats (a dateTime with a time needs its seconds and zone), required bindings, the R4 invariants of each type, and references: one to a resource that is not here is a 422. Every problem is listed, each with its FHIRPath expression. POST /fhir/{type}/$validate checks without writing.
  • Prefer: return=minimal, representation (the default) or OperationOutcome.
  • History: /_history on a resource, a type or the whole server, with _since and _count; /_history/{n} reads a version.

Transactions and batches

POST a Bundle to https://api.sondahub.com/fhir. A transaction runs as R4 orders it — the DELETEs, the POSTs, the PUTs and PATCHes, then the GETs — and every POST gets its id first, so a reference to an entry's fullUrl (urn:uuid:…) anywhere in the Bundle becomes Type/id; ifNoneExist and conditional references (Practitioner?identifier=…) resolve to the one resource they find. If any entry fails, nothing is kept and the answer is that entry's OperationOutcome. A batch runs each entry on its own and answers each with its status. Up to 200 entries; a PATCH entry carries its JSON Patch as a Binary.

From code

FHIR is HTTP and JSON underneath; this is Python's requests — a reverse-chained search, a write carried in the session, and the next links followed:

import requests

base = 'https://api.sondahub.com/fhir'
http = requests.Session()
http.headers['Accept'] = 'application/fhir+json'

# patients with type 2 diabetes, and their latest HbA1c
bundle = http.get(f'{base}/Patient', params={
    '_has:Condition:patient:code': 'http://snomed.info/sct|44054006',
    '_count': 5,
}).json()
for entry in bundle['entry']:
    patient = entry['resource']
    a1c = http.get(f'{base}/Observation', params={
        'patient': patient['id'],
        'code': 'http://loinc.org|4548-4',
        '_sort': '-date',
        '_count': 1,
    }).json()
    latest = a1c['entry'][0]['resource']['valueQuantity'] if a1c.get('entry') else None
    print(patient['name'][0]['family'], latest and f"{latest['value']} {latest['unit']}")

# a write sticks when the session token travels back
made = http.post(f'{base}/Patient', json={
    'resourceType': 'Patient',
    'name': [{'family': 'Doe', 'given': ['Ana']}],
    'gender': 'female',
})
http.headers['X-Sondahub-Session'] = made.headers['X-Sondahub-Session']
print(made.status_code, made.headers['Location'])
print(http.get(f"{base}/Patient/{made.json()['id']}").json()['name'])

# every page of a search, following the next links
url, count = f'{base}/Observation?patient=1&category=vital-signs', 0
while url:
    page = http.get(url).json()
    count += len(page.get('entry', []))
    url = next((link['url'] for link in page['link'] if link['relation'] == 'next'), None)
print(count)

What it answers

GET/fhir/metadataThe CapabilityStatement.
GET/fhir/{type}Search; also POST /fhir/{type}/_search, and GET /fhir?_type=… across types.
GET/fhir/{type}/{id}Read, with ETag and Last-Modified; If-None-Match and If-Modified-Since answer 304.
GET/fhir/{type}/{id}/_history/{n}A version; /_history on a resource, a type or the server.
POST/fhir/{type}Create; If-None-Exist for a conditional create.
PUT/fhir/{type}/{id}Update (If-Match), or create with your own id; PUT /fhir/{type}?… is conditional.
PATCH/fhir/{type}/{id}JSON Patch.
DELETE/fhir/{type}/{id}Delete; _cascade=delete for what refers to it.
GET/fhir/Patient/{id}/{type}The Patient compartment.
GET/fhir/Patient/{id}/$everythingThe patient and everything about them: start, end, _since, _type, _count.
POST/fhir/{type}/$validateCheck a resource without storing it (also GET /fhir/{type}/{id}/$validate).
POST/fhirA transaction or batch Bundle.

Questions

Is any of this real patient data?

No. The clinic is synthetic, generated from a fixed seed: the people, places and record numbers are invented, every NPI starts with 9 (a real one starts with 1 or 2), phone numbers are in the 555-01xx range, emails are at example domains, and every resource carries the HTEST tag (test health data). The codes are real — LOINC, SNOMED CT, ICD-10-CM, RxNorm, CVX, UCUM — so the data reads like a chart. Please do not send real patient data: it would travel in your session token.

Which FHIR version?

R4, 4.0.1 — the version most servers and the US Core and international IGs build on. The CapabilityStatement says so, an Accept header asking for another fhirVersion is answered 406, and the R4B/R5 shapes (an Encounter.class that is a list, a CodeableReference) are refused by the validator as R4 refuses them.

Do writes stick?

Yes, for you: create, update, patch and delete are validated, versioned and answered as a FHIR server answers them, and the answer carries an X-Sondahub-Session header. Send it back and the next request sees your change — in reads, searches, includes, $everything and _history (the newest 3 versions of each resource are kept). Nothing is stored on the server; drop the token and the clinic is as it was. How sessions work.

Why does deleting a patient answer 409?

Because something refers to it — an encounter, an observation — and a delete that leaves references pointing at nothing is refused, as HAPI FHIR refuses it by default. The OperationOutcome names the first resource in the way. Delete with _cascade=delete (or the header X-Cascade: delete) to delete what refers to it too, up to 200 resources.

Can I use a FHIR client library or SMART app?

A client library takes the base URL https://api.sondahub.com/fhir and reads the CapabilityStatement at /metadata; JSON only, so set it to JSON if it defaults to XML. There is no authorization server: the server is open, so a SMART on FHIR launch (which needs one) cannot be tested here.

What is not supported?

XML and Turtle (406); FHIRPath Patch (415 — JSON Patch is); the token modifiers that need a terminology server, :in, :not-in, :above and :below (400); _filter, _list, _query and _contained (ignored with a warning, or 400 under Prefer: handling=strict); _at in history; compartments other than the Patient one; resource types beyond these fourteen (404); subscriptions, bulk $export and the terminology operations. Units are not converted: a quantity search matches the unit it names.

How are dates without a time zone searched?

As UTC: date=2025-03-04 is that day in UTC. The clinic itself keeps US Eastern time, so its visits (between 8 and 5) fall on the same day either way.