Developers / FHIR for systems
Integration engines and clinical systems

FHIR between systems

If your integration engine already speaks FHIR R4, it can post Bundles into a HealthOS flow, write to and read from our FHIR API, or export whole populations in bulk. No person signs in. The connection itself is authorised, and it sees one organisation.

In short

  • Send Bundles to a signed flow endpoint when the provider wants the ESB to map them, or write straight to /fhir when your FHIR is already clean.
  • Reads and bulk exports come from /fhir, R4 with AU Core profiles, using SMART Backend Services.
  • Every connection is installed by one provider and stays inside that organisation.

Step by step: Send FHIR Bundles through the ESB and connect a backend system to the fhir api, with screenshots of each step.

Where it fits

Most hospitals and larger providers already run an integration engine. It is where feeds are routed, translated and monitored, and the team that runs it would rather not hand-build another point-to-point link. So we treat your engine as the system on the other side, and HealthOS’s own integration engine (the ESB) as the front door.

A Bundle that arrives is unwrapped into its resources. Each one runs through the flow the provider configured: the mapping, the person match on Medicare, IHI, MRN or DVA numbers, and the write into the Canonical Care Record. Anything the match can’t settle waits in a review queue rather than creating a duplicate person.

Sending records through the ESB

When a provider adds a FHIR source to a flow, HealthOS gives them a URL with an unguessable token and a signing secret. They pass both to you. Post a transaction or batch Bundle to the FHIR endpoint and you get a transaction-response or batch-response back, one entry per entry you sent.

POST /api/v1/flow-webhooks/{token}/fhir
Content-Type: application/fhir+json
X-HealthOS-Timestamp: 1760668800
X-HealthOS-Signature: 5f1c…e9 (hex HMAC-SHA256 of "1760668800." + body)
Idempotency-Key: lab-feed-2026-10-17-000193

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:7b0c…",
      "resource": { "resourceType": "Observation", "status": "final", … },
      "request": { "method": "POST", "url": "Observation" }
    }
  ]
}

A Bundle carries up to 1,000 entries, and each entry runs as its own unit of work, so one bad entry doesn’t sink the rest. The plain flow endpoint, /api/v1/flow-webhooks/{token} without the /fhir, also takes batch, collection, searchset and message Bundles, and keeps a document Bundle whole. Signing works the same way on both, and is described on the webhooks page.

Reading records out

The FHIR API lives at https://{your HealthOS host}/fhir. Its CapabilityStatement is at /fhir/metadata, and the SMART configuration at /.well-known/smart-configuration. A system with no user present authenticates with SMART Backend Services: the provider registers your system, and you exchange its credentials for a short-lived token with system/ scopes.

POST /smart/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=system/Patient.rs system/Observation.rs
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={JWT signed with your private key}

We prefer an asymmetric client, which signs a JWT with a private key we never see and publishes its public keys at a JWKS URL. A client secret also works. The token then goes on every read:

GET /fhir/Patient?identifier=http://ns.electronichealth.net.au/id/hi/ihi/1.0|8003608166690503
Authorization: Bearer {access_token}
Accept: application/fhir+json

Writing through the FHIR API

When your system already holds clean FHIR and needs no mapping in between, write straight to the API. Ask for write scopes (system/Observation.cu, or .write in the v1 form) and the provider adds them to your registration.

InteractionRequestAnswer
CreatePOST /fhir/{type}201 with Location and ETag
Conditional createThe same, with If-None-Exist: identifier=system|value201 when nothing matches, 200 with the existing record when one does
UpdatePUT /fhir/{type}/{id} with If-Match: W/"3"200, or 412 if someone else changed it first
TransactionPOST /fhir with a transaction BundleAll entries or none. urn:uuid references between entries are resolved
BatchPOST /fhir with a batch BundleEach entry on its own, with a status per entry

Each write lands where the same data entered by hand would. A vital sign is charted on the patient’s current stay, so the NEWS score is calculated and the deterioration rules run. Conditions go on the problem list, coded in ICD-10-AM or SNOMED CT-AU. A Patient create runs person matching first. A confident match returns the existing person and writes nothing, an uncertain one is refused with a 409 rather than creating a duplicate, and only a person with no match is registered.

If HealthOS can’t store something faithfully, it says so with a 422 and an OperationOutcome naming the element, rather than keeping half of it. Every write is audited and recorded as a Provenance naming your system, which you can read back with GET /fhir/Provenance?target=Observation/{id}. Prefer: return=minimal, representation and OperationOutcome are honoured.

Bulk export

For a whole population, use FHIR Bulk Data export rather than paging through searches. Kick off an export for the organisation, for every patient, or for one facility’s patients:

GET /fhir/Group/{facility id}/$export?_type=Patient,Observation&_since=2026-10-01T00:00:00Z
Authorization: Bearer {access_token}
Accept: application/fhir+json
Prefer: respond-async

202 Accepted
Content-Location: https://{your HealthOS host}/fhir/bulk-status/{job id}

Poll the status URL. While the job runs you get 202 with X-Progress. When it finishes you get the manifest, listing one or more NDJSON files per resource type, downloadable with the same token. DELETE on the status URL cancels the job and removes its files. Files are deleted 24 hours after the job finishes.

  • Groups are facilities. GET /fhir/Group lists them.
  • _type, _since and _outputFormat are supported, and patient in a POSTed Parameters body.
  • An export carries only the types your system/ scopes can read. Anything else you asked for is listed in the error file.
  • One export runs per client at a time. A second gets a 429 with Retry-After.

Resources

These are the resources the API serves, profiled on AU Core where a profile exists. The ones in bold can also be written.

AreaResources
PeoplePatient, RelatedPerson, Practitioner, Organization, Group
ClinicalCondition, AllergyIntolerance, Observation, Procedure, Immunization, MedicationRequest
Care eventsEncounter, ServiceRequest, Device
FormsQuestionnaire, QuestionnaireResponse
FundingCoverage, Account, ChargeItem
TrustProvenance

Identifiers

Send the identifiers you hold, with their standard systems. IHI, Medicare and DVA numbers are what make a match certain. An MRN helps too, alongside the facility that issued it. A record we can’t match with confidence is held for a person to review. It is never guessed.

Errors and retries

Errors come back as an OperationOutcome on the FHIR endpoints and as a JSON error with a stable code on the plain ones. On the ESB endpoints, send an Idempotency-Key with every delivery. For 24 hours, a retry with the same key gets the first answer back instead of writing twice, and a retry that arrives while the first is still running is told to wait with a 409. On the FHIR API, use a conditional create with If-None-Exist so a retried create finds the record it already made.

Bring your interface spec

Send us the resources and profiles your engine produces. We’ll map them with you against a test organisation.