FHIR Exchange
Elix uses HL7 FHIR R4 where records cross into or out of another system. Inside Elix, data lives in its own model; at the boundary it is expressed as standard FHIR resources with standard codes, so the other side does not need to learn Elix's model.
| What | FHIR resources | How |
|---|---|---|
| Encounters | Encounter, Bundle (searchset) | Read and search over the FHIR interface |
| Lab orders and results | ServiceRequest, Observation, DiagnosticReport | Fetch as a bundle, or publish to a FHIR server |
| Imaging orders and reports | ServiceRequest, DiagnosticReport | Fetch as a bundle, or publish to a FHIR server |
| Vital signs | Observation | Fetch, or publish to a FHIR server |
| Patients | Patient | Delivered to a health information exchange, confirmed by signed callback |
All calls below use the base address https://api.app.elix.ng/api/v1 and the bearer token, facility headers and purpose of use described in Authentication.
Reading encounters
The encounter service exposes a FHIR R4 read interface. Responses use the media type application/fhir+json.
Read one encounter
curl 'https://api.app.elix.ng/api/v1/encounters/fhir/Encounter/<encounterId>?purposeOfUse=TREATMENT' \
-H 'Authorization: Bearer <accessToken>' \
-H 'x-org-id: <organisation id>' -H 'x-facility-id: <facility id>'
Returns a FHIR Encounter. The resource carries:
status:planned,in-progress,finished,cancelledorentered-in-errorclass, coded from the HL7 v3 ActCode systemsubjectasPatient/<id>andserviceProviderasOrganization/<id>periodwith the start and, once the visit closes, the endparticipantentries for the clinicians involved
An encounter outside your tenant or facility answers 404, so the response never confirms that an id exists elsewhere.
Search a patient's encounters
curl 'https://api.app.elix.ng/api/v1/encounters/fhir/Encounter?patient=Patient/<patientId>&status=finished&date=ge2026-01-01&_count=50&purposeOfUse=TREATMENT' \
-H 'Authorization: Bearer <accessToken>' \
-H 'x-org-id: <organisation id>' -H 'x-facility-id: <facility id>'
| Parameter | Required | Notes |
|---|---|---|
patient | Yes | A patient id, bare or as Patient/<id>. Search is always for one patient, so their privacy setting can be applied. |
purposeOfUse | Yes | TREATMENT, PAYMENT, OPS or RESEARCH |
status | No | One FHIR encounter status. An unknown value is a 400, not an empty result. |
date | No | A FHIR date with an optional prefix: ge2026-01-01, le2026-12-31. A bare date means that day. |
_count | No | Page size, 1 to 100, default 20 |
cursor | No | Taken from the previous page's next link |
The answer is a searchset Bundle, newest encounter first. It has no total: Elix pages by position and does not count the whole set. If there are more results, the Bundle has a link with relation: "next". Follow it until there is none.
Required permission: encounters.access, or the narrower encounters.fhir.encounter.search and encounters.fhir.encounter.read.
Lab, imaging and vital signs as FHIR
Each lab order, imaging order and vital signs set can be returned as FHIR resources.
| Endpoint | Returns | Permission |
|---|---|---|
GET /investigations/lab/orders/<labOrderId>/clinical/fhir | ServiceRequest, Observation per result, DiagnosticReport | investigations.lab.list |
GET /investigations/imaging/orders/<imagingOrderId>/clinical/fhir | ServiceRequest, DiagnosticReport | investigations.imaging.list |
GET /encounters/vitals/<vitalSetId>/clinical/fhir | Observation per vital sign, LOINC coded | encounters.vitals.read |
The lab and imaging answers hold the source bundle and the resources sorted by type:
{
"orderId": "3f9a...",
"bundle": { "resourceType": "Bundle", "type": "collection", "entry": [ ... ] },
"orders": [ { "localId": "...", "resourceType": "ServiceRequest", "status": "completed", "code": "...", "resource": { ... } } ],
"observations": [ { "localId": "...", "resourceType": "Observation", "code": "718-7", "occurredAt": "2026-10-02T09:14:00Z", "resource": { ... } } ],
"reports": [ { "localId": "...", "resourceType": "DiagnosticReport", "conclusion": "...", "resource": { ... } } ]
}
The resource field is the FHIR resource itself. Use it as is; the fields beside it are a summary for convenience.
Publishing to a FHIR server
Elix can push the same resources to a FHIR server, such as the one behind an external EMR or a regional record.
| Endpoint | Publishes | Permission |
|---|---|---|
POST /investigations/lab/orders/<labOrderId>/clinical/publish-fhir | The order, its results and its report | investigations.lab.release |
POST /investigations/imaging/orders/<imagingOrderId>/clinical/publish-fhir | The order and its report | investigations.imaging.write |
POST /encounters/vitals/<vitalSetId>/clinical/publish-fhir | Each vital sign | encounters.vitals.manage |
How Elix writes to the server:
- A resource with an id is sent as
PUT <base>/<Type>/<id>, so publishing twice updates the same resource rather than creating a copy. A resource without one is sent asPOST <base>/<Type>. - Requests carry
Content-Type: application/jsonand, where Elix has them,x-correlation-idandx-idempotency-keyheaders that your server can log or use to drop repeats. - The server answers with the stored resource. Elix records the id your server returns.
The FHIR server Elix publishes to is configured by Elix for your deployment. Tell us its base address and how it authenticates (bearer token, basic auth or an API key header) when you request access; see Connect your EMR.
Health information exchanges
Elix sends patient records to health information exchanges, such as a state's shared record, as FHIR Patient resources wrapped in a delivery envelope. Delivery runs through a queue with retries, so a slow or unavailable exchange delays a record rather than losing it. Records that still fail after the retries are held for review and can be replayed.
Each request Elix sends to an exchange is signed. It carries the time it was signed, a nonce, a SHA-256 hash of the body, and an HMAC-SHA256 signature over the method, path, query, body hash, time, nonce and client id. An exchange can check the signature and refuse anything stale or replayed.
Signed callbacks
When an exchange or partner calls Elix back, for example to confirm that a patient was accepted and give the id it assigned, the call has no bearer token. It is authenticated by a signature instead.
- Elix and the partner share a secret, issued per connection.
- The partner computes
HMAC-SHA256(secret, raw request body)and sends the hex digest in the agreed header, optionally prefixed withsha256=. - The signature is over the exact bytes sent. Do not sign a re-serialised copy of the JSON: spacing or key order that differs from what you send will fail the check.
- A request with no signature, a wrong signature, or for a connection with no secret configured is refused with
401. Elix never accepts an unauthenticated callback.
A signing example in Node.js:
import {createHmac} from 'node:crypto';
const body = JSON.stringify(payload); // send exactly this string
const signature = createHmac('sha256', secret).update(body).digest('hex');
await fetch(callbackUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'x-signature': `sha256=${signature}`},
body,
});
The callback address, header name and secret are agreed per connection when the integration is set up.
Codes and terminology
Elix codes clinical content with standard systems where they exist: LOINC for observations and lab tests, HL7 terminology for encounter classes and participation types. Where a facility uses local codes, an administrator maps them to the standard ones in the terminology maps of the integration platform, so what leaves Elix is coded consistently.