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:

  1. Recompute HMAC-SHA256(secret, "{t}.{raw_body}") — over the raw body bytes, before any JSON parsing or re-serialization.

  2. Constant-time-compare it against a v1= value.

  3. Reject if t is 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.approved

  • open_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.