Skip to main content

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 internal sourceSystem code matches.
  • orgSlugs — restrict to one or more organizations; every slug must already exist, or the request is rejected with 400 { "code": "unknown-org" } and nothing is persisted.

payloadMode controls whether deliveries carry the full record:

  • "full" (default) — every event carries its complete record, same as the change feed.
  • "id-only" — events omit record; call GET /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​

  1. Receive the delivery. Headers:
    content-type: application/json
    x-rosetta-signature: t=1752469976082,v1=6f2a9c1e4b7d0f3a8c5e2b9d6f1a4c7e0b3d6f9a2c5e8b1d4f7a0c3e6b9d2f5a
    x-rosetta-subscription-id: 8d9e4a9a-9c2b-4a9e-8c9d-4f6b0a2e6f11
    x-rosetta-delivery-id: 1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7a8b
    Body (one event, 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"
    }
    }
    ]
    }
  2. Verify the signature before trusting the body: recompute HMAC-SHA256(secret, "1752469976082.${rawBody}") over the exact raw bytes received and compare it to v1 using a constant-time comparison.
  3. Respond 2xx quickly, then process the event asynchronously and dedupe by events[].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).