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).
| Type | Resources | What is in it |
|---|---|---|
| Patient | 130 | People of every age, with an MRN, address, language, GP and clinic; a few deceased. |
| Practitioner | 20 | Family, internal and emergency medicine, pediatrics, cardiology, nurse practitioners and nurses, with NPIs. |
| PractitionerRole | 20 | Who works where: specialty (NUCC), location, hours. |
| Organization | 6 | A community health system, its three clinics, a hospital and its lab. |
| Location | 6 | The clinics, the emergency department, a ward and the lab, with coordinates. |
| Encounter | 876 | Physicals, well-child visits, follow-ups, sick visits, video visits, ED visits and admissions. |
| Observation | 6,884 | Vital signs to the R4 profiles, labs with reference ranges and flags, smoking status, PHQ-9 and GAD-7 scores. |
| Condition | 360 | Problem-list conditions and encounter diagnoses, SNOMED CT with ICD-10-CM beside it. |
| MedicationRequest | 285 | Long-term and short-course prescriptions, RxNorm, with dosage and dispense. |
| AllergyIntolerance | 50 | Food, drug and environmental allergies and intolerances, with reactions. |
| Immunization | 377 | Childhood series, flu, COVID-19, zoster, pneumococcal, Tdap — CVX codes. |
| Procedure | 612 | Screenings, ECGs, spirometry, venipunctures, throat cultures, colonoscopies. |
| DiagnosticReport | 365 | Lipid panels, metabolic panels, blood counts and HbA1c, with their results. |
| Appointment | 645 | Past 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
curl -G https://api.sondahub.com/fhir/Patient \ --data-urlencode "name=GARCÍA" \ --data-urlencode "_count=5"
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"
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"
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"
curl -G https://api.sondahub.com/fhir/DiagnosticReport \ --data-urlencode "patient=1" \ --data-urlencode "_include=DiagnosticReport:result" \ --data-urlencode "_count=1"
curl -G https://api.sondahub.com/fhir/Patient/1/$everything \ --data-urlencode "_type=Patient,Condition,MedicationRequest"
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]"
}
]
}'
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"
}
}'
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"
}
]'
curl https://api.sondahub.com/fhir/Patient/131/_history
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"
}
}
]
}'
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"
}
}'
Search
All 260 R4 search parameters of these types, from the R4 definitions, plus the common ones (_id, _lastUpdated, _tag, _profile, _security, _source, _content, _text). A comma is OR, a repeated parameter is AND, a backslash escapes , | and $. GET /fhir/{type}?…, POST /fhir/{type}/_search with a form, or across types with GET /fhir?_type=….
| Type | Matching |
|---|---|
| string | Starts with, blind to case and accents (name=garc); :exact, :contains. A name matches on any part, and on given and family together. |
| token | code, system|code, |code (no system), system|; :not, :text, :of-type for identifiers. A bare code carries the system of its binding: gender=http://hl7.org/fhir/administrative-gender|female works. |
| date | eq ne gt lt ge le sa eb ap over ranges at the precision written: date=2025 is the year, a Period is its span. |
| number, quantity | The same prefixes, with implicit precision (100 is 99.5 to 100.5); value-quantity=gt5.4|http://unitsofmeasure.org|mg/dL, or gt5.4||mg/dL. |
| reference | 123, Patient/123 or the absolute URL; :Patient, :identifier; chains (subject:Patient.name=doe, patient.birthdate=lt1950) and _has, three deep. |
| composite | code-value-quantity, component-code-value-quantity and the rest, parts joined with $. |
| special | Location near=39.96|-82.99|10|km. |
| all | :missing=true|false. |
Results. _count (20 by default, 500 at most) with self, first, previous, next and last links; _sort by any parameter, - for descending; _include and _revinclude, with :iterate and * (up to 1,000 included resources a page, not counted in total); _summary (true, text, data, count) and _elements, tagged SUBSETTED; _total=none. An unknown parameter is ignored and named in an OperationOutcome entry (and left out of the self link); with Prefer: handling=strict it is a 400. A wrong value is always a 400 saying why.
Writes, versions and references
- Create: POST to the type — 201 with
Location: …/Patient/{id}/_history/1,ETag: W/"1"andLast-Modified.If-None-Existmakes 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}/$validatechecks without writing. - Prefer:
return=minimal,representation(the default) orOperationOutcome. - History:
/_historyon a resource, a type or the whole server, with_sinceand_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
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.