Developers / Webhooks and contracts
Systems that send events as they happen

Post it when it happens

If your system can send JSON over HTTPS, it can push to HealthOS. For rostering, remote monitoring and location systems there is already a contract written, so you build to it once and every provider’s HealthOS reads it the same way.

In short

  • Each connection gets its own URL token and signing secret, scoped to one organisation.
  • Sign with HMAC-SHA256 over a timestamp and the raw body. Requests older than five minutes are refused.
  • Tokens and secrets rotate with 24 hours of overlap.

Step by step: Post your own JSON to a signed webhook and send worked shifts and device readings in our formats, with screenshots of each step.

How a webhook works

A provider adds your system as a source, either on a flow they build in the ESB or by installing one of the contracts below from the connector marketplace. HealthOS mints a URL with an unguessable token and a signing secret, and the provider’s administrator gives you both.

The token tells us which connection, and so which organisation, the request belongs to. Nothing in your body can move it to another one. A plain flow webhook then maps your JSON with the provider’s mapping, so you can send the shape you already have.

Signing

POST /api/v1/flow-webhooks/{token}
Content-Type: application/json
X-HealthOS-Timestamp: 1760668800
X-HealthOS-Signature: {hex HMAC-SHA256 of "1760668800." + raw body}
Idempotency-Key: {your id for this delivery}
  • Sign the exact bytes you send, after serialising the JSON.
  • The timestamp is unix seconds. A request signed more than five minutes ago, or more than a minute in the future, is refused, so a captured request can’t be replayed later.
  • We document lowercase hex. Upper-case hex, base64 and a sha256= prefix are accepted too.

A signature over the body alone, without a timestamp, is still accepted for older senders. It can be replayed for as long as the secret lives, so build new senders with the timestamp.

Rotation

An administrator can rotate the token, the secret or both. Whatever is replaced keeps working for 24 hours, so you can switch over without dropping events. A second rotation inside those 24 hours retires the first rotation’s old values straight away.

Retries

Retry anything that timed out or came back 5xx, with backoff. Each contract keeps a retry from counting twice:

EndpointHow duplicates are caught
Flow webhooksAn Idempotency-Key seen in the last 24 hours gets the first 202 back with Idempotent-Replayed: true. If the first delivery is still running, you get a 409
Worked shiftsThe same shift sent again is a redelivery and changes nothing. An amendment carries a later revision
Device readingsEach reading’s id is recorded, and a redelivered one is skipped
Location eventseventId is unique per connection. A redelivery answers { "duplicate": true } and alerts no one twice

Errors

Every refusal uses one envelope, and the code is stable, so branch on it rather than the message.

{ "success": false,
  "error": { "code": "SIGNATURE_INVALID", "message": "Invalid signature" },
  "meta": { "requestId": "…", "timestamp": "…" } }
StatusCodes
401SIGNATURE_INVALID, TIMESTAMP_INVALID, TIMESTAMP_EXPIRED
404WEBHOOK_UNKNOWN: the token is wrong, or was rotated more than 24 hours ago
409IDEMPOTENCY_IN_PROGRESS
400 or 422PAYLOAD_INVALID, IDEMPOTENCY_KEY_INVALID
503SECRET_NOT_CONFIGURED: the provider hasn’t finished setting up the connection

Ready-made contracts

Some kinds of system send the same kind of data to every provider. For these we’ve written the format down, and HealthOS does the mapping, so a provider only installs the connector and maps their sites.

Worked shifts from rostering and time and attendance systems

Post worked shifts to /api/v1/rostering/webhooks/{token}, one at a time or up to 1,000 in a batch. Or publish them at GET {your base URL}/shifts?modifiedSince=…&page=… and HealthOS will pull them on a schedule. A connection can do both.

{ "vendorShiftId": "RS-20260916-2300-E3001",
  "revision": "2026-09-17T10:00:00Z",
  "workerId": "E3001",
  "payCode": "RN",
  "shiftDate": "2026-09-16", "startTime": "23:00", "endTime": "07:30",
  "breakMinutes": 30,
  "siteCode": "MVB" }

Times are the site’s wall-clock time, and an end before the start means the shift finishes the next day. Unknown fields are refused, so a typo fails loudly. A shift that can’t be counted as it stands, such as an unknown worker, an unmapped site or an overlap, is held as an exception for staff to resolve. Accepted shifts feed care minutes and 24/7 RN reporting.

Readings from remote monitoring platforms

Post readings to /api/v1/devices/webhooks/{token}, up to 5,000 at a time, against an account the provider has paired with a person.

{ "providerUserId": "patient-account-123",
  "readings": [
    { "id": "r1", "type": "heart_rate", "value": 72, "unit": "bpm",
      "timestamp": "2026-10-13T06:00:00Z" },
    { "id": "r3", "type": "blood_pressure", "systolic": 128, "diastolic": 82,
      "timestamp": "2026-10-13T06:05:00Z" }
  ] }

Types include heart rate, blood pressure, blood glucose, oxygen saturation, respiratory rate, temperature, weight and steps, with units converted on the way in. A reading of a type we don’t know yet is counted and set aside, and the rest are kept. An account that isn’t paired gets a 202 and nothing is written, so your retries don’t pile up.

Duress and location events

A real-time location system posts duress, man-down and assist button presses, and zone changes, to /api/v1/locate/events/{token}. Each carries an eventType such as button.duress or location.zone_change, a unique eventId, the badge’s deviceId and its location. A button press raises a task and a notification to the roles the provider chose.

Already sending webhooks elsewhere?

Show us the payload. Often the only work is a mapping on our side and a signature on yours.