Ricevere i webhook
Un webhook informa il tuo sistema di una modifica in Sursa nel momento in cui avviene: una scheda viene pubblicata,
verificata o archiviata, una domanda è rimasta senza risposta. Sursa invia un POST HTTPS con l’evento a un indirizzo
che registri tu. Questa guida costruisce un ricevitore che funziona correttamente anche con nuovi tentativi, ordine
alterato e duplicati.
Per sapere cosa significa e cosa contiene ogni evento, vedi la panoramica dei webhook e il catalogo degli eventi nel riferimento API.
1. Registrare un webhook
Un amministratore registra i webhook in Sursa in Administration → Integrations → Webhooks and API (amministrazione,
integrazioni, webhook), oppure con POST /v1/webhooks e una chiave con il ruolo
admin e l’ambito webhooks:manage:
curl -X POST https://api.sursa.com/v1/webhooks \
-H "Authorization: Bearer $SURSA_ADMIN_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/sursa",
"description": "Sync verified cards to the help center",
"events": ["card.published", "card.updated", "card.verified", "card.archived"]
}'La risposta contiene il segreto di firma in secret, che inizia con whsec_. Viene mostrato una sola volta.
Conservalo insieme al tuo ricevitore, per esempio in una variabile d’ambiente SURSA_WEBHOOK_SECRET.
L’indirizzo deve usare https e risolversi in indirizzi IP pubblici: gli indirizzi di reti private, di loopback e di
altre reti riservate vengono rifiutati alla registrazione e a ogni consegna. Per sviluppare sul tuo computer, esponi il
ricevitore locale tramite un servizio di tunneling che gli assegni un indirizzo HTTPS pubblico.
Invia un evento di test in qualsiasi momento con POST /v1/webhooks/{webhook_id}/test.
La risposta mostra cosa ha risposto il tuo ricevitore.
2. Com’è fatta una consegna
POST /sursa HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
webhook-id: evt_0123456789abcdef0123456789abcdef
webhook-timestamp: 1791194400
webhook-signature: v1,l1LLl/aEl4dQq7Geb383KbyNeZgs7VZpiw6iuoJgunI=
X-KB-Signature: t=1791194400,v1=82aa6b27378d89d7d880d17c3fe96268598443ae0e64d059c0c41e94e5d3ffbe
X-KB-Event-Id: evt_0123456789abcdef0123456789abcdef
X-KB-Event: card.verified
X-KB-Delivery: 99812
X-KB-Attempt: 1
X-KB-Sequence: 1042
{"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"api_version":"1.60.0","sequence":1042,"object":{"type":"card","id":"c1"},"payload":"full","data":{"card":{"id":"c1"}},"test":false}
| Intestazione | Significato |
|---|---|
webhook-id | L’ID dell’evento. È lo stesso a ogni nuovo tentativo. Usalo per scartare i duplicati. |
webhook-timestamp | Quando è stato firmato questo tentativo, in secondi Unix. |
webhook-signature | La firma nel formato Standard Webhooks. |
X-KB-Signature | La stessa consegna firmata nel formato precedente di Sursa. |
X-KB-Event | Il tipo di evento. |
X-KB-Attempt | Il numero del tentativo, a partire da 1. |
X-KB-Sequence | La posizione di questa consegna nell’ordine del webhook. |
I nomi delle intestazioni non distinguono maiuscole e minuscole; il tuo framework potrebbe mostrarli come
X-Kb-Signature o x-kb-signature.
Il corpo è l’evento. Il suo data dipende dal tipo. Un webhook impostato sul payload thin riceve l’evento senza
data; in quel caso leggi l’oggetto tramite l’API con la tua chiave.
3. Verificare la firma
Controlla la firma di ogni consegna prima di fidarti. Entrambi i formati firmano i byte esatti del corpo, quindi leggi il corpo grezzo prima di qualsiasi analisi del JSON: serializzare di nuovo il JSON cambia i byte e fa fallire la verifica.
| Formato | Stringa firmata | Chiave | Firma |
|---|---|---|---|
Standard Webhooks (webhook-signature) | <webhook-id>.<webhook-timestamp>.<body> | La parte del segreto dopo whsec_, decodificata da base64 | v1, + base64 di HMAC-SHA256 |
Formato precedente (X-KB-Signature) | <t>.<body> | L’intera stringa del segreto, come testo | v1= + esadecimale di HMAC-SHA256 |
Basta verificarne una. Preferisci l’intestazione Standard Webhooks: la stessa verifica è disponibile in molte librerie
già pronte. Durante una rotazione del segreto, ogni intestazione porta una firma per ogni segreto valido (separate da
spazi in webhook-signature, v1= ripetuto in X-KB-Signature). Una consegna è valida se una qualsiasi di esse
corrisponde.
Rifiuta anche un timestamp che si discosta di più di 5 minuti dal tuo orologio. Così nessuno può riprodurre più tardi una consegna registrata.
# Check a saved delivery by hand: body.json holds the raw body, the values come from its headers.
SECRET="whsec_…"; WEBHOOK_ID="evt_…"; TIMESTAMP="1791194400"
# Standard Webhooks: compare with the value after "v1," in webhook-signature
KEY=$(printf '%s' "${SECRET#whsec_}" | tr '_-' '/+')
while [ $(( ${#KEY} % 4 )) -ne 0 ]; do KEY="$KEY="; done
KEY_HEX=$(printf '%s' "$KEY" | openssl base64 -d -A | xxd -p | tr -d '\n')
{ printf '%s.%s.' "$WEBHOOK_ID" "$TIMESTAMP"; cat body.json; } \
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY_HEX" -binary | openssl base64 -A
# Earlier format: compare with the value after "v1=" in X-KB-Signature
{ printf '%s.' "$TIMESTAMP"; cat body.json; } | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1Testare la verifica delle firme
Esegui il tuo codice con questi valori prima di collegarlo alle consegne reali. Con l’orologio impostato sul timestamp, entrambe le verifiche devono riuscire, e devono fallire quando cambia un byte del corpo.
| Input | Valore |
|---|---|
| Segreto | whsec_test_secret_0123456789abcdef |
webhook-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp e t | 1791194400 |
| Corpo | {"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"data":{"card":{"id":"c1"}}} |
webhook-signature | v1,+xxWSXH6weQJ/MVg4+N78a1Dj4y/AQ4p94+PRNzpECg= |
X-KB-Signature | t=1791194400,v1=ce26a19cb63cd3ae620217bf7688e1df1044fa7d59d9ae148d215f0292b768dc |
4. Rispondere subito, lavorare dopo
Sursa attende la tua risposta fino a 10 secondi. Qualsiasi stato 2xx significa che la consegna è riuscita;
qualsiasi altra cosa, compresi un reindirizzamento, un timeout o una connessione chiusa, significa che non è riuscita e
verrà ritentata.
Quindi il ricevitore dovrebbe solo verificare, registrare l’evento e rispondere. Il lavoro vero e proprio va fatto dopo, dalla tua coda.
// Node.js 20+, no framework. verifyStandard and verifyLegacy come from step 3.
import http from "node:http";
const SECRET = process.env.SURSA_WEBHOOK_SECRET;
http
.createServer(async (req, res) => {
const parts = [];
for await (const part of req) parts.push(part);
const body = Buffer.concat(parts); // the raw bytes, before any JSON parsing
const valid = req.headers["webhook-signature"]
? verifyStandard(SECRET, req.headers, body)
: verifyLegacy(SECRET, req.headers["x-kb-signature"], body);
if (!valid) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(body);
await saveEventOnce(event); // insert into your table or queue, keyed by event.id; ignore a duplicate
res.writeHead(204).end();
})
.listen(3000);Rispondi 2xx solo dopo aver salvato l’evento in modo sicuro. Se rispondi prima e poi il processo si arresta, per te
l’evento è perso, perché Sursa lo ha già considerato consegnato.
5. Nuovi tentativi
Una consegna non riuscita viene ritentata con pause sempre più lunghe: dopo circa 1 minuto, 5 minuti, 30 minuti, 2 ore, 5 ore, 10 ore, 24 ore e 24 ore, nove tentativi in quasi tre giorni. Ogni pausa varia fino al 20% perché i nuovi tentativi di molti webhook non arrivino nello stesso momento.
- Una risposta
429o503conRetry-Aftersposta il tentativo successivo almeno a quel momento, fino a 24 ore dopo. Usala quando il tuo sistema è sovraccarico o in manutenzione. - Una risposta
410 Gonedisattiva il webhook: inviala solo quando non vuoi più consegne a quell’indirizzo. - Dopo 10 tentativi consecutivi non riusciti, il webhook viene messo in pausa e gli amministratori
dell’organizzazione vengono avvisati. Gli eventi continuano ad accumularsi e, quando il webhook viene ripreso,
nell’interfaccia di amministrazione o con
POST /v1/webhooks/{webhook_id}/resume, vengono consegnati in ordine. - Dopo l’ultimo tentativo, una consegna viene contrassegnata come
exhausted. Puoi inviarla di nuovo manualmente.
Le consegne di un webhook, con ogni tentativo, il codice della tua risposta e i relativi tempi, sono elencate da
GET /v1/webhooks/{webhook_id}/deliveries e nell’interfaccia di
amministrazione. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver
ne invia di nuovo una. Una nuova consegna ha lo stesso webhook-id.
6. Ordine e duplicati
- Ordine. Le consegne a uno stesso webhook avvengono rigorosamente una dopo l’altra nell’ordine di
sequence. Mentre una consegna viene ritentata, quelle successive attendono. Webhook diversi non si attendono a vicenda. - Duplicati. Lo stesso evento può arrivare più di una volta, per esempio quando la tua risposta è andata persa
sulla via del ritorno. Conserva il
webhook-id(che è ancheidnel corpo) di ogni evento che elabori e ignora quelli che hai già visto. - Ragionare per stati. Per le modifiche allo stesso oggetto, confronta
created_atosequencecon ciò che hai salvato e ignora un evento più vecchio dello stato che hai già. Nel dubbio, leggi l’oggetto attuale tramite l’API: l’evento ti dice che qualcosa è cambiato, l’API ti dice com’è adesso.
7. Riconciliare con il feed degli eventi
Ogni evento viene anche conservato per 30 giorni nel feed degli eventi, che un webhook lo abbia sottoscritto o no.
Se il tuo ricevitore è rimasto fermo più a lungo di quanto coprono i nuovi tentativi, o se vuoi controllare che non sia
sfuggito nulla, leggi il feed
con una chiave che ha il ruolo admin e l’ambito webhooks:manage:
curl -G https://api.sursa.com/v1/webhook-events \
-H "Authorization: Bearer $SURSA_API_KEY" \
--data-urlencode "type=card.*" \
--data-urlencode "created_since=2026-10-20T00:00:00Z" \
--data-urlencode "limit=200"Un evento nel feed ha lo stesso id e la stessa forma che ha in una consegna, senza i campi della consegna
sequence, payload e test. Poiché il tuo ricevitore ignora gli eventi già visti, puoi eseguire la riconciliazione
tutte le volte che vuoi, per esempio una volta all’ora.
GET /v1/webhook-events/{event_id} legge un evento in base al suo ID.
8. Ruotare il segreto
POST /v1/webhooks/{webhook_id}/rotate-secret emette un nuovo segreto e
mantiene valido il precedente per un periodo di sovrapposizione, 24 ore per impostazione predefinita e fino a 7 giorni.
Durante la sovrapposizione, ogni consegna viene firmata con entrambi i segreti.
- Esegui la rotazione e conserva il nuovo segreto accanto a quello vecchio nel tuo ricevitore.
- Accetta una consegna se la verifica riesce con uno qualsiasi dei due segreti.
- Dopo la sovrapposizione, rimuovi il vecchio segreto.
Se il vecchio segreto è stato divulgato, termina subito la sovrapposizione con
POST /v1/webhooks/{webhook_id}/revoke-previous-secret, oppure
esegui la rotazione con "overlap_hours": 0.
Checklist
- Verifica una delle firme sul corpo grezzo e controlla il timestamp.
- Salva l’evento, identificato da
webhook-id, poi rispondi2xxentro 10 secondi. - Ignora i duplicati e gli eventi superati; quando conta, leggi l’oggetto attuale tramite l’API.
- Rispondi
429o503conRetry-Afterquando hai bisogno che Sursa rallenti; non rispondere mai410per errore. - Ignora i tipi di evento e i campi che non conosci.
- Riconcilia regolarmente con il feed degli eventi.