รับเว็บฮุก
เว็บฮุกแจ้งระบบของคุณถึงการเปลี่ยนแปลงใน 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-id | ID ของอีเวนต์ มีค่าเดิมในทุกการลองใหม่ ใช้ค่านี้ตัดรายการซ้ำ |
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-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp และ t | 1791194400 |
| เนื้อหา | {"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"data":{"card":{"id":"c1"}}} |
webhook-signature | v1,+xxWSXH6weQJ/MVg4+N78a1Dj4y/AQ4p94+PRNzpECg= |
X-KB-Signature | t=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 วัน ระหว่างช่วงเปลี่ยนผ่าน ทุกการส่งจะลงนามด้วยข้อมูลลับทั้งสองค่า
- หมุนเวียน แล้วเก็บข้อมูลลับใหม่ไว้คู่กับค่าเดิมในเซิร์ฟเวอร์ผู้รับ
- ยอมรับการส่งหากตรวจผ่านด้วยข้อมูลลับค่าใดค่าหนึ่ง
- หลังช่วงเปลี่ยนผ่าน ให้นำข้อมูลลับเดิมออก
หากข้อมูลลับเดิมรั่วไหล ให้ยุติช่วงเปลี่ยนผ่านทันทีด้วย POST /v1/webhooks/{webhook_id}/revoke-previous-secret หรือหมุนเวียนด้วย "overlap_hours": 0
เช็กลิสต์
- ตรวจลายเซ็นรูปแบบใดรูปแบบหนึ่งบนเนื้อหาแบบดิบ และตรวจการประทับเวลา
- เก็บอีเวนต์โดยใช้
webhook-idเป็นคีย์ แล้วตอบกลับ2xxภายใน 10 วินาที - ข้ามรายการซ้ำและอีเวนต์ที่ล้าสมัย และอ่านออบเจ็กต์ปัจจุบันผ่าน API เมื่อจำเป็น
- ตอบกลับ
429หรือ503พร้อมRetry-Afterเมื่อต้องการให้ Sursa ชะลอการส่ง และอย่าเผลอตอบกลับ410 - ข้ามประเภทอีเวนต์และฟิลด์ที่ไม่รู้จัก
- ตรวจเทียบกับฟีดอีเวนต์ตามกำหนดเวลา