Omni Health
Developer guide

Build on OmniHealth

A FHIR R4 API for reading and writing patient health data, plus platform extensions — webhooks and patient notifications — that go beyond the FHIR baseline. This guide gets you from application to a working integration.

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

  1. Apply. Sign in and submit the developer application — tell us who you are and what you want to build.
  2. Approval. We review it; you’re notified when you’re approved.
  3. Workspace. Your developer workspace is where you install services, create API clients, and manage webhooks.
  4. Create a client. In the workspace, create an API client. You’ll see a client_id and a client_secret once — store the secret securely; it’s never shown again.
Keep secrets server-side. Client secrets and webhook signing secrets authenticate you. Never ship them in a mobile app, browser code, or a public repo. If one leaks, revoke the client in your workspace and create a new one.

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:

DistributionWho can use it
GeneralAny patient (e.g. a Fitbit integration)
Service providerOnly patients of the assigned providers
PrivateNobody yet — your own testing
Why this matters for your API calls: you can only read and write data for services you own, and only for patients who have enabled that service. Ownership + patient enrolment is the consent model the whole API rests on.

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_…"
Request only what you need. The 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

স্কোপGrants
system/Observation.readRead/search observations
system/Observation.writeWrite (ingest) observations
system/Patient.readRead a patient resource
system/*.readRead any supported resource
system/Notification.writeSend a patient notification

Access levels

LevelData visible
Restricted defaultOnly data produced by your own services. This is enforced in the database on every query — a scope alone never widens it.
ElevatedAll 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.)

The consent gate. A write is accepted only when (1) the service is one you own and (2) the patient has enabled it. Otherwise you get 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:

EventFires when
service.enabledA patient enables your service — you may now sync/write their data.
service.disabledA patient disables it — stop syncing.
observation.createdNew 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.

Make your handler idempotent. A retry can redeliver an event you already processed. Key off the payload (patient + event + 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();
Always verify. Without the signature check, anyone who learns your callback URL could POST forged events. Use a constant-time comparison (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 effectiveDateTime on writes so data lands with the time it was measured, not the time you uploaded it.

Errors

অবস্থাMeaning
400Malformed request — the body describes what’s wrong (FHIR OperationOutcome).
401Missing/expired token — fetch a new one.
403Scope missing, or the consent gate blocked a write/notify (not your service, or patient hasn’t enabled it).
404Resource not found or not visible to your client.
429Rate 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 →

×