Skip to main content

Webhooks

A webhook tells your system that something happened on the platform, instead of you polling for it. A platform admin registers an HTTPS endpoint of yours and chooses what it hears about, and the platform then delivers a signed request for every matching event. The event list and every payload shape are in the API reference under webhooks; this page is the contract around them.

The request you receive​

POST <your endpoint>
user-agent: Intervengine-Webhooks/1.0
content-type: application/json
webhook-id: <uuid>
webhook-timestamp: <unix seconds>
webhook-signature: v1,<base64 HMAC-SHA256>

{"type":"userBiomarkers.created","timestamp":"<ISO 8601>","data":{…}}

webhook-id is stable across retries — it is your deduplication key. type is <resource>.<verb>: the API resource, in camelCase plural, and a past-tense verb.

Verify every request​

Deliveries follow the Standard Webhooks specification, so an official library for your language does the work. The secret — shown once, prefixed whsec_ — is displayed when the endpoint is created or its secret rotated. If you verify by hand:

  1. Build "{webhook-id}.{webhook-timestamp}.{raw body}" — the raw bytes, not a re-serialised copy.
  2. HMAC-SHA256 it with the base64-decoded secret (strip whsec_), and base64-encode the result.
  3. Compare, in constant time, against each space-separated v1, value in webhook-signature. Any match verifies.
  4. Reject a timestamp more than five minutes from now.

During a secret rotation the header carries two signatures for 24 hours, so the old secret keeps verifying while you switch.

Delivery semantics​

  • At least once. Retries can duplicate a delivery; deduplicate on webhook-id.
  • No ordering. Sort by the envelope timestamp if sequence matters.
  • Respond 2xx fast, then do the work. The request times out after 20 seconds.
  • Retries on any non-2xx, timeout or connection failure: about 5 seconds, then 5 minutes, 30 minutes, 2, 5, 10 and 10 hours — eight attempts over roughly 28 hours, after which the delivery is parked for an operator to replay.
  • Redirects are failures. Register the final URL.
  • 410 Gone switches the endpoint off permanently; use it to say stop.
  • 429 with Retry-After (up to an hour) is honoured.
  • HTTPS with a valid certificate, TLS 1.2 or later; no credentials in the URL; no private addresses. Deliveries come from your deployment's egress address — ask the operator for it if you filter inbound traffic.

What arrives​

Every domain the platform stores has events, and the payload is normally the same object the resource's GET returns. Five carry deliberately less:

  • Conversations are body-free. A chatMessages.* event says a message exists — id, thread, sender, timestamps — never its content. Read it through the chats endpoints if you are entitled to.
  • Biomarkers carry the clinical facts, never the device id or internal scores.
  • Assets always have url: null; a URL is minted per reader and would expire mid-retry.
  • Users and invitations carry no direct identifiers — no name, email, mobile or date of birth. Match on the user id or your own externalId, and read the rest through GET /api/v1/users/{id} if you are entitled to.

users.deleted is the erasure signal: when a participant is erased every other queued event about them is dropped, and this one is kept so you learn to erase your own copy.

What a participant types into the app arrives as responses (userAtomResponses, userSequenceResponses), not as biomarkers; readings from the phone's health store arrive as userBiomarkers.created. Responses fire on every write of the row, so filter on status (submitted, against drafts, dismissed and missed) for the facts you act on. Payloads carry ids, not names — read the atom or sequence through the API to learn what an id is.

Choosing what you hear about​

A webhook's events is a list of entries. Each names a resource, then narrows twice: actions picks the verbs and recordRefs the records.

[
{ "resource": "userTags", "actions": ["created", "deleted"], "recordRefs": ["CIED"] },
{ "resource": "userAtomResponses", "actions": ["created", "updated"], "recordRefs": ["BP_MORNING"] },
{ "resource": "userBiomarkers", "actions": ["created"], "recordRefs": ["heart_rate"] }
]

actions names at least one verb. A verb the resource has no event for is accepted and never fires — userTags has only created and deleted — so check it against the event list in the reference. An empty or missing recordRefs means every record of the resource.

A ref is an id or a code, and it names the catalogue record the event is about — userSequences narrows by sequence, userAtomResponses by atom, userTags by tag, tasks by task type; biomarkers take their codes (heart_rate, …). A tag's ref is its id or its code, never the external id an integration writes tags with. A ref that matches nothing delivers nothing rather than erroring, so a webhook that stays quiet is worth checking for a misspelt ref.

Build for change​

Payloads are additive: new optional fields appear without notice, and a breaking change ships as a new type name (….v2) that you subscribe to explicitly. Tolerate unknown fields and unknown types, and treat ping — the operator's test message — as one to verify and ignore.