Overview
The base URL for all API calls is:
https://omnihealth.solutions/fhir
The API speaks FHIR R4 (JSON). Authentication is OAuth 2.0 client credentials — the SMART-on-FHIR “backend services” pattern — so your server talks to ours directly, no user login in the loop. Discover capabilities at any time:
curl https://omnihealth.solutions/fhir/metadata
On top of standard FHIR you also get two platform extensions: webhooks that call you back when something happens to your service, and a patient-notification operation.
Getting access
- Apply. Sign in and submit the developer application — tell us who you are and what you want to build.
- Approval. We review it; you’re notified when you’re approved.
- Workspace. Your developer workspace is where you install services, create API clients, and manage webhooks.
- Create a client. In the workspace, create an API client. You’ll see a
client_idand aclient_secretonce — store the secret securely; it’s never shown again.
Sandbox & installing services
A monitoring tool is a service — a definition of the forms and data it collects. Build and test your service on the sandbox instance first. When it’s ready, install it onto production from your workspace by pasting the definition JSON you exported from the sandbox.
An installed service arrives as submitted and inactive. We review it, then publish it with a distribution scope:
| Distribution | Who can use it |
|---|---|
| General | Any patient (e.g. a Fitbit integration) |
| Service provider | Only patients of the assigned providers |
| Private | Nobody yet — your own testing |
Authentication
Exchange your client credentials for a short-lived bearer token:
curl -X POST https://omnihealth.solutions/fhir/token \
-d grant_type=client_credentials \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d scope="system/Observation.read system/Observation.write"
Response:
{
"access_token": "oht_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "system/Observation.read system/Observation.write"
}
Send it on every call:
curl https://omnihealth.solutions/fhir/Observation \
-H "Authorization: Bearer oht_…"
scope parameter can ask for a subset of your granted scopes — a token used only for reading shouldn’t carry write access. Tokens live 3600 seconds (60 min); fetch a new one when it expires rather than requesting long-lived credentials.Scopes & access levels
Two things govern what a call can do: the scopes on the token (which resources/actions) and the client’s access level (how much data).
Scopes
| Scope | Grants |
|---|---|
system/Observation.read | Read/search observations |
system/Observation.write | Write (ingest) observations |
system/Patient.read | Read a patient resource |
system/*.read | Read any supported resource |
system/Notification.write | Send a patient notification |
Access levels
| Level | Data visible |
|---|---|
| Restricted default | Only data produced by your own services. This is enforced in the database on every query — a scope alone never widens it. |
| Elevated | All patient data. Granted by an administrator only, for a clear reason, and audited. Even elevated clients cannot write outside the consent model. |
Reading data
Observations map your services’ recorded data into FHIR. Search:
GET https://omnihealth.solutions/fhir/Observation?patient=123&_count=50
Supported search params: patient (patient id), identifier / _tag (your service id), and code (the clinical code, e.g. a LOINC). The response is a FHIR searchset Bundle. Read one by id:
GET https://omnihealth.solutions/fhir/Observation/psd-42
Read a patient (identifiers are only returned to elevated clients; restricted clients get a de-identified shell):
GET https://omnihealth.solutions/fhir/Patient/123
Diagnostic reports
A DiagnosticReport groups a diagnostic/lab service's Observations for a patient into a
single lab report (category LAB, report code LOINC 11502-2, with each result
referencing an Observation). Reports are produced for services that declare a
clinical code. Requires system/DiagnosticReport.read.
GET https://omnihealth.solutions/fhir/DiagnosticReport?patient=123
GET https://omnihealth.solutions/fhir/DiagnosticReport/dr-…
Bangladesh national profile alignment
We are aligning to the DGHS
Bangladesh Core FHIR IG (R4).
Observations already carry the shape those profiles expect: LOINC code, the fixed
laboratory category (for services that declare a clinical code), subject.display,
performer, and a valueQuantity with a UCUM unit when a service declares one.
A service declares its clinical code and unit via clinical_code/clinical_system
and value_key/value_unit/value_ucum.
We do not yet assert meta.profile for the national profiles, and we say so plainly: every
DGHS clinical profile — bd-observation,
bd-lab-result-observation and
bd-lab-report —
makes Encounter mandatory (1..1). Once Encounter (and Organization/Practitioner for
performer) land, meta.profile conformance can be asserted and validated against the DGHS sandbox.
Writing data
Push data in — for example a wearable syncing step counts — by POSTing a FHIR Observation.
Identify your service in meta.tag (or an identifier) under the system
https://omnihealth.solutions/fhir/service. Use Observation.code for the
clinical concept — a LOINC or SNOMED code where one exists. subject.reference is the
patient; put your metrics in component[].
POST https://omnihealth.solutions/fhir/Observation
Authorization: Bearer oht_…
Content-Type: application/fhir+json
{
"resourceType": "Observation",
"status": "final",
"meta": { "tag": [{ "system": "https://omnihealth.solutions/fhir/CodeSystem/service", "code": "your.service.id" }] },
"code": { "coding": [{ "system": "http://loinc.org", "code": "2339-0", "display": "Glucose" }], "text": "Blood glucose" },
"subject": { "reference": "Patient/123" },
"effectiveDateTime": "2026-07-24T09:00:00Z",
"component": [
{ "code": { "text": "mmol_l" }, "valueQuantity": { "value": 5.4 } }
]
}
Success returns 201 Created with a Location header and the stored resource.
(For backward compatibility, a service id in code.coding[] under our service CodeSystem is still accepted, but meta.tag is preferred.)
403. This is deliberate — a patient enabling your service is what authorises you to write their data. Watch the service.enabled / service.disabled events to know when you may start or must stop.Notify a patient
A platform extension with no FHIR equivalent, exposed as a FHIR operation: send an in-app notification to a patient who has enabled your service (“You hit 10,000 steps!”). Requires system/Notification.write.
POST https://omnihealth.solutions/fhir/$notify-patient
Authorization: Bearer oht_…
{
"service_id": "your.service.id",
"patient_id": 123,
"title": "Goal reached",
"body": "You hit 10,000 steps today.",
"link": "/health-river"
}
Same consent gate as writes. Returns a FHIR Parameters resource with delivered (how many recipients were notified — a patient’s record may be shared with carers).
Webhooks
Rather than poll, register a callback URL in your workspace and we’ll POST you a signed JSON payload when something happens to your service. Subscribe to any of:
| Event | Fires when |
|---|---|
service.enabled | A patient enables your service — you may now sync/write their data. |
service.disabled | A patient disables it — stop syncing. |
observation.created | New data was recorded for your service. |
The payload:
{
"event": "service.enabled",
"service_id": "your.service.id",
"service": "Step Counter",
"data": { "patient_id": 123 },
"created_at": "2026-07-24T09:00:00Z"
}
Delivery headers: X-OmniHealth-Event (the event name) and X-OmniHealth-Signature (see below). Respond 2xx to acknowledge — anything else is treated as a failure and retried with backoff for a few attempts.
created_at, or the observation id) so a duplicate is a no-op. Return 2xx quickly and do heavy work asynchronously — we time out after a few seconds.Verifying callbacks
Every webhook carries X-OmniHealth-Signature: t=<timestamp>,v1=<hmac>. The HMAC is SHA-256 over the string <timestamp>.<raw-body>, keyed with your webhook’s signing secret. Verify it before trusting a payload:
// PHP
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_OMNIHEALTH_SIGNATURE'] ?? '';
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $sig, $m);
$expected = hash_hmac('sha256', $m[1] . '.' . $raw, $YOUR_WEBHOOK_SECRET);
if (!hash_equals($expected, $m[2] ?? '')) { http_response_code(400); exit; }
// optional: reject if $m[1] is older than a few minutes (replay protection)
# Node
const crypto = require('crypto');
const [ , t, v1 ] = req.headers['x-omnihealth-signature'].match(/t=(\d+),v1=([a-f0-9]+)/);
const expected = crypto.createHmac('sha256', SECRET).update(t + '.' + rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.status(400).end();
hash_equals / timingSafeEqual), and treat the raw body as bytes — don’t re-serialize before hashing.Best practices
- Least privilege. Give each client only the scopes it needs; keep a client restricted unless there’s a real, approved reason for elevated.
- Respect consent. Only write and notify for enabled patients; stop on
service.disabled. Don’t retain data after a patient leaves. - Store only what you need, and never log secrets or full patient payloads.
- Handle token expiry by refreshing on
401, not by holding tokens forever. - Verify every webhook and make handlers idempotent and fast.
- Use
effectiveDateTimeon writes so data lands with the time it was measured, not the time you uploaded it.
Errors
| Status | Meaning |
|---|---|
400 | Malformed request — the body describes what’s wrong (FHIR OperationOutcome). |
401 | Missing/expired token — fetch a new one. |
403 | Scope missing, or the consent gate blocked a write/notify (not your service, or patient hasn’t enabled it). |
404 | Resource not found or not visible to your client. |
429 | Rate limited (see below) — retry after the Retry-After header. |
Every error is a FHIR OperationOutcome with a human-readable diagnostics field, so you can log and surface the reason directly.
Rate limits
Today: there are no hard rate limits enforced, but every call is logged. Please be a considerate citizen while limits are finalised:
- Reuse tokens for their full hour instead of minting one per call.
- Batch where you can, and prefer webhooks over polling.
- Back off on errors — exponential backoff with jitter, not a tight retry loop.
Coming: per-client quotas returning 429 Too Many Requests with a Retry-After header, plus X-RateLimit-Remaining on responses. When these land they’ll be announced on the changelog with generous defaults; sustained abuse may be limited sooner. Build the 429 + Retry-After path into your client now and you won’t need to change anything later.
Ready? Apply for developer access → · Try the 10-minute tutorial →