Webhook#

Invece di interrogare ciclicamente, iscriviti e lascia che le modifiche ti raggiungano.

Registra un endpoint sulla risorsa webhook_endpoints — richiede webhook_endpoints.write; i percorsi esatti sono nella Referenza API. La creazione restituisce una sola volta un segreto di firma (whsec_…). Conservalo subito; non è recuperabile.

Gli eventi sono leggeri#

Il corpo è deliberatamente piccolo:

{
  "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 è un percorso — uniscilo all”api_base del tuo token. related_object.object indica il tipo di risorsa, così un gestore misto può ramificare senza analizzare l’URL.

Recupera lo stato attuale da quell’URL invece di fidarti che sia l’evento a portarlo. Così resti corretto quando diverse modifiche arrivano ravvicinate. Anche un 404 lì è una risposta — per esempio, il record è stato eliminato, oppure si tratta di un turno non pubblicato, che la risorsa shifts non restituisce mai.

La consegna è almeno una volta e possibilmente fuori ordine, quindi il tuo handler deve essere idempotente: deduplica sull”id dell’evento.

Verifica ogni consegna#

Ogni POST porta un header Webhook-Signature (più Webhook-Id):

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

Per verificare:

  1. Ricalcola HMAC-SHA256(secret, "{t}.{raw_body}") — sui byte grezzi del corpo, prima di qualsiasi analisi JSON o nuova serializzazione.

  2. Confrontalo a tempo costante con un valore v1=.

  3. Rifiuta se t è fuori da una tolleranza di ~5 minuti.

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)

Importante

Durante una rigenerazione del segreto l’header porta più firme v1= — vecchia e nuova — per una finestra di sovrapposizione. Accetta una corrispondenza con una qualsiasi di esse, altrimenti le rigenerazioni faranno perdere delle consegne.

Recuperare dopo un’interruzione#

Una consegna riesce quando il tuo endpoint risponde 2xx entro 10 secondi. Una consegna che fallisce per un errore di rete, un timeout, un 408, un 429 o un 5xx viene ripetuta con backoff esponenziale — rispettando il Retry-After in secondi di un 429 — fino a un massimo di otto tentativi in totale. Qualsiasi altro codice di stato fa fallire subito la consegna, e un 410 disabilita anche l’endpoint.

La cronologia degli eventi inviati viene conservata nella risorsa in sola lettura webhook_log (webhook_log.read), e webhook_endpoints/<id>/deliveries/ mostra lo stato e i tentativi di ogni consegna. Se il tuo endpoint era fuori servizio puoi riconciliare da quella cronologia invece di interrogare di nuovo le risorse di dominio.

Iscriviti a tutto il ciclo di vita#

L’errore di integrazione più comune qui è iscriversi solo al percorso ideale e lasciare nella tua copia una pubblicazione morta ancora attiva.

Gli eventi del mercato arrivano in coppie open_shift.* / swap.* — una stessa offerta è pubblicata come due risorse (open_shifts/ per i posti vacanti, swaps/ per cessioni e scambi) e il tipo di evento ti dice quale collezione recuperare. Entrambe portano sia le transizioni finali sia le attribuzioni:

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

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

Iscriviti anche alle transizioni finali, non solo alle attribuzioni. Oltre che per un ritiro manuale, *.withdrawn scatta quando una modifica al turno fa cadere un’offerta attiva — per esempio quando il turno viene riprogrammato, eliminato o se ne annulla la pubblicazione — oppure quando l’azienda disattiva il mercato dei turni o quel tipo di offerta. *.expired scatta quando un’offerta si esaurisce — per esempio perché la sua scadenza è passata senza risposte in attesa, oppure perché il suo turno è terminato, è stato annullato o ha avuto le ore approvate. Recupera l’offerta per vedere come si è conclusa: il suo status è expired, oppure completed se qualcuno era già stato scelto. *.auto_rejected è ancora accettato in un’iscrizione, ma non viene più inviato.

Elenca i tipi che vuoi nel campo enabled_events dell’endpoint, oppure lascialo vuoto per ricevere tutti i tipi. Un tipo sconosciuto è un 400. Ecco tutti i tipi, raggruppati per la risorsa a cui punta il loro related_object:

related_object

Tipi di evento

shift

shift.created, shift.updated, shift.published, shift.unpublished, shift.cancelled, shift.deleted, assignment.accepted, assignment.declined

event (eventi generali)

event.created, event.updated, event.published, event.unpublished, event.cancelled, event.deleted

open_shift

open_shift.claimed, open_shift.awarded, open_shift.rejected, open_shift.withdrawn, open_shift.expired

swap

swap.approved, swap.rejected, swap.withdrawn, swap.expired

leave_request

leave_request.confirmed, leave_request.declined, leave_request.extended, leave_request.revoked, leave_request.withdrawn

timesheet

timesheet.approved, timesheet.unapproved, assignment.reset

leave_request.withdrawn significa che chi ha fatto la richiesta l’ha ritirata; leave_request.revoked significa che l’ha chiusa qualcun altro.

Le consegne in coda si interrompono mentre un endpoint è disabilitato. Un tentativo fallito tardivo non può sostituire un successo registrato e una risposta 410 da un vecchio URL non disabilita l’URL sostitutivo. Un’attività ripetuta non invia nuovamente le consegne concluse. Un controllo orario invia tutto ciò che è ancora in sospeso quando arriva il momento — dopo che hai riattivato l’endpoint, dopo che il piano dell’azienda include di nuovo l’API, oppure quando un tentativo in coda è andato perso.