استقبال خطافات الويب
يخبر خطاف الويب نظامك بالتغيير في Sursa فور حدوثه: نشر بطاقة أو توثيقها أو أرشفتها، أو سؤال بقي بلا إجابة. يرسل
Sursa طلب POST عبر HTTPS يحمل الحدث إلى عنوان تسجّله. ويبني هذا الدليل مستقبِلًا يعمل بشكل صحيح مع إعادة المحاولة
وتبدّل الترتيب والتكرارات.
لمعرفة معنى كل حدث ومحتواه، انظر النظرة العامة على خطافات الويب وكتالوج الأحداث في مرجع API.
1. سجّل خطاف ويب
يسجّل المشرف خطافات الويب في Sursa من Administration → Integrations → Webhooks and API (الإدارة ← التكاملات ←
خطافات الويب و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 | معرّف الحدث. وهو نفسه في كل إعادة محاولة. استخدمه للتخلص من التكرارات. |
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= ثم الترميز الست عشري لقيمة 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. إعادة المحاولة
يُعاد إرسال التسليم الفاشل بفواصل زمنية متزايدة: بعد دقيقة واحدة تقريبًا، ثم 5 دقائق، و30 دقيقة، وساعتين، و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} حدثًا واحدًا بمعرّفه.
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عن طريق الخطأ أبدًا. - تجاهل أنواع الأحداث والحقول التي لا تعرفها.
- طابق مع سجل الأحداث وفق جدول زمني.