Chuyển tới nội dung
Sursa Developers

Nhận webhook

Webhook báo cho hệ thống của bạn về một thay đổi trong Sursa ngay khi nó xảy ra: một thẻ được xuất bản, được kiểm chứng hoặc được lưu trữ, một câu hỏi không có câu trả lời. Sursa gửi một yêu cầu HTTPS POST kèm sự kiện tới địa chỉ mà bạn đăng ký. Hướng dẫn này xây dựng một máy chủ nhận hoạt động đúng khi có thử lại, đảo thứ tự và trùng lặp.

Để biết mỗi sự kiện có nghĩa gì và chứa gì, hãy xem tổng quan về webhook và catalog sự kiện trong tài liệu tham chiếu API.

1. Đăng ký webhook

Quản trị viên đăng ký webhook trong Sursa tại Administration → Integrations → Webhooks and API (Quản trị → Tích hợp → Webhook và API), hoặc bằng POST /v1/webhooks với một khóa có vai trò admin và phạm vi quyền 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"]
  }'

Phản hồi chứa khóa bí mật để ký trong secret, bắt đầu bằng whsec_. Nó chỉ hiển thị một lần. Hãy lưu nó cùng máy chủ nhận của bạn, ví dụ trong biến môi trường SURSA_WEBHOOK_SECRET.

Địa chỉ phải dùng https và phân giải tới địa chỉ IP công khai: các địa chỉ trong mạng riêng, mạng loopback và các mạng dành riêng khác bị từ chối khi bạn đăng ký và ở mỗi lần gửi. Để phát triển trên máy tính của bạn, hãy công khai máy chủ nhận cục bộ qua một dịch vụ tạo đường hầm cấp cho nó một địa chỉ HTTPS công khai.

Gửi sự kiện thử bất cứ lúc nào bằng POST /v1/webhooks/{webhook_id}/test. Phản hồi cho thấy máy chủ nhận của bạn đã trả lời gì.

2. Một lần gửi trông như thế nào

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}
HeaderÝ nghĩa
webhook-idID của sự kiện. Giống nhau ở mọi lần thử lại. Dùng nó để loại bỏ bản trùng lặp.
webhook-timestampThời điểm lần thử này được ký, tính bằng giây Unix.
webhook-signatureChữ ký theo định dạng Standard Webhooks.
X-KB-SignatureCùng lần gửi đó được ký theo định dạng Sursa trước đây.
X-KB-EventLoại sự kiện.
X-KB-AttemptSố thứ tự lần thử, bắt đầu từ 1.
X-KB-SequenceVị trí của lần gửi này trong thứ tự gửi của webhook.

Tên header không phân biệt chữ hoa chữ thường; framework của bạn có thể hiển thị chúng là X-Kb-Signature hoặc x-kb-signature.

Phần thân là sự kiện. data của nó phụ thuộc vào loại sự kiện. Webhook đặt chế độ payload thin nhận sự kiện không có data; thay vào đó, hãy đọc đối tượng qua API bằng khóa của chính bạn.

3. Xác minh chữ ký

Hãy kiểm tra chữ ký của mọi lần gửi trước khi tin vào nó. Cả hai định dạng đều ký chính xác các byte của phần thân, nên hãy đọc phần thân thô trước khi phân tích JSON: tuần tự hóa lại JSON sẽ làm thay đổi các byte và làm hỏng phép kiểm tra.

Định dạngChuỗi được kýKhóaChữ ký
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Phần của khóa bí mật sau whsec_, giải mã base64v1, + base64 của HMAC-SHA256
Định dạng trước đây (X-KB-Signature)<t>.<body>Toàn bộ chuỗi khóa bí mật, dưới dạng văn bảnv1= + hex của HMAC-SHA256

Chỉ cần xác minh một trong hai. Hãy ưu tiên header Standard Webhooks: phép kiểm tra này có sẵn trong nhiều thư viện dựng sẵn. Trong khi xoay vòng khóa bí mật, mỗi header mang một chữ ký cho mỗi khóa bí mật còn hiệu lực (phân tách bằng dấu cách trong webhook-signature, lặp lại v1= trong X-KB-Signature). Lần gửi hợp lệ nếu bất kỳ chữ ký nào khớp.

Ngoài ra, hãy từ chối dấu thời gian lệch quá 5 phút so với đồng hồ của bạn. Điều này ngăn người khác phát lại một lần gửi đã ghi lại vào lúc sau.

# 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

Kiểm tra đoạn mã xác minh chữ ký

Hãy chạy mã của bạn với các giá trị sau trước khi kết nối nó với các lần gửi thật. Khi đồng hồ được đặt bằng dấu thời gian, cả hai phép kiểm tra phải thành công, và phải thất bại khi một byte của phần thân thay đổi.

Đầu vàoGiá trị
Khóa bí mậtwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp và t1791194400
Phần thân{"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. Trả lời nhanh, xử lý sau

Sursa chờ câu trả lời của bạn tối đa 10 giây. Mọi trạng thái 2xx nghĩa là lần gửi thành công; mọi thứ khác, kể cả chuyển hướng, hết thời gian chờ hay kết nối bị đóng, nghĩa là lần gửi thất bại và sẽ được thử lại.

Vì vậy máy chủ nhận chỉ nên xác minh, ghi lại sự kiện và trả lời. Hãy làm công việc thật sau đó, từ hàng đợi của riêng bạn.

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

Chỉ trả lời 2xx sau khi sự kiện đã được lưu an toàn. Nếu bạn trả lời trước rồi bị sập, sự kiện sẽ mất đối với bạn, vì Sursa đã tính nó là đã gửi thành công.

5. Thử lại

Lần gửi thất bại được thử lại với khoảng chờ tăng dần: sau khoảng 1 phút, 5 phút, 30 phút, 2 giờ, 5 giờ, 10 giờ, 24 giờ và 24 giờ, tổng cộng chín lần thử trong gần ba ngày. Mỗi khoảng chờ dao động tối đa 20% để các lần thử lại từ nhiều webhook không tới cùng một lúc.

  • Câu trả lời 429 hoặc 503 kèm Retry-After dời lần thử tiếp theo tới ít nhất thời điểm đó, tối đa 24 giờ sau. Hãy dùng nó khi hệ thống của bạn quá tải hoặc đang bảo trì.
  • Câu trả lời 410 Gone vô hiệu hóa webhook: chỉ gửi nó khi bạn không muốn nhận thêm lần gửi nào tới địa chỉ đó.
  • Sau 10 lần thử thất bại liên tiếp, webhook bị tạm dừng, và các quản trị viên của tổ chức được thông báo. Sự kiện tiếp tục được tích lũy, và khi webhook được tiếp tục, trong giao diện quản trị hoặc bằng POST /v1/webhooks/{webhook_id}/resume, chúng được gửi theo đúng thứ tự.
  • Sau lần thử cuối cùng, lần gửi được đánh dấu exhausted. Bạn có thể gửi lại nó thủ công.

Các lần gửi của một webhook, cùng từng lần thử, mã trả lời của bạn và thời gian của nó, được liệt kê bởi GET /v1/webhooks/{webhook_id}/deliveries và trong giao diện quản trị. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver gửi lại một lần gửi. Lần gửi lại có cùng webhook-id.

6. Thứ tự và trùng lặp

  • Thứ tự. Các lần gửi tới một webhook diễn ra lần lượt theo đúng thứ tự của sequence. Trong khi một lần gửi đang được thử lại, các lần gửi sau phải chờ. Các webhook khác nhau không chờ nhau.
  • Trùng lặp. Cùng một sự kiện có thể tới nhiều hơn một lần, ví dụ khi câu trả lời của bạn bị mất trên đường về. Hãy lưu webhook-id (cũng là id trong phần thân) của mọi sự kiện bạn xử lý, và bỏ qua sự kiện bạn đã thấy.
  • Tư duy theo trạng thái. Với các thay đổi của cùng một đối tượng, hãy so sánh created_at hoặc sequence với những gì bạn đã lưu, và bỏ qua sự kiện cũ hơn trạng thái bạn đang có. Khi không chắc, hãy đọc đối tượng hiện tại qua API: sự kiện cho bạn biết rằng có gì đó đã thay đổi, còn API cho bạn biết bây giờ nó là gì.

7. Đối soát với nguồn cấp sự kiện

Mọi sự kiện cũng được lưu giữ trong 30 ngày trong nguồn cấp sự kiện, dù có webhook nào đăng ký nhận nó hay không. Nếu máy chủ nhận của bạn ngừng hoạt động lâu hơn khoảng thời gian mà các lần thử lại bao quát, hoặc bạn muốn kiểm tra rằng không có gì bị bỏ sót, hãy đọc nguồn cấp bằng một khóa có vai trò admin và phạm vi quyền 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"

Sự kiện trong nguồn cấp có cùng id và cấu trúc như trong một lần gửi, nhưng không có các trường của lần gửi sequence, payload và test. Vì máy chủ nhận của bạn bỏ qua các sự kiện đã thấy, bạn có thể chạy đối soát thường xuyên tùy ý, ví dụ mỗi giờ một lần.

GET /v1/webhook-events/{event_id} đọc một sự kiện theo ID của nó.

8. Xoay vòng khóa bí mật

POST /v1/webhooks/{webhook_id}/rotate-secret cấp một khóa bí mật mới và giữ khóa trước đó còn hiệu lực trong một thời gian chồng lấn, mặc định 24 giờ và tối đa 7 ngày. Trong thời gian chồng lấn, mọi lần gửi được ký bằng cả hai khóa bí mật.

  1. Xoay vòng, và lưu khóa bí mật mới cạnh khóa cũ trong máy chủ nhận của bạn.
  2. Chấp nhận lần gửi nếu nó được xác minh bằng một trong hai khóa bí mật.
  3. Sau thời gian chồng lấn, xóa khóa bí mật cũ.

Nếu khóa bí mật cũ đã bị lộ, hãy kết thúc thời gian chồng lấn ngay bằng POST /v1/webhooks/{webhook_id}/revoke-previous-secret, hoặc xoay vòng với "overlap_hours": 0.

Danh sách kiểm tra

  • Xác minh một trong các chữ ký trên phần thân thô, và kiểm tra dấu thời gian.
  • Lưu sự kiện, định danh bằng webhook-id, rồi trả lời 2xx trong vòng 10 giây.
  • Bỏ qua bản trùng lặp và sự kiện đã cũ; đọc đối tượng hiện tại qua API khi điều đó quan trọng.
  • Trả lời 429 hoặc 503 kèm Retry-After khi bạn cần Sursa giảm tốc; không bao giờ vô tình trả lời 410.
  • Bỏ qua các loại sự kiện và trường bạn không biết.
  • Đối soát với nguồn cấp sự kiện theo lịch.