Rather than polling, give us a URL and we’ll POST a signed message when something changes. Set them up in Settings → API → Webhooks — deliberately not over the API, so a leaked key cannot add a destination of its own.
{
"id": "evt_...",
"type": "member.updated",
"created_at": "2026-08-29T14:03:11Z",
"org": { "id": 42, "slug": "acme-district" },
"origin": { "kind": "sync", "source": "acme-membership" },
"data": { "object": "member", "id": 1489 }
}data is built by the same serializer as the REST response and honours the endpoint’s data classes — so an endpoint without them sees names and roles only.origin says where the change came from, and is null for something a person did in the app.
We follow Standard Webhooks, so an off-the-shelf verifier works. Three headers:
webhook-id: evt_...
webhook-timestamp: 1756483391
webhook-signature: v1,<base64 HMAC-SHA256 of "{id}.{timestamp}.{body}">During a secret rotation the header carries several space-separated signatures — accept the message if any of them verifies, and you can roll without an outage.
id is stable across retries, so dedupe on it.410 Gone switches the endpoint off straight away. So do 20 consecutive failures, and we email the workspace Owner.Bulk operations send one summary, not one message per row. A 500-record sync sends a single sync.run.completed carrying the counters — read the run for the detail. The same applies to CSV imports, full-roster deactivation, auto-assigned lodging, and “no-shows forfeit seats”. This is deliberate: per-row messages would mean thousands of deliveries from one click, and your endpoint would be the first casualty.
Also worth knowing: un-checking someone in has no event type in v1, and neither does a registration being demoted by a capacity change. Promotion off the waitlist does — registration.promoted.