Langsung ke konten
Sursa Developers

Menerima webhook

Webhook memberi tahu sistem Anda tentang perubahan di Sursa saat perubahan itu terjadi: kartu dipublikasikan, diverifikasi, atau diarsipkan, pertanyaan tidak terjawab. Sursa mengirim POST HTTPS berisi peristiwa ke alamat yang Anda daftarkan. Panduan ini membangun penerima yang tetap benar saat terjadi percobaan ulang, perubahan urutan, dan duplikat.

Untuk arti dan isi setiap peristiwa, lihat ikhtisar webhook dan katalog peristiwa di referensi API.

1. Daftarkan webhook

Administrator mendaftarkan webhook di Sursa melalui Administration → Integrations → Webhooks and API (Administrasi → Integrasi → Webhook dan API), atau dengan POST /v1/webhooks dan kunci dengan peran admin dan cakupan 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"]
  }'

Respons berisi rahasia penandatanganan di secret, diawali whsec_. Rahasia ini ditampilkan sekali. Simpan bersama penerima Anda, misalnya di variabel lingkungan SURSA_WEBHOOK_SECRET.

Alamat harus memakai https dan mengarah ke alamat IP publik: alamat di jaringan privat, loopback, dan jaringan khusus lainnya ditolak saat Anda mendaftarkannya dan pada setiap pengiriman. Untuk mengembangkan di komputer Anda sendiri, buka akses ke penerima lokal Anda melalui layanan tunneling yang memberinya alamat HTTPS publik.

Kirim peristiwa uji kapan saja dengan POST /v1/webhooks/{webhook_id}/test. Respons menunjukkan apa yang dijawab penerima Anda.

2. Seperti apa sebuah pengiriman

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}
HeaderArtinya
webhook-idID peristiwa. Sama di setiap percobaan ulang. Pakai untuk membuang duplikat.
webhook-timestampKapan upaya ini ditandatangani, dalam detik Unix.
webhook-signatureTanda tangan dalam format Standard Webhooks.
X-KB-SignaturePengiriman yang sama, ditandatangani dalam format Sursa yang lebih lama.
X-KB-EventJenis peristiwa.
X-KB-AttemptNomor upaya, mulai dari 1.
X-KB-SequencePosisi pengiriman ini dalam urutan webhook.

Nama header tidak peka huruf besar-kecil; framework Anda mungkin menampilkannya sebagai X-Kb-Signature atau x-kb-signature.

Isinya adalah peristiwa. data-nya bergantung pada jenis peristiwa. Webhook yang diatur ke payload thin menerima peristiwa tanpa data; sebagai gantinya, baca objek melalui API dengan kunci Anda sendiri.

3. Periksa tanda tangan

Periksa tanda tangan setiap pengiriman sebelum Anda memercayainya. Kedua format menandatangani byte isi yang persis, jadi baca raw body sebelum penguraian JSON apa pun: menyerialisasi ulang JSON mengubah byte-nya dan menggagalkan pemeriksaan.

FormatString yang ditandatanganiKunciTanda tangan
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Bagian rahasia setelah whsec_, didekode dari base64v1, + base64 dari HMAC-SHA256
Format lama (X-KB-Signature)<t>.<body>Seluruh string rahasia, sebagai teksv1= + hex dari HMAC-SHA256

Memeriksa salah satunya sudah cukup. Utamakan header Standard Webhooks: pemeriksaan yang sama tersedia di banyak pustaka siap pakai. Selama rotasi rahasia, setiap header membawa satu tanda tangan per rahasia yang berlaku (dipisahkan spasi di webhook-signature, v1= berulang di X-KB-Signature). Pengiriman valid jika salah satunya cocok.

Tolak juga timestamp yang berselisih lebih dari 5 menit dari jam Anda. Ini mencegah seseorang memutar ulang pengiriman yang direkam di kemudian hari.

# 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

Uji kode pemeriksa Anda

Jalankan kode Anda dengan nilai-nilai ini sebelum menghubungkannya ke pengiriman sungguhan. Dengan jam disetel ke timestamp tersebut, kedua pemeriksaan harus lolos, dan harus gagal jika satu byte isi berubah.

MasukanNilai
Rahasiawhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp dan t1791194400
Isi{"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. Jawab dengan cepat, kerjakan belakangan

Sursa menunggu jawaban Anda hingga 10 detik. Status 2xx apa pun berarti pengiriman berhasil; selain itu, termasuk pengalihan, timeout, atau koneksi yang ditutup, berarti pengiriman gagal dan akan dicoba ulang.

Jadi penerima sebaiknya hanya memeriksa, mencatat peristiwa, dan menjawab. Kerjakan pekerjaan sebenarnya sesudahnya, dari antrean Anda sendiri.

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

Jawab 2xx hanya setelah peristiwa tersimpan dengan aman. Jika Anda menjawab lebih dulu lalu sistem Anda mogok, peristiwa itu hilang bagi Anda, karena Sursa sudah menganggapnya terkirim.

5. Percobaan ulang

Pengiriman yang gagal dicoba ulang dengan jeda yang makin panjang: setelah sekitar 1 menit, 5 menit, 30 menit, 2 jam, 5 jam, 10 jam, 24 jam, dan 24 jam, sembilan upaya selama hampir tiga hari. Setiap jeda bervariasi hingga 20% agar percobaan ulang dari banyak webhook tidak tiba pada saat yang sama.

  • Jawaban 429 atau 503 dengan Retry-After memundurkan upaya berikutnya setidaknya hingga waktu itu, paling lambat 24 jam kemudian. Pakai ini saat sistem Anda kelebihan beban atau sedang dalam pemeliharaan.
  • Jawaban 410 Gone menonaktifkan webhook: kirim jawaban ini hanya jika Anda tidak menginginkan pengiriman lagi ke alamat itu.
  • Setelah 10 upaya gagal berturut-turut, webhook dijeda, dan administrator organisasi diberi tahu. Peristiwa terus terkumpul, dan saat webhook dilanjutkan kembali, di antarmuka administrasi atau dengan POST /v1/webhooks/{webhook_id}/resume, peristiwa itu dikirim sesuai urutan.
  • Setelah upaya terakhir, pengiriman ditandai exhausted. Anda dapat mengirimnya lagi secara manual.

Pengiriman sebuah webhook, beserta setiap upaya, kode jawaban Anda, dan waktunya, dicantumkan oleh GET /v1/webhooks/{webhook_id}/deliveries dan di antarmuka administrasi. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver mengirim satu pengiriman lagi. Pengiriman ulang memiliki webhook-id yang sama.

6. Urutan dan duplikat

  • Urutan. Pengiriman ke satu webhook berjalan ketat satu demi satu sesuai urutan sequence. Selama satu pengiriman dicoba ulang, pengiriman berikutnya menunggu. Webhook yang berbeda tidak saling menunggu.
  • Duplikat. Peristiwa yang sama dapat tiba lebih dari sekali, misalnya ketika jawaban Anda hilang dalam perjalanan kembali. Simpan webhook-id (sama dengan id di isi) setiap peristiwa yang Anda proses, dan lewati peristiwa yang sudah pernah Anda lihat.
  • Berpikir dalam keadaan objek. Untuk perubahan pada objek yang sama, bandingkan created_at atau sequence dengan yang Anda simpan, dan abaikan peristiwa yang lebih lama daripada keadaan yang sudah Anda miliki. Jika ragu, baca objek terkini melalui API: peristiwa memberi tahu Anda bahwa sesuatu berubah, API memberi tahu Anda bagaimana keadaannya sekarang.

7. Lakukan rekonsiliasi dengan umpan peristiwa

Setiap peristiwa juga disimpan selama 30 hari di umpan peristiwa, terlepas dari ada atau tidaknya webhook yang berlangganan peristiwa itu. Jika penerima Anda mati lebih lama daripada rentang percobaan ulang, atau Anda ingin memeriksa bahwa tidak ada yang terlewat, baca umpannya dengan kunci yang memiliki peran admin dan cakupan 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"

Peristiwa di umpan memiliki id dan bentuk yang sama seperti dalam pengiriman, tanpa field pengiriman sequence, payload, dan test. Karena penerima Anda melewati peristiwa yang sudah pernah dilihatnya, Anda dapat menjalankan rekonsiliasi sesering yang Anda mau, misalnya sekali per jam.

GET /v1/webhook-events/{event_id} membaca satu peristiwa menurut ID-nya.

8. Rotasi rahasia

POST /v1/webhooks/{webhook_id}/rotate-secret menerbitkan rahasia baru dan membiarkan rahasia sebelumnya tetap berlaku selama masa tumpang tindih, secara default 24 jam dan paling lama 7 hari. Selama masa tumpang tindih, setiap pengiriman ditandatangani dengan kedua rahasia.

  1. Lakukan rotasi, dan simpan rahasia baru di samping rahasia lama di penerima Anda.
  2. Terima pengiriman jika lolos pemeriksaan dengan salah satu rahasia.
  3. Setelah masa tumpang tindih, hapus rahasia lama.

Jika rahasia lama bocor, segera akhiri masa tumpang tindih dengan POST /v1/webhooks/{webhook_id}/revoke-previous-secret, atau rotasi rahasia dengan "overlap_hours": 0.

Daftar periksa

  • Periksa salah satu tanda tangan pada raw body, dan periksa timestamp-nya.
  • Simpan peristiwa dengan kunci webhook-id, lalu jawab 2xx dalam 10 detik.
  • Lewati duplikat dan peristiwa yang sudah kedaluwarsa; baca objek terkini melalui API jika hal itu penting.
  • Jawab 429 atau 503 dengan Retry-After saat Anda membutuhkan Sursa untuk menahan diri; jangan pernah menjawab 410 secara tidak sengaja.
  • Lewati jenis peristiwa dan field yang tidak Anda kenal.
  • Lakukan rekonsiliasi dengan umpan peristiwa secara terjadwal.