انتقل إلى المحتوى
Sursa Developers

استقبال خطافات الويب

يخبر خطاف الويب نظامك بالتغيير في 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-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. إعادة المحاولة

يُعاد إرسال التسليم الفاشل بفواصل زمنية متزايدة: بعد دقيقة واحدة تقريبًا، ثم 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 أيام. وفي فترة التداخل، يُوقَّع كل تسليم بالسرّين.

  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 عن طريق الخطأ أبدًا.
  • تجاهل أنواع الأحداث والحقول التي لا تعرفها.
  • طابق مع سجل الأحداث وفق جدول زمني.