Webhooks
A general-purpose alternative to polling the change feed: register
an endpoint once and receive signed HTTP POSTs as new events land.
Creating a subscription
POST /api/v1/subscriptions
{
"name": "sync-integration",
"endpointUrl": "https://your-app.example.com/webhooks/rosetta",
"filter": { "resourceTypes": ["patient"] },
"payloadMode": "full"
}
The 201 response is the only place the signing secret is ever returned — store
it immediately, since no read endpoint returns it again. Losing it means creating a new
subscription; there is no "reveal secret" endpoint.
{
"id": "8d9e4a9a-9c2b-4a9e-8c9d-4f6b0a2e6f11",
"name": "sync-integration",
"endpointUrl": "https://your-app.example.com/webhooks/rosetta",
"filter": { "resourceTypes": ["patient"] },
"payloadMode": "full",
"status": "active",
"lastDeliveredSequence": 0,
"createdAt": "2026-07-14T04:32:56.082Z",
"updatedAt": "2026-07-14T04:32:56.082Z",
"secret": "3f6c2b8e9a1d4f7c0b2e5a8d1c4f7b0e9a2d5c8f1b4e7a0d"
}
Manage subscriptions afterward with GET /api/v1/subscriptions (list),
GET /api/v1/subscriptions/:id (fetch one), PATCH /api/v1/subscriptions/:id
(partial update — name, endpoint, filter, payload mode, or status), and
DELETE /api/v1/subscriptions/:id. None of these ever include secret.
Filter and payload mode
filter narrows which events are delivered to this subscription. Every field is an
allowlist; an absent or empty array means "everything" for that dimension:
resourceTypes— one or more of the resource types; see Data types for each type's full payload shape.operations—"create"and/or"update".sourceSystems— restrict to events whose internalsourceSystemcode matches.orgSlugs— restrict to one or more organizations; every slug must already exist, or the request is rejected with400 { "code": "unknown-org" }and nothing is persisted.
payloadMode controls whether deliveries carry the full record:
"full"(default) — every event carries its completerecord, same as the change feed."id-only"— events omitrecord; callGET /api/v1/records/:resourceType/:canonicalId(see Records API) for the fields you need. Useful for keeping webhook payloads small.
Verifying deliveries
Every delivery to endpointUrl carries a signature header:
x-rosetta-signature: t=<unix-ms>,v1=<hex hmac>
x-rosetta-subscription-id: <subscription id>
x-rosetta-delivery-id: <delivery id>
Verify it before trusting the body: parse t (a millisecond timestamp) and v1 (a hex
HMAC-SHA256 digest) out of the header, recompute HMAC-SHA256(secret, "${t}.${rawBody}") over the exact raw request bytes you received (do not
re-serialize the parsed JSON before verifying), and compare the two digests using a
constant-time comparison. Reject the delivery if they don't match.
Because a failed delivery is retried with the same event window, dedupe applied
events by events[].sequence — the same idempotency rule as the change feed. Respond
2xx quickly and do any slow processing asynchronously afterward; the delivery worker
treats a timeout the same as an error response.
Worked example: a signed delivery
- Receive the delivery. Headers:
Body (one event,content-type: application/jsonx-rosetta-signature: t=1752469976082,v1=6f2a9c1e4b7d0f3a8c5e2b9d6f1a4c7e0b3d6f9a2c5e8b1d4f7a0c3e6b9d2f5ax-rosetta-subscription-id: 8d9e4a9a-9c2b-4a9e-8c9d-4f6b0a2e6f11x-rosetta-delivery-id: 1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7a8b
payloadMode: "full"):{"deliveryId": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7a8b","subscriptionId": "8d9e4a9a-9c2b-4a9e-8c9d-4f6b0a2e6f11","attempt": 1,"events": [{"sequence": 1,"resourceType": "patient","canonicalId": "f3024f2e-0583-4759-a9b8-c534e6763391","operation": "create","version": 1,"occurredAt": "2026-07-14T04:32:56.082Z","orgSlug": "default","record": {"identifiers": [{ "system": "urn:michels:pid", "value": "1234567" }],"familyName": "Mueller","givenNames": ["Anna"],"birthDate": "1980-05-01","gender": "female"}}]} - Verify the signature before trusting the body: recompute
HMAC-SHA256(secret, "1752469976082.${rawBody}")over the exact raw bytes received and compare it tov1using a constant-time comparison. - Respond
2xxquickly, then process the event asynchronously and dedupe byevents[].sequence.
Retries, backoff, and suspension
If your endpoint responds with anything other than 2xx — including a network error or
a timeout — the delivery is retried: the same event window is redelivered later with
exponential backoff (each retry waits roughly twice as long as the previous one, up to
a fixed cap). After enough consecutive failures, the subscription is automatically
flipped to status: "suspended" and stops receiving deliveries.
To resume a suspended subscription, send:
PATCH /api/v1/subscriptions/:id
{ "status": "active" }
This resets the failure count and backoff so it doesn't immediately re-trip. You can
inspect delivery history — including failures and their HTTP status — with
GET /api/v1/subscriptions/:id/deliveries[?limit=] (default 50, capped at 200).