Salt la conținut
Sursa Developers

Primiți webhookuri

Un webhook anunță sistemul dumneavoastră despre o modificare din Sursa în momentul în care se produce: o fișă este publicată, verificată sau arhivată, o întrebare a rămas fără răspuns. Sursa trimite un POST HTTPS cu evenimentul la o adresă pe care o înregistrați. Acest ghid construiește un receptor care funcționează corect la reîncercări, schimbări de ordine și dubluri.

Pentru ce înseamnă și ce conține fiecare eveniment, vedeți prezentarea generală a webhookurilor și catalogul evenimentelor din referința API.

1. Înregistrați un webhook

Un administrator înregistrează webhookuri în Sursa, în Administration → Integrations → Webhooks and API (administrare → integrări → webhookuri și API), sau cu POST /v1/webhooks și o cheie cu rolul admin și domeniul 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"]
  }'

Răspunsul conține secretul de semnare în secret, care începe cu whsec_. Acesta este afișat o singură dată. Stocați-l împreună cu receptorul, de exemplu în variabila de mediu SURSA_WEBHOOK_SECRET.

Adresa trebuie să folosească https și să se rezolve la adrese IP publice: adresele din rețele private, de loopback și din alte rețele rezervate sunt respinse la înregistrare și la fiecare livrare. Pentru a dezvolta pe propriul calculator, expuneți receptorul local printr-un serviciu de tunelare care îi oferă o adresă HTTPS publică.

Trimiteți oricând un eveniment de test cu POST /v1/webhooks/{webhook_id}/test. Răspunsul arată ce a răspuns receptorul.

2. Cum arată o livrare

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}
AntetSemnificație
webhook-idIdentificatorul evenimentului. Același la fiecare reîncercare. Folosiți-l pentru a elimina dublurile.
webhook-timestampMomentul în care a fost semnată această încercare, în secunde Unix.
webhook-signatureSemnătura în formatul Standard Webhooks.
X-KB-SignatureAceeași livrare, semnată în formatul anterior al Sursa.
X-KB-EventTipul evenimentului.
X-KB-AttemptNumărul încercării, de la 1.
X-KB-SequencePoziția acestei livrări în ordinea livrărilor webhookului.

Numele antetelor nu țin cont de majuscule; framework-ul folosit le poate afișa ca X-Kb-Signature sau x-kb-signature.

Corpul este evenimentul. data lui depinde de tip. Un webhook setat pe conținutul thin primește evenimentul fără data; în acest caz, citiți obiectul prin API cu propria cheie.

3. Verificați semnătura

Verificați semnătura fiecărei livrări înainte de a avea încredere în ea. Ambele formate semnează octeții exacți ai corpului, așa că citiți corpul brut înainte de orice analiză JSON: o nouă serializare a JSON-ului schimbă octeții și strică verificarea.

FormatȘirul semnatCheieSemnătură
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Partea secretului de după whsec_, decodificată din base64v1, + base64 al HMAC-SHA256
Formatul anterior (X-KB-Signature)<t>.<body>Întregul șir al secretului, ca textv1= + hex al HMAC-SHA256

Este suficient să verificați unul dintre ele. Preferați antetul Standard Webhooks: aceeași verificare există în multe biblioteci gata făcute. În timpul reînnoirii secretului, fiecare antet conține câte o semnătură pentru fiecare secret valid (separate prin spațiu în webhook-signature, cu v1= repetat în X-KB-Signature). O livrare este validă dacă oricare dintre ele corespunde.

Respingeți și un marcaj de timp aflat la mai mult de 5 minute față de ceasul dumneavoastră. Astfel, nimeni nu poate retrimite mai târziu o livrare înregistrată.

# 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

Testați funcția de verificare

Rulați codul cu aceste valori înainte să îl conectați la livrări reale. Cu ceasul setat la marcajul de timp, ambele verificări trebuie să reușească și trebuie să eșueze când se schimbă un singur octet al corpului.

IntrareValoare
Secretwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp și t1791194400
Corp{"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ăspundeți rapid, lucrați după aceea

Sursa așteaptă răspunsul cel mult 10 secunde. Orice status 2xx înseamnă că livrarea a reușit; orice altceva, inclusiv o redirecționare, o expirare sau o conexiune închisă, înseamnă că a eșuat și va fi reîncercată.

Așadar, receptorul trebuie doar să verifice, să înregistreze evenimentul și să răspundă. Faceți prelucrarea propriu-zisă după aceea, din propria coadă.

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

Răspundeți 2xx doar după ce evenimentul este stocat în siguranță. Dacă răspundeți mai întâi și apoi procesul se oprește brusc, evenimentul este pierdut pentru dumneavoastră, pentru că Sursa l-a socotit deja livrat.

5. Reîncercări

O livrare eșuată este reîncercată cu pauze din ce în ce mai lungi: după aproximativ 1 minut, 5 minute, 30 de minute, 2 ore, 5 ore, 10 ore, 24 de ore și 24 de ore, adică nouă încercări în aproape trei zile. Fiecare pauză variază cu până la 20%, astfel încât reîncercările de la multe webhookuri să nu sosească în același moment.

  • Un răspuns 429 sau 503 cu Retry-After amână următoarea încercare cel puțin până la acel moment, dar nu mai mult de 24 de ore. Folosiți-l când sistemul este supraîncărcat sau în mentenanță.
  • Un răspuns 410 Gone dezactivează webhookul: trimiteți-l doar când nu mai vreți livrări la acea adresă.
  • După 10 încercări eșuate consecutive, webhookul este pus în pauză, iar administratorii organizației sunt anunțați. Evenimentele continuă să se acumuleze și, când webhookul este reluat, în interfața de administrare sau cu POST /v1/webhooks/{webhook_id}/resume, sunt livrate în ordine.
  • După ultima încercare, o livrare este marcată exhausted. O puteți trimite din nou manual.

Livrările unui webhook, cu fiecare încercare, codul răspunsului și durata lui, sunt enumerate de GET /v1/webhooks/{webhook_id}/deliveries și în interfața de administrare. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver trimite din nou o livrare. O livrare nouă are același webhook-id.

6. Ordine și dubluri

  • Ordine. Livrările către un webhook se fac strict una după alta, în ordinea sequence. Cât timp o livrare este reîncercată, cele ulterioare așteaptă. Webhookurile diferite nu se așteaptă unele pe altele.
  • Dubluri. Același eveniment poate sosi de mai multe ori, de exemplu când răspunsul dumneavoastră s-a pierdut pe drumul înapoi. Stocați webhook-id (este și id din corp) al fiecărui eveniment prelucrat și ignorați un eveniment pe care l-ați văzut deja.
  • Gândiți în stări. Pentru modificările aceluiași obiect, comparați created_at sau sequence cu ce ați stocat și ignorați un eveniment mai vechi decât starea pe care o aveți deja. Când aveți îndoieli, citiți obiectul curent prin API: evenimentul vă spune că ceva s-a schimbat, API-ul vă spune cum este acum.

7. Reconcilierea cu fluxul de evenimente

Fiecare eveniment se păstrează și 30 de zile în fluxul de evenimente, indiferent dacă vreun webhook era abonat la el sau nu. Dacă receptorul a fost oprit mai mult decât acoperă reîncercările sau vreți să verificați că nu s-a pierdut nimic, citiți fluxul cu o cheie care are rolul admin și domeniul 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 eveniment din flux are același id și aceeași formă ca într-o livrare, fără câmpurile de livrare sequence, payload și test. Pentru că receptorul ignoră evenimentele văzute deja, puteți rula reconcilierea cât de des doriți, de exemplu o dată pe oră.

GET /v1/webhook-events/{event_id} citește un eveniment după identificatorul lui.

8. Reînnoirea secretului

POST /v1/webhooks/{webhook_id}/rotate-secret emite un secret nou și păstrează valabil secretul anterior pe o perioadă de suprapunere, implicit 24 de ore și cel mult 7 zile. În timpul suprapunerii, fiecare livrare este semnată cu ambele secrete.

  1. Reînnoiți secretul și stocați-l pe cel nou lângă cel vechi în receptor.
  2. Acceptați o livrare dacă se verifică cu oricare dintre secrete.
  3. După suprapunere, eliminați secretul vechi.

Dacă secretul vechi a fost divulgat, încheiați imediat suprapunerea cu POST /v1/webhooks/{webhook_id}/revoke-previous-secret sau reînnoiți secretul cu "overlap_hours": 0.

Listă de verificare

  • Verificați una dintre semnături pe corpul brut și verificați marcajul de timp.
  • Stocați evenimentul după webhook-id, apoi răspundeți 2xx în cel mult 10 secunde.
  • Ignorați dublurile și evenimentele învechite; citiți obiectul curent prin API când contează.
  • Răspundeți 429 sau 503 cu Retry-After când aveți nevoie ca Sursa să încetinească; nu răspundeți niciodată 410 din greșeală.
  • Ignorați tipurile de evenimente și câmpurile necunoscute.
  • Reconciliați periodic cu fluxul de evenimente.