The Skills Fest

Webhooks

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.

The message

{
  "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.

Checking the signature

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.

Delivery

  • At-least-once. id is stable across retries, so dedupe on it.
  • Any 2xx within 10 seconds is success. Retries back off: immediately, 30s, 5m, 30m, 2h, 6h, 24h — then we stop.
  • 410 Gone switches the endpoint off straight away. So do 20 consecutive failures, and we email the workspace Owner.
  • Your URL must be public https. Private, loopback and link-local addresses are refused, at setup and again at every delivery, and we never follow redirects.

What does NOT emit

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.