Webhooks#
Au lieu d’interroger, abonne-toi et laisse les changements venir à toi.
Enregistre un point de terminaison sur la ressource webhook_endpoints — cela requiert webhook_endpoints.write ; les routes exactes figurent dans la référence de l’API. La création renvoie un secret de signature (whsec_…) une seule fois. Stocke-le immédiatement ; il est irrécupérable.
Les événements sont minces#
Le corps est délibérément petit :
{
"id": "0194d1c0-…",
"object": "event",
"type": "shift.published",
"created": "2026-07-22T10:04:11.123456+00:00",
"related_object": {
"id": "0194d1c0-…",
"object": "shift",
"url": "/api/public/v1/shifts/0194d1c0-…/"
}
}
related_object.url est un chemin — combine-le avec l’api_base de ton jeton. related_object.object nomme le type de ressource, si bien qu’un gestionnaire mixte peut brancher sans analyser l’URL.
Récupère l’état courant depuis cette URL plutôt que de croire que l’événement le porte. Cela te garde juste quand plusieurs changements se succèdent de près. Un 404 à cet endroit est aussi une réponse — par exemple, l’enregistrement a été supprimé, ou c’est un shift non publié, que la ressource shifts ne renvoie jamais.
La livraison est au moins une fois et éventuellement dans le désordre ; ton gestionnaire doit donc être idempotent — déduplique sur l’id de l’événement.
Vérifie chaque livraison#
Chaque POST porte un en-tête Webhook-Signature (plus Webhook-Id) :
Webhook-Signature: t=1753178651,v1=5f3a…
Pour vérifier :
Recalcule
HMAC-SHA256(secret, "{t}.{raw_body}")— sur les octets bruts du corps, avant tout parsing JSON ou re-sérialisation.Compare-la en temps constant à une valeur
v1=.Rejette si
tsort d’une tolérance d’environ 5 minutes.
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
Pendant une rotation de secret, l’en-tête porte plusieurs signatures v1= — l’ancienne et la nouvelle — durant une fenêtre de recouvrement. Accepte une correspondance avec n’importe laquelle, sinon les rotations feront perdre des livraisons.
Rattraper après une panne#
Une livraison réussit quand ton point de terminaison répond 2xx en moins de 10 secondes. Une livraison qui échoue sur une erreur réseau, un délai dépassé, un 408, un 429 ou un 5xx est réessayée avec un back-off exponentiel — en respectant le Retry-After d’un 429, en secondes — jusqu’à huit tentatives au total. Tout autre statut fait échouer la livraison immédiatement, et un 410 désactive en plus le point de terminaison.
L’historique des événements envoyés est conservé sur la ressource en lecture seule webhook_log (webhook_log.read), et webhook_endpoints/<id>/deliveries/ montre le statut et les tentatives de chaque livraison. Si ton point de terminaison était hors service, tu peux te resynchroniser à partir de cet historique au lieu d’interroger à nouveau les ressources métier.
Abonne-toi à tout le cycle de vie#
Le bug d’intégration le plus courant ici consiste à ne s’abonner qu’au chemin heureux et à laisser une annonce morte active dans ta copie.
Les événements de la bourse aux shifts vont par paires open_shift.* / swap.* — une offre est publiée sous forme de deux ressources (open_shifts/ pour les postes vacants, swaps/ pour les cessions et les échanges), et le type d’événement t’indique quelle collection récupérer. Les deux portent les transitions terminales aussi bien que les attributions :
open_shift.claimed,open_shift.awarded,swap.approvedopen_shift.rejected,swap.rejected,*.withdrawn,*.expired
Abonne-toi aussi aux transitions terminales, pas seulement aux attributions. Outre un retrait manuel, *.withdrawn se déclenche quand une modification du shift fait tomber une offre active — par exemple quand il est replanifié, dépublié ou supprimé — ou quand l’entreprise désactive la bourse aux shifts ou ce type d’offre. *.expired se déclenche quand une offre arrive à son terme — par exemple parce que son échéance est passée sans réponse en attente, ou que son shift s’est terminé, a été annulé ou a eu ses heures approuvées. Récupère l’offre pour voir comment elle s’est terminée : son status vaut expired, ou completed si quelqu’un a déjà été choisi. *.auto_rejected est encore accepté dans un abonnement mais n’est plus envoyé.
Liste les types que tu veux dans les enabled_events du point de terminaison, ou laisse ce champ vide pour recevoir tous les types. Un type inconnu donne un 400. Tous les types, classés selon la ressource vers laquelle pointe leur related_object :
|
Types d’événements |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
leave_request.withdrawn signifie que le demandeur a retiré sa propre demande ; leave_request.revoked signifie que quelqu’un d’autre y a mis fin.
Les livraisons en attente sont suspendues tant qu’un point de terminaison est désactivé. Un échec tardif ne remplace pas un succès enregistré, et une réponse 410 d’une ancienne URL ne désactive pas l’URL de remplacement. Une tâche répétée ne renvoie pas les livraisons terminées. Une vérification horaire envoie ce qui est encore en attente dès que c’est dû — après que tu as réactivé le point de terminaison, quand le plan de l’entreprise inclut de nouveau l’API, ou quand une tentative en file a été perdue.