Anar al contingut
Sursa Developers

Rebre webhooks

Un webhook informa el vostre sistema d’un canvi a Sursa en el moment que es produeix: s’ha publicat, verificat o arxivat una entrada, una pregunta s’ha quedat sense resposta. Sursa envia un POST HTTPS amb l’esdeveniment a una adreça que registreu. Aquesta guia construeix un receptor que funciona correctament amb reintents, canvis d’ordre i duplicats.

Per saber què vol dir i què conté cada esdeveniment, vegeu la visió general dels webhooks i el catàleg d’esdeveniments de la referència de l’API.

1. Registrar un webhook

Qui administra l’organització registra els webhooks a Sursa a Administration → Integrations → Webhooks and API (administració, integracions, webhooks), o amb POST /v1/webhooks i una clau amb el rol admin i l’àmbit 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 resposta conté el secret de signatura a secret, que comença per whsec_. Es mostra una sola vegada. Deseu-lo amb el vostre receptor, per exemple en una variable d’entorn SURSA_WEBHOOK_SECRET.

L’adreça ha de fer servir https i resoldre’s en adreces IP públiques: les adreces de xarxes privades, de bucle local i d’altres xarxes reservades es rebutgen en registrar-les i en cada lliurament. Per desenvolupar al vostre ordinador, exposeu el receptor local mitjançant un servei de túnel que li doni una adreça HTTPS pública.

Envieu un esdeveniment de prova en qualsevol moment amb POST /v1/webhooks/{webhook_id}/test. La resposta mostra què ha respost el vostre receptor.

2. Com és un lliurament

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}
CapçaleraSignificat
webhook-idL’ID de l’esdeveniment. És el mateix en cada reintent. Feu-lo servir per descartar duplicats.
webhook-timestampQuan s’ha signat aquest intent, en segons Unix.
webhook-signatureLa signatura en el format Standard Webhooks.
X-KB-SignatureEl mateix lliurament signat en el format anterior de Sursa.
X-KB-EventEl tipus d’esdeveniment.
X-KB-AttemptEl número d’intent, a partir d’1.
X-KB-SequenceLa posició d’aquest lliurament en l’ordre del webhook.

Els noms de les capçaleres no distingeixen entre majúscules i minúscules; el vostre framework els pot mostrar com a X-Kb-Signature o x-kb-signature.

El cos és l’esdeveniment. El seu data depèn del tipus. Un webhook configurat amb la càrrega útil thin rep l’esdeveniment sense data; en aquest cas, llegiu l’objecte a través de l’API amb la vostra pròpia clau.

3. Verificar la signatura

Comproveu la signatura de cada lliurament abans de confiar-hi. Tots dos formats signen els bytes exactes del cos, per això heu de llegir el cos en brut abans de qualsevol anàlisi del JSON: tornar a serialitzar el JSON canvia els bytes i fa fallar la comprovació.

FormatCadena signadaClauSignatura
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>La part del secret posterior a whsec_, descodificada de base64v1, + base64 de l’HMAC-SHA256
Format anterior (X-KB-Signature)<t>.<body>La cadena sencera del secret, com a textv1= + hexadecimal de l’HMAC-SHA256

N’hi ha prou de verificar-ne un dels dos. Feu servir preferentment la capçalera de Standard Webhooks: la mateixa comprovació és disponible en moltes biblioteques ja fetes. Durant una rotació del secret, cada capçalera porta una signatura per cada secret vàlid (separades per espais a webhook-signature, amb v1= repetit a X-KB-Signature). Un lliurament és vàlid si qualsevol d’elles coincideix.

Rebutgeu també una marca de temps que s’allunyi més de 5 minuts del vostre rellotge. Això impedeix que algú torni a enviar més tard un lliurament enregistrat.

# 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

Proveu la verificació de signatures

Executeu el vostre codi amb aquests valors abans de connectar-lo als lliuraments reals. Amb el rellotge posat a la marca de temps, totes dues comprovacions han de passar, i han de fallar quan canvia un byte del cos.

DadaValor
Secretwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp i t1791194400
Cos{"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. Respondre ràpidament i treballar després

Sursa espera la vostra resposta fins a 10 segons. Qualsevol estat 2xx vol dir que el lliurament ha anat bé; qualsevol altra cosa, inclosos una redirecció, un temps d’espera esgotat o una connexió tancada, vol dir que ha fallat i que es reintentarà.

Per tant, el receptor només ha de verificar, desar l’esdeveniment i respondre. Feu la feina real després, des de la vostra pròpia cua.

// 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);

Responeu 2xx només quan l’esdeveniment estigui desat de manera segura. Si responeu primer i després el procés falla, perdeu l’esdeveniment, perquè Sursa ja l’ha comptat com a lliurat.

5. Reintents

Un lliurament fallit es reintenta amb pauses cada vegada més llargues: després d’aproximadament 1 minut, 5 minuts, 30 minuts, 2 hores, 5 hores, 10 hores, 24 hores i 24 hores, nou intents en gairebé tres dies. Cada pausa varia fins a un 20 % perquè els reintents de molts webhooks no arribin al mateix moment.

  • Una resposta 429 o 503 amb Retry-After ajorna l’intent següent com a mínim fins a aquell moment, amb un màxim de 24 hores. Feu-la servir quan el vostre sistema estigui sobrecarregat o en manteniment.
  • Una resposta 410 Gone desactiva el webhook: envieu-la només quan no vulgueu rebre més lliuraments en aquella adreça.
  • Després de 10 intents fallits seguits, el webhook es posa en pausa i se n’avisa els administradors de l’organització. Els esdeveniments es continuen acumulant, i quan el webhook es reprèn, a la interfície d’administració o amb POST /v1/webhooks/{webhook_id}/resume, es lliuren en ordre.
  • Després de l’últim intent, un lliurament es marca com a exhausted. El podeu tornar a enviar a mà.

Els lliuraments d’un webhook, amb cada intent, el vostre codi de resposta i el temps que ha trigat, es poden consultar amb GET /v1/webhooks/{webhook_id}/deliveries i a la interfície d’administració. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver en torna a enviar un. Un reenviament té el mateix webhook-id.

6. Ordre i duplicats

  • Ordre. Els lliuraments a un webhook van estrictament un darrere l’altre en l’ordre de sequence. Mentre es reintenta un lliurament, els posteriors esperen. Els diferents webhooks no s’esperen entre si.
  • Duplicats. El mateix esdeveniment pot arribar més d’una vegada, per exemple quan la vostra resposta s’ha perdut pel camí de tornada. Deseu el webhook-id (que també és l’id del cos) de cada esdeveniment que processeu, i ometeu els que ja hàgiu vist.
  • Pensar en estats. Per als canvis d’un mateix objecte, compareu created_at o sequence amb el que heu desat, i ignoreu un esdeveniment més antic que l’estat que ja teniu. En cas de dubte, llegiu l’objecte actual a través de l’API: l’esdeveniment us diu que alguna cosa ha canviat, l’API us diu com és ara.

7. Conciliar amb el canal d’esdeveniments

Cada esdeveniment també es conserva durant 30 dies al canal d’esdeveniments, tant si hi havia un webhook subscrit com si no. Si el vostre receptor ha estat aturat més temps del que cobreixen els reintents, o voleu comprovar que no s’ha perdut res, llegiu el canal amb una clau que tingui el rol admin i l’àmbit 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 esdeveniment del canal té el mateix id i la mateixa forma que en un lliurament, sense els camps del lliurament sequence, payload i test. Com que el vostre receptor omet els esdeveniments que ja ha vist, podeu executar la conciliació tan sovint com vulgueu, per exemple un cop per hora.

GET /v1/webhook-events/{event_id} llegeix un esdeveniment pel seu ID.

8. Fer rotar el secret

POST /v1/webhooks/{webhook_id}/rotate-secret emet un secret nou i manté vàlid l’anterior durant un període de solapament, de 24 hores per defecte i fins a 7 dies. Durant el solapament, cada lliurament se signa amb tots dos secrets.

  1. Feu la rotació i deseu el secret nou al costat de l’antic al vostre receptor.
  2. Accepteu un lliurament si es verifica amb qualsevol dels dos secrets.
  3. Després del solapament, elimineu el secret antic.

Si el secret antic s’ha filtrat, acabeu el solapament immediatament amb POST /v1/webhooks/{webhook_id}/revoke-previous-secret, o feu rotar el secret amb "overlap_hours": 0.

Llista de comprovació

  • Verifiqueu una de les signatures sobre el cos en brut i comproveu la marca de temps.
  • Deseu l’esdeveniment, identificat pel webhook-id, i després responeu 2xx en menys de 10 segons.
  • Ometeu els duplicats i els esdeveniments desfasats; quan importi, llegiu l’objecte actual a través de l’API.
  • Responeu 429 o 503 amb Retry-After quan necessiteu que Sursa alenteixi els lliuraments; no respongueu mai 410 per error.
  • Ometeu els tipus d’esdeveniment i els camps que no conegueu.
  • Concilieu periòdicament amb el canal d’esdeveniments.