Event delivery to your endpoint
Roster POSTs each subscribed event to your registered endpoint,
signed with the endpoint's whsec_ secret:
Roster-Signature: t={unix_seconds},v1={hex hmac_sha256("{t}.{raw_body}", secret)}
The HMAC key is the whole secret string, whsec_ prefix included,
as UTF-8 bytes: do not strip the prefix or decode the rest. Verify by
recomputing the HMAC over {t}.{raw body} and comparing in constant
time; reject the delivery when t is more than 300 seconds away
from your clock, in either direction, even when the HMAC matches.
Every delivery, the test one included, identifies itself with
User-Agent: Roster-Webhooks/1.0; allow it if a firewall filters
the calls to your receiver. It also names the endpoint it was sent
to in Roster-Webhook-Endpoint, the we_ id, so two endpoints on
the same URL can be told apart. The signature covers only t and
the raw body, not the headers: use the id to pick the endpoint's
secret, and trust it only once the signature verifies with that
secret.
Answer with any 2xx within 3 seconds. A slower answer, any other
status, a redirect (Roster never follows redirects) or a failed
connection counts as a failed attempt.
Delivery is at-least-once — deduplicate by event_id. A retry
goes only to the endpoints that have not accepted the event yet, so
a failing endpoint does not multiply deliveries to the others. An
endpoint that answered 2xx can still receive a duplicate, for
example when its answer did not reach Roster in time.
Retries come in rounds. The first round starts right away, with
attempts about one minute apart: 5 in production and 100 in the
sandbox. If every attempt in a round fails, a new round starts within
about an hour. No new round starts later than 24 hours after the
event was first queued for delivery; after that, automatic retries
stop.
Delivery order is not guaranteed: a retried or delayed notice can
arrive after a newer one, so do not use arrival order as business
order. Use GET /v1/events for activity changes or
GET /v1/financial_events for donation, donor and refund updates
when recovering missed notices.
Headers
- Type: stringRoster
- Signature requiredt={unix_seconds},v1={hmac_sha256("{t}.{body}", secret)}, keyed with the wholewhsec_secret as UTF-8 — reject atmore than 300 seconds away from your clock, in either direction. The header carries onetand one or morev1, comma-separated: today exactly one, and you accept the delivery when anyv1matches, so a delivery signed with more than one secret (for example during a secret rotation) still verifies. - Type: stringRoster
- Webhook - Endpoint requiredId of the endpoint this delivery was sent to (
we_...), asPOST /v1/webhook_endpointsreturned it. It is not a secret, and it is not covered byRoster-Signature: use it to pick that endpoint's secret, and trust it only after the signature verifies with that secret.
Body
- Type:
- churchType: string
_id requiredRoster identifier of the church this record belongs to.
- entityType: string
_id requiredIdentifier of the record that changed; the enrollment ID for
enrollment.*, the donor session ID forpartner_session.*and the church ID forchurch.*. - eventType: string
_id requiredGlobally unique — the deduplication key.
- eventType: enum
_type requiredEvent types visible to the partner (feed and webhooks).
church.onboardedarrives once per church, the first time the church becomes ready for paycheck giving: the same momentpayout_readyturnstrueonGET /v1/churches/{id}andGET /v1/churches. If the church later stops being ready, no event is sent; readpayout_ready. Churches that were already ready before Roster started sendingchurch.onboardeddo not receive it. Today it is sent in the sandbox only; in production, readpayout_ready.values- partner
_session .created - partner
_session .opened - partner
_session .completed - partner
_session .expired - church
.created - church
.onboarded - enrollment
.created - enrollment
.activated
- partner
- livemodeType: booleanrequired
truewhen the event comes from production (sk_live_keys),falsewhen it comes from the sandbox (sk_test_keys). When one receiver gets events from both environments, use it to keep them apart. - occurredType: stringFormat: date-time
_at requiredBusiness time of the event.
- payloadType: objectrequired
Field-allowlisted projection, per event type.
Responses
- 200
Return any 2xx to acknowledge the delivery.
{
"event_id": "",
"event_type": "partner_session.created",
"occurred_at": "",
"church_id": "",
"entity_id": "",
"livemode": true,
"payload": {}
}