Webhooks
Roster pushes every subscribed event to your endpoint as an HTTP POST. The
push channel and the pull feed (GET /v1/events) carry exactly the same
envelope and fields — one contract, two channels.
The envelope
{
"event_id": "evt_...",
"event_type": "enrollment.activated",
"occurred_at": "2026-08-31T12:00:00.000Z",
"church_id": "ch_...",
"payload": { "enrollment_id": "en_..." }
}
payload carries only the fields documented for each event type — never
donor PII.
Verifying the signature
Every delivery is signed with your endpoint's whsec_ secret:
Roster-Signature: t={unix_seconds},v1={hex hmac_sha256("{t}.{raw_body}", secret)}
Verify before trusting anything:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header, nowUnixSeconds) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!match) return false;
const t = Number(match[1]);
// Reject old timestamps even with a valid HMAC (replay protection).
if (Math.abs(nowUnixSeconds - t) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(match[2], "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
Compute the HMAC over the raw request body — parse the JSON only after the signature checks out.
Delivery semantics
-
At-least-once. A retry can re-deliver an event you already accepted —
deduplicate by
event_idand treat duplicates as a no-op. - Acknowledge fast. Return any 2xx quickly (enqueue and process async if needed). Anything else, or a timeout, counts as a failed attempt and will be retried a limited number of times.
-
The feed is your safety net. If your endpoint is down past the
retries, nothing is lost: poll
GET /v1/eventswith your stored cursor and you will see every event. Build reconciliation on the feed, use webhooks for latency. -
Inspect deliveries.
GET /v1/webhook_endpoints/{id}/deliverieslists the last 50 attempts with outcome and status code — your first stop when debugging.
Operational tips
- Rotate an endpoint by creating a new one, confirming it receives events, then deleting the old — both stay active in between.
-
Use
POST /v1/webhook_endpoints/{id}/testafter any change on your side; it exercises the exact signing and delivery path of real events.