Webhooks#

Statt abzufragen, abonnierst du und lässt die Änderungen zu dir kommen.

Registrier einen Endpunkt auf der Ressource webhook_endpoints — sie braucht webhook_endpoints.write; die genauen Routen stehen in der API-Referenz. Das Anlegen gibt einmalig ein Signatur-Geheimnis (whsec_…) zurück. Speichere es sofort; es lässt sich nicht wiederherstellen.

Ereignisse sind schlank#

Der Body ist bewusst klein:

{
  "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-…/"
  }
}

Hol den aktuellen Zustand über related_object.url, statt darauf zu vertrauen, dass das Ereignis ihn mitbringt. So bleibst du richtig, wenn mehrere Änderungen dicht aufeinander folgen.

Die Zustellung erfolgt mindestens einmal und möglicherweise ausser der Reihe, dein Handler muss also idempotent sein — entdopple über die id des Ereignisses.

Jede Zustellung prüfen#

Jedes POST trägt einen Webhook-Signature-Header (dazu Webhook-Id):

Webhook-Signature: t=1753178651,v1=5f3a…

So prüfst du:

  1. Berechne HMAC-SHA256(secret, "{t}.{raw_body}") neu — über die rohen Body-Bytes, vor jedem JSON-Parsen und erneuten Serialisieren.

  2. Vergleich sie in konstanter Zeit mit einem v1=-Wert.

  3. Weise ab, wenn t ausserhalb einer Toleranz von ~5 Minuten liegt.

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)

Wichtig

Während einer Rotation des Geheimnisses trägt der Header mehrere v1=-Signaturen — alte und neue — für ein Überlappungsfenster. Akzeptier eine Übereinstimmung mit irgendeiner davon, sonst gehen bei Rotationen Zustellungen verloren.

Nach einem Ausfall aufholen#

Fehlgeschlagene Zustellungen werden mit exponentiell wachsendem Abstand wiederholt, und der Verlauf der ausgelieferten Ereignisse bleibt erhalten. War dein Endpunkt ausgefallen, kannst du aus diesem Verlauf abgleichen, statt die Fachressourcen erneut abzufragen.

Den ganzen Lebenszyklus abonnieren#

Der häufigste Anbindungsfehler hier ist, nur den guten Fall zu abonnieren und eine tote Ausschreibung in der eigenen Kopie am Leben zu lassen.

Ereignisse der Schichtbörse kommen paarweise als open_shift.* / swap.* — ein Angebot wird als zwei Ressourcen veröffentlicht (open_shifts/ für Vakanzen, swaps/ für Abgaben und Tausche), und der Ereignistyp sagt dir, welche Sammlung du holen musst. Beide führen die Endübergänge ebenso wie die Vergaben:

  • open_shift.claimed, open_shift.awarded, swap.approved

  • open_shift.rejected, swap.rejected, *.withdrawn, *.auto_rejected, *.expired

Abonnier auch die Endübergänge, nicht nur die Vergaben.

Die vollständige Liste der abonnierbaren Ereignistypen steht bei der Ressource webhook_endpoints in der API-Referenz.