Webhooks#
Instead of polling, subscribe and let changes reach you.
Register an endpoint on the webhook_endpoints resource — it needs webhook_endpoints.write; the exact routes are in the API Reference. Creation returns a signing secret (whsec_…) once. Store it immediately; it cannot be recovered.
Events are thin#
The body is deliberately small:
{
"id": "evt_0194d1c0-…",
"object": "event",
"type": "shift.published",
"created": "2026-07-22T10:04:11Z",
"related_object": {
"id": "0194d1c0-…",
"url": "https://app.shiftavo.com/api/public/v1/shifts/0194d1c0-…/"
}
}
Fetch the current state from related_object.url rather than trusting the event to carry it. That keeps you correct when several changes land close together.
Delivery is at-least-once and possibly out of order, so your handler must be idempotent — dedupe on the event id.
Verify every delivery#
Each POST carries a Webhook-Signature header (plus Webhook-Id):
Webhook-Signature: t=1753178651,v1=5f3a…
To verify:
Recompute
HMAC-SHA256(secret, "{t}.{raw_body}")— over the raw body bytes, before any JSON parsing or re-serialization.Constant-time-compare it against a
v1=value.Reject if
tis outside a ~5-minute tolerance.
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(
secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
).hexdigest()
# A rotation sends several v1= values — accept a match against any of them.
signatures = [v for k, v in (p.split("=", 1) for p in header.split(",")) if k == "v1"]
return any(hmac.compare_digest(expected, sig) for sig in signatures)
Important
During a secret rotation the header carries multiple v1= signatures — old and new — for an overlap window. Accept a match against any of them, or rotations will drop deliveries.
Catching up after an outage#
Failed deliveries are retried with exponential backoff, and the dispatched-event history is retained. If your endpoint was down you can reconcile from that history instead of re-polling the domain resources.
Subscribe to the whole lifecycle#
The commonest integration bug here is subscribing only to the happy path and leaving a dead posting live in your copy.
Marketplace events come in open_shift.* / swap.* pairs — one offer is published as two resources (open_shifts/ for vacancies, swaps/ for give-aways and swaps), and the event type tells you which collection to fetch. Both carry the terminal transitions as well as the awards:
open_shift.claimed,open_shift.awarded,swap.approvedopen_shift.rejected,swap.rejected,*.withdrawn,*.auto_rejected,*.expired
Subscribe to the terminal transitions too, not just the awards.
The full list of event types you can subscribe to is on the webhook_endpoints resource in the API Reference.