Recevoir des webhooks
Un webhook informe votre système d’une modification dans Sursa au moment où elle se produit : une fiche est publiée,
validée ou archivée, une question est restée sans réponse. Sursa envoie un POST HTTPS contenant l’événement à une
adresse que vous enregistrez. Ce guide construit un récepteur qui reste correct face aux nouvelles tentatives, aux
changements d’ordre et aux doublons.
Pour la signification et le contenu de chaque événement, consultez la vue d’ensemble des webhooks et le catalogue des événements dans la référence de l’API.
1. Enregistrer un webhook
Un administrateur enregistre les webhooks dans Sursa sous Administration → Integrations → Webhooks and API, ou avec
POST /v1/webhooks et une clé dotée du rôle admin et de la portée
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 réponse contient le secret de signature dans secret, qui commence par whsec_. Il n’est affiché qu’une fois.
Conservez-le avec votre récepteur, par exemple dans une variable d’environnement SURSA_WEBHOOK_SECRET.
L’URL de réception doit utiliser https et pointer vers des adresses IP publiques : les adresses des réseaux privés,
de bouclage et des autres réseaux réservés sont refusées lors de l’enregistrement et à chaque livraison. Pour
développer sur votre propre ordinateur, exposez votre récepteur local au moyen d’un service de tunnel qui lui donne une
adresse HTTPS publique.
Envoyez à tout moment un événement de test avec POST /v1/webhooks/{webhook_id}/test.
La réponse indique ce que votre récepteur a répondu.
2. À quoi ressemble une livraison
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}
| En-tête | Signification |
|---|---|
webhook-id | L’identifiant de l’événement. Identique à chaque nouvelle tentative. Utilisez-le pour écarter les doublons. |
webhook-timestamp | Le moment où cette tentative a été signée, en secondes Unix. |
webhook-signature | La signature au format Standard Webhooks. |
X-KB-Signature | La même livraison signée dans l’ancien format de Sursa. |
X-KB-Event | Le type d’événement. |
X-KB-Attempt | Le numéro de la tentative, à partir de 1. |
X-KB-Sequence | La position de cette livraison dans l’ordre du webhook. |
Les noms d’en-têtes ne sont pas sensibles à la casse ; votre framework peut les afficher sous la forme
X-Kb-Signature ou x-kb-signature.
Le corps est l’événement. Son data dépend du type. Un webhook réglé sur la charge utile thin reçoit l’événement
sans data ; lisez alors l’objet par l’API avec votre propre clé.
3. Vérifier la signature
Vérifiez la signature de chaque livraison avant de vous y fier. Les deux formats signent les octets exacts du corps : lisez donc le corps brut avant toute analyse du JSON, car resérialiser le JSON change les octets et fait échouer la vérification.
| Format | Chaîne signée | Clé | Signature |
|---|---|---|---|
Standard Webhooks (webhook-signature) | <webhook-id>.<webhook-timestamp>.<body> | La partie du secret qui suit whsec_, décodée en base64 | v1, + base64 du HMAC-SHA256 |
Ancien format (X-KB-Signature) | <t>.<body> | La chaîne entière du secret, en tant que texte | v1= + hexadécimal du HMAC-SHA256 |
Il suffit de vérifier l’une des deux. Préférez l’en-tête Standard Webhooks : la même vérification existe dans de
nombreuses bibliothèques prêtes à l’emploi. Pendant un renouvellement du secret, chaque en-tête porte une signature par
secret valide (séparées par des espaces dans webhook-signature, v1= répété dans X-KB-Signature). Une livraison
est valide si l’une d’elles correspond.
Rejetez aussi un horodatage qui s’écarte de plus de 5 minutes de votre horloge. Cela empêche quelqu’un de rejouer plus tard une livraison enregistrée.
# 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' ' -f1Tester votre vérification
Exécutez votre code sur ces valeurs avant de le brancher sur de vraies livraisons. Avec l’horloge réglée sur l’horodatage, les deux vérifications doivent réussir, et elles doivent échouer dès qu’un octet du corps change.
| Entrée | Valeur |
|---|---|
| Secret | whsec_test_secret_0123456789abcdef |
webhook-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp et t | 1791194400 |
| Corps | {"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. Répondre vite, traiter ensuite
Sursa attend votre réponse jusqu’à 10 secondes. Tout statut 2xx signifie que la livraison a réussi ; toute autre
issue, y compris une redirection, un délai dépassé ou une connexion fermée, signifie qu’elle a échoué et qu’elle sera
relancée.
Le récepteur doit donc seulement vérifier, enregistrer l’événement et répondre. Faites le vrai travail ensuite, depuis votre propre file d’attente.
// 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);Ne répondez 2xx qu’une fois l’événement enregistré en lieu sûr. Si vous répondez d’abord et que votre processus
plante ensuite, l’événement est perdu pour vous, car Sursa l’a déjà compté comme livré.
5. Nouvelles tentatives
Une livraison en échec est relancée avec des pauses de plus en plus longues : au bout d’environ 1 minute, 5 minutes, 30 minutes, 2 heures, 5 heures, 10 heures, 24 heures et 24 heures, soit neuf tentatives sur près de trois jours. Chaque pause varie d’au plus 20 % pour que les nouvelles tentatives de nombreux webhooks n’arrivent pas au même moment.
- Une réponse
429ou503avecRetry-Afterrepousse la tentative suivante au moins à ce moment, jusqu’à 24 heures plus tard. Utilisez-la quand votre système est surchargé ou en maintenance. - Une réponse
410 Gonedésactive le webhook : ne l’envoyez que si vous ne voulez plus aucune livraison à cette adresse. - Après 10 tentatives en échec d’affilée, le webhook est mis en pause et les administrateurs de l’organisation sont
prévenus. Les événements continuent de s’accumuler et, à la reprise du webhook, dans l’interface d’administration
ou avec
POST /v1/webhooks/{webhook_id}/resume, ils sont livrés dans l’ordre. - Après la dernière tentative, une livraison est marquée
exhausted. Vous pouvez la renvoyer à la main.
Les livraisons d’un webhook, avec chaque tentative, le code de votre réponse et sa durée, sont listées par
GET /v1/webhooks/{webhook_id}/deliveries et dans l’interface
d’administration. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver
en renvoie une. Une nouvelle livraison a le même webhook-id.
6. Ordre et doublons
- Ordre. Les livraisons vers un même webhook se suivent strictement, dans l’ordre de
sequence. Pendant qu’une livraison est relancée, les suivantes attendent. Des webhooks différents ne s’attendent pas les uns les autres. - Doublons. Le même événement peut arriver plusieurs fois, par exemple quand votre réponse s’est perdue en chemin.
Conservez le
webhook-id(c’est aussi l'iddu corps) de chaque événement traité, et ignorez celui que vous avez déjà vu. - Raisonner en états. Pour les modifications d’un même objet, comparez
created_atousequenceavec ce que vous avez enregistré, et ignorez un événement plus ancien que l’état dont vous disposez déjà. En cas de doute, lisez l’objet actuel par l’API : l’événement vous dit que quelque chose a changé, l’API vous dit ce qu’il est maintenant.
7. Faire le rapprochement avec le flux d’événements
Chaque événement est aussi conservé pendant 30 jours dans le flux d’événements, qu’un webhook y soit abonné ou non.
Si votre récepteur a été indisponible plus longtemps que ne le couvrent les nouvelles tentatives, ou si vous voulez
vérifier que rien n’a été manqué, lisez le flux avec une clé dotée du rôle admin et de la portée 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 événement du flux a le même id et la même forme que dans une livraison, sans les champs propres à la livraison
sequence, payload et test. Comme votre récepteur ignore les événements déjà vus, vous pouvez lancer le
rapprochement aussi souvent que vous le souhaitez, par exemple une fois par heure.
GET /v1/webhook-events/{event_id} lit un événement à partir de son
identifiant.
8. Renouveler le secret
POST /v1/webhooks/{webhook_id}/rotate-secret émet un nouveau secret et
garde le précédent valide pendant une période de chevauchement, de 24 heures par défaut et jusqu’à 7 jours. Pendant le
chevauchement, chaque livraison est signée avec les deux secrets.
- Renouvelez le secret et conservez le nouveau à côté de l’ancien dans votre récepteur.
- Acceptez une livraison si elle se vérifie avec l’un ou l’autre secret.
- Après le chevauchement, supprimez l’ancien secret.
Si l’ancien secret a fuité, mettez fin au chevauchement immédiatement avec
POST /v1/webhooks/{webhook_id}/revoke-previous-secret, ou
renouvelez le secret avec "overlap_hours": 0.
Liste de contrôle
- Vérifiez l’une des signatures sur le corps brut et contrôlez l’horodatage.
- Enregistrez l’événement, identifié par
webhook-id, puis répondez2xxen moins de 10 secondes. - Ignorez les doublons et les événements périmés ; lisez l’objet actuel par l’API quand c’est important.
- Répondez
429ou503avecRetry-Afterquand vous avez besoin que Sursa ralentisse ; ne répondez jamais410par erreur. - Ignorez les types d’événements et les champs que vous ne connaissez pas.
- Faites régulièrement le rapprochement avec le flux d’événements.