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:
- Build
"{webhook-id}.{webhook-timestamp}.{raw body}"— the raw bytes, not a re-serialised copy. - HMAC-SHA256 it with the base64-decoded secret (strip
whsec_), and base64-encode the result. - Compare, in constant time, against each space-separated
v1,value inwebhook-signature. Any match verifies. - 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
timestampif 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 Goneswitches the endpoint off permanently; use it to say stop.429withRetry-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 throughGET /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.