ข้ามไปยังเนื้อหา
Sursa Developers

รับเว็บฮุก

เว็บฮุกแจ้งระบบของคุณถึงการเปลี่ยนแปลงใน Sursa ทันทีที่เกิดขึ้น เช่น การ์ดถูกเผยแพร่ ผ่านการตรวจสอบ หรือถูกเก็บเข้าคลัง หรือมีคำถามที่ไม่ได้รับคำตอบ Sursa ส่งคำขอ HTTPS POST ที่มีอีเวนต์ไปยังที่อยู่ที่คุณลงทะเบียนไว้ คู่มือนี้สร้างเซิร์ฟเวอร์ผู้รับที่ทำงานถูกต้องแม้มีการลองใหม่ การสลับลำดับ และการส่งซ้ำ

ความหมายและข้อมูลของแต่ละอีเวนต์ ดูได้ใน ภาพรวมของเว็บฮุก และแค็ตตาล็อกอีเวนต์ใน เอกสารอ้างอิง API

1. ลงทะเบียนเว็บฮุก

ผู้ดูแลระบบลงทะเบียนเว็บฮุกใน Sursa ได้ที่ Administration → Integrations → Webhooks and API (การดูแลระบบ → การเชื่อมต่อระบบ → เว็บฮุก) หรือด้วย POST /v1/webhooks และคีย์ที่มีบทบาท admin และขอบเขตสิทธิ์ 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"]
  }'

การตอบกลับมีข้อมูลลับสำหรับลงนามอยู่ใน secret ซึ่งขึ้นต้นด้วย whsec_ ค่านี้แสดง ครั้งเดียว เท่านั้น ให้เก็บไว้กับเซิร์ฟเวอร์ผู้รับ เช่น ในตัวแปรสภาพแวดล้อม SURSA_WEBHOOK_SECRET

ที่อยู่ผู้รับต้องใช้ https และชี้ไปยังที่อยู่ IP สาธารณะ ที่อยู่ในเครือข่ายส่วนตัว เครือข่าย loopback และเครือข่ายสงวนอื่นจะถูกปฏิเสธทั้งตอนลงทะเบียนและทุกครั้งที่ส่ง หากต้องการพัฒนาบนคอมพิวเตอร์ของคุณเอง ให้เปิดเซิร์ฟเวอร์ผู้รับในเครื่องผ่านบริการทันเนลที่ให้ที่อยู่ HTTPS สาธารณะ

ส่งอีเวนต์ทดสอบได้ทุกเมื่อด้วย POST /v1/webhooks/{webhook_id}/test การตอบกลับจะแสดงว่าเซิร์ฟเวอร์ผู้รับของคุณตอบกลับอย่างไร

2. หน้าตาของการส่ง

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}
เฮดเดอร์ความหมาย
webhook-idID ของอีเวนต์ มีค่าเดิมในทุกการลองใหม่ ใช้ค่านี้ตัดรายการซ้ำ
webhook-timestampเวลาที่ลงนามความพยายามส่งครั้งนี้ เป็นวินาทีแบบ Unix
webhook-signatureลายเซ็นในรูปแบบ Standard Webhooks
X-KB-Signatureการส่งเดียวกันที่ลงนามในรูปแบบเดิมของ Sursa
X-KB-Eventประเภทอีเวนต์
X-KB-Attemptหมายเลขความพยายามส่ง เริ่มจาก 1
X-KB-Sequenceตำแหน่งของการส่งนี้ในลำดับการส่งของเว็บฮุก

ชื่อเฮดเดอร์ไม่แยกตัวพิมพ์ใหญ่และเล็ก เฟรมเวิร์กของคุณอาจแสดงเป็น X-Kb-Signature หรือ x-kb-signature

เนื้อหาคืออีเวนต์ และ data ขึ้นอยู่กับประเภท เว็บฮุกที่ตั้งเพย์โหลดเป็น thin จะได้รับอีเวนต์ที่ไม่มี data ให้อ่านออบเจ็กต์ผ่าน API ด้วยคีย์ของคุณเองแทน

3. ตรวจลายเซ็น

ตรวจลายเซ็นของทุกการส่งก่อนเชื่อถือ ทั้งสองรูปแบบลงนามไบต์ของเนื้อหาตรงตามที่ส่งมา จึงต้องอ่าน เนื้อหาแบบดิบ ก่อนแยกวิเคราะห์ JSON ใด การแปลง JSON กลับเป็นข้อความใหม่จะเปลี่ยนไบต์และทำให้การตรวจล้มเหลว

รูปแบบสตริงที่ลงนามคีย์ลายเซ็น
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>ส่วนของข้อมูลลับหลัง whsec_ ที่ถอดรหัส base64 แล้วv1, + base64 ของ HMAC-SHA256
รูปแบบเดิม (X-KB-Signature)<t>.<body>ข้อมูลลับทั้งสตริงในรูปข้อความv1= + hex ของ HMAC-SHA256

ตรวจรูปแบบใดรูปแบบหนึ่งก็เพียงพอ แนะนำให้ใช้เฮดเดอร์ Standard Webhooks เพราะมีไลบรารีสำเร็จรูปจำนวนมากที่ตรวจแบบเดียวกันได้ ระหว่างการหมุนเวียนข้อมูลลับ แต่ละเฮดเดอร์มีลายเซ็นหนึ่งรายการต่อข้อมูลลับที่ใช้ได้แต่ละค่า (คั่นด้วยช่องว่างใน webhook-signature และใช้ v1= ซ้ำใน X-KB-Signature) การส่งถือว่าถูกต้องหากลายเซ็นใดลายเซ็นหนึ่งตรงกัน

นอกจากนี้ ให้ปฏิเสธการประทับเวลาที่ห่างจากนาฬิกาของคุณเกิน 5 นาที เพื่อป้องกันไม่ให้มีผู้นำการส่งที่บันทึกไว้มาส่งซ้ำในภายหลัง

# 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

ทดสอบโค้ดตรวจลายเซ็น

รันโค้ดของคุณกับค่าเหล่านี้ก่อนเชื่อมต่อกับการส่งจริง เมื่อตั้งนาฬิกาให้ตรงกับการประทับเวลา การตรวจทั้งสองแบบต้องผ่าน และต้องล้มเหลวเมื่อเนื้อหาเปลี่ยนไปแม้เพียงหนึ่งไบต์

ข้อมูลนำเข้าค่า
ข้อมูลลับwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp และ t1791194400
เนื้อหา{"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. ตอบกลับให้เร็ว แล้วค่อยทำงาน

Sursa รอการตอบกลับของคุณได้นานสุด 10 วินาที สถานะ 2xx ใดก็ตามหมายความว่าการส่งสำเร็จ อย่างอื่นทั้งหมด รวมถึงการเปลี่ยนเส้นทาง การหมดเวลา หรือการเชื่อมต่อที่ถูกปิด หมายความว่าการส่งล้มเหลวและจะมีการลองใหม่

ดังนั้นเซิร์ฟเวอร์ผู้รับควรทำเพียงตรวจลายเซ็น บันทึกอีเวนต์ และตอบกลับ แล้วจึงทำงานจริงภายหลังจากคิวของคุณเอง

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

ตอบกลับ 2xx หลังจากเก็บอีเวนต์ไว้อย่างปลอดภัยแล้วเท่านั้น หากตอบกลับก่อนแล้วระบบล่ม อีเวนต์นั้นจะหายไปสำหรับคุณ เพราะ Sursa นับว่าส่งถึงแล้ว

5. การลองใหม่

การส่งที่ล้มเหลวจะลองส่งใหม่โดยเว้นระยะนานขึ้นทุกครั้ง คือหลังประมาณ 1 นาที 5 นาที 30 นาที 2 ชั่วโมง 5 ชั่วโมง 10 ชั่วโมง 24 ชั่วโมง และ 24 ชั่วโมง รวมเก้าความพยายามส่งในเวลาเกือบสามวัน ระยะเว้นแต่ละครั้งคลาดเคลื่อนได้ไม่เกิน 20% เพื่อไม่ให้การลองใหม่จากเว็บฮุกจำนวนมากมาถึงในเวลาเดียวกัน

  • การตอบกลับ 429 หรือ 503 พร้อม Retry-After จะเลื่อนความพยายามส่งครั้งถัดไปออกไปอย่างน้อยตามเวลานั้น ได้ไม่เกิน 24 ชั่วโมง ใช้เมื่อระบบของคุณรับโหลดไม่ไหวหรืออยู่ระหว่างการบำรุงรักษา
  • การตอบกลับ 410 Gone จะ ปิดใช้งาน เว็บฮุก ส่งเฉพาะเมื่อคุณไม่ต้องการรับการส่งที่ที่อยู่นั้นอีก
  • หลังความพยายามส่งล้มเหลว 10 ครั้งติดต่อกัน เว็บฮุกจะ หยุดชั่วคราว และผู้ดูแลระบบขององค์กรจะได้รับแจ้ง อีเวนต์ยังคงสะสมไว้ และเมื่อเปิดใช้เว็บฮุกต่อในหน้าจอผู้ดูแลระบบหรือด้วย POST /v1/webhooks/{webhook_id}/resume อีเวนต์จะถูกส่งตามลำดับ
  • หลังความพยายามส่งครั้งสุดท้าย การส่งจะถูกทำเครื่องหมายเป็น exhausted คุณส่งซ้ำด้วยตนเองได้

การส่งของเว็บฮุก พร้อมแต่ละความพยายามส่ง รหัสการตอบกลับของคุณ และเวลาที่ใช้ แสดงไว้ใน GET /v1/webhooks/{webhook_id}/deliveries และในหน้าจอผู้ดูแลระบบ ส่วน POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver ส่งการส่งนั้นซ้ำ การส่งซ้ำมี webhook-id เดิม

6. ลำดับและการส่งซ้ำ

  • ลำดับ การส่งไปยังเว็บฮุกเดียวกันเป็นไปทีละรายการอย่างเคร่งครัดตามลำดับของ sequence ระหว่างที่การส่งรายการหนึ่งกำลังลองใหม่ รายการถัดไปจะรอ เว็บฮุกต่างรายการกันไม่รอกัน
  • การส่งซ้ำ อีเวนต์เดียวกันอาจมาถึงมากกว่าหนึ่งครั้ง เช่น เมื่อการตอบกลับของคุณสูญหายระหว่างทาง ให้เก็บ webhook-id (ซึ่งเป็น id ในเนื้อหาด้วย) ของทุกอีเวนต์ที่ประมวลผล และข้ามอีเวนต์ที่เคยเห็นแล้ว
  • คิดในแง่ของสถานะ สำหรับการเปลี่ยนแปลงของออบเจ็กต์เดียวกัน ให้เทียบ created_at หรือ sequence กับค่าที่เก็บไว้ และละเว้นอีเวนต์ที่เก่ากว่าสถานะที่คุณมีอยู่แล้ว หากไม่แน่ใจ ให้อ่านออบเจ็กต์ปัจจุบันผ่าน API อีเวนต์บอกว่า มีบางอย่าง เปลี่ยนไป ส่วน API บอกว่า ตอนนี้เป็นอย่างไร

7. ตรวจเทียบกับฟีดอีเวนต์

ทุกอีเวนต์ยังเก็บไว้ใน ฟีดอีเวนต์ เป็นเวลา 30 วัน ไม่ว่าจะมีเว็บฮุกสมัครรับอีเวนต์นั้นหรือไม่ หากเซิร์ฟเวอร์ผู้รับหยุดทำงานนานกว่าช่วงที่การลองใหม่ครอบคลุม หรือคุณต้องการตรวจว่าไม่มีอะไรตกหล่น ให้อ่านฟีดด้วยคีย์ที่มีบทบาท admin และขอบเขตสิทธิ์ 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"

อีเวนต์ในฟีดมี id และโครงสร้างเดียวกับในการส่ง โดยไม่มีฟิลด์ของการส่ง sequence payload และ test เนื่องจากเซิร์ฟเวอร์ผู้รับข้ามอีเวนต์ที่เคยเห็นแล้ว คุณจึงรันการตรวจเทียบได้บ่อยเท่าที่ต้องการ เช่น ชั่วโมงละครั้ง

GET /v1/webhook-events/{event_id} อ่านอีเวนต์หนึ่งรายการด้วย ID

8. หมุนเวียนข้อมูลลับ

POST /v1/webhooks/{webhook_id}/rotate-secret ออกข้อมูลลับใหม่ และให้ข้อมูลลับเดิมใช้ได้ต่อในช่วงเปลี่ยนผ่าน ซึ่งมีค่าเริ่มต้น 24 ชั่วโมงและนานได้ถึง 7 วัน ระหว่างช่วงเปลี่ยนผ่าน ทุกการส่งจะลงนามด้วยข้อมูลลับทั้งสองค่า

  1. หมุนเวียน แล้วเก็บข้อมูลลับใหม่ไว้คู่กับค่าเดิมในเซิร์ฟเวอร์ผู้รับ
  2. ยอมรับการส่งหากตรวจผ่านด้วยข้อมูลลับค่าใดค่าหนึ่ง
  3. หลังช่วงเปลี่ยนผ่าน ให้นำข้อมูลลับเดิมออก

หากข้อมูลลับเดิมรั่วไหล ให้ยุติช่วงเปลี่ยนผ่านทันทีด้วย POST /v1/webhooks/{webhook_id}/revoke-previous-secret หรือหมุนเวียนด้วย "overlap_hours": 0

เช็กลิสต์

  • ตรวจลายเซ็นรูปแบบใดรูปแบบหนึ่งบนเนื้อหาแบบดิบ และตรวจการประทับเวลา
  • เก็บอีเวนต์โดยใช้ webhook-id เป็นคีย์ แล้วตอบกลับ 2xx ภายใน 10 วินาที
  • ข้ามรายการซ้ำและอีเวนต์ที่ล้าสมัย และอ่านออบเจ็กต์ปัจจุบันผ่าน API เมื่อจำเป็น
  • ตอบกลับ 429 หรือ 503 พร้อม Retry-After เมื่อต้องการให้ Sursa ชะลอการส่ง และอย่าเผลอตอบกลับ 410
  • ข้ามประเภทอีเวนต์และฟิลด์ที่ไม่รู้จัก
  • ตรวจเทียบกับฟีดอีเวนต์ตามกำหนดเวลา