Aller au contenu
Sursa Developers

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êteSignification
webhook-idL’identifiant de l’événement. Identique à chaque nouvelle tentative. Utilisez-le pour écarter les doublons.
webhook-timestampLe moment où cette tentative a été signée, en secondes Unix.
webhook-signatureLa signature au format Standard Webhooks.
X-KB-SignatureLa même livraison signée dans l’ancien format de Sursa.
X-KB-EventLe type d’événement.
X-KB-AttemptLe numéro de la tentative, à partir de 1.
X-KB-SequenceLa 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.

FormatChaîne signéeCléSignature
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>La partie du secret qui suit whsec_, décodée en base64v1, + base64 du HMAC-SHA256
Ancien format (X-KB-Signature)<t>.<body>La chaîne entière du secret, en tant que textev1= + 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' ' -f1

Tester 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éeValeur
Secretwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp et t1791194400
Corps{"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"data":{"card":{"id":"c1"}}}
webhook-signaturev1,+xxWSXH6weQJ/MVg4+N78a1Dj4y/AQ4p94+PRNzpECg=
X-KB-Signaturet=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 429 ou 503 avec Retry-After repousse 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 Gone dé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'id du 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_at ou sequence avec 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.

  1. Renouvelez le secret et conservez le nouveau à côté de l’ancien dans votre récepteur.
  2. Acceptez une livraison si elle se vérifie avec l’un ou l’autre secret.
  3. 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épondez 2xx en 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 429 ou 503 avec Retry-After quand vous avez besoin que Sursa ralentisse ; ne répondez jamais 410 par 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.