> ## Documentation Index
> Fetch the complete documentation index at: https://docs.linkiasoft.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> نرسل POST إلى نقطة نهايتك عند وقوع أي حدث — الرسائل والتعليقات وتحرك العملاء المحتملين في مسارك.

بدل استطلاعنا بحثًا عن التغييرات، سجّل نقطة نهاية فنرسل إليها POST فور وقوع الأحداث. وصول رسالة،
أو بلوغ عميل محتمل مرحلة «رابح»، أو تعليق على منشور فيسبوك — كل منها يصبح طلب HTTP إلى رابط
تتحكم فيه.

وهذا ما تريده إن كنت تشغّل n8n أو Zapier أو Make أو شيئًا كتبته بنفسك. راجع
[دليل n8n](/ar/guides/n8n-lead-webhook) لمثال عملي.

**تُضبط الـ Webhooks داخل التطبيق لا عبر الـ API.** اذهب إلى **التكاملات ← Webhooks**. فنقطة
النهاية مكان نرسل إليه بيانات عملائك، لذا فإنشاؤها إجراء إداري عمدًا لا شيء يستطيع مفتاح API فعله.

## إعداد واحدة

<Steps>
  <Step title="أضف نقطة النهاية">
    **التكاملات ← Webhooks ← نقطة نهاية جديدة**. أعطها اسمًا ورابط `https://` متاحًا علنًا.
  </Step>

  <Step title="اختر أحداثك">
    حدّد الأحداث التي تريدها. ولكل حدث يمكنك أيضًا تحديد الحقول التي تحملها الحمولة — راجع
    [اختيار الحقول](#choosing-which-fields-to-send).
  </Step>

  <Step title="احفظ مفتاح التوقيع">
    يُعرض **مرة واحدة** عند الإنشاء، ويبدأ بـ`whsec_`. وتحتاجه
    [للتحقق من التوقيعات](#verifying-the-signature). خزّنه في مدير الأسرار لديك قبل إغلاق النافذة.
  </Step>

  <Step title="أرسل اختبارًا">
    استخدم **إرسال اختبار** في صفحة نقطة النهاية. فهو يمر بمسار التسليم الحقيقي — التوقيع نفسه
    والتسجيل نفسه — لذا فالاختبار الذي يصل يثبت أن الإعداد يعمل.
  </Step>
</Steps>

<Warning>
  يجب أن يشير الرابط إلى عنوان عام. فالنطاقات الخاصة و`localhost` وعناوين الرابط المحلي وعناوين
  بيانات السحابة الوصفية مرفوضة، عند حفظ نقطة النهاية وفي كل عملية تسليم أيضًا — فاسم المضيف الذي
  يُعاد توجيهه للداخل لاحقًا يتوقف عن العمل بدل أن يصبح مدخلًا إلى شبكتنا.

  تختبر محليًا؟ استخدم نفقًا مثل ngrok أو رابط الاختبار الخاص بـ n8n، لا `http://localhost`.
</Warning>

## الأحداث

| الحدث                | يقع عندما                                   |
| -------------------- | ------------------------------------------- |
| `message.received`   | يرسل عميل رسالة على أي قناة مربوطة          |
| `message.sent`       | يرسل موظف أو بوت أو أتمتة رسالة             |
| `message.delivered`  | تؤكد المنصة وصول رسالة صادرة إلى العميل     |
| `message.read`       | يقرأ العميل رسالة صادرة                     |
| `message.failed`     | تعذّر تسليم رسالة صادرة                     |
| `comment.received`   | يعلّق أحدهم على منشور في فيسبوك أو انستجرام |
| `lead.created`       | يُنشأ عميل محتمل من أي مصدر                 |
| `lead.updated`       | تُعدَّل بيانات عميل محتمل                   |
| `lead.stage.changed` | ينتقل عميل محتمل إلى مرحلة أخرى             |
| `lead.assigned`      | يُسنَد عميل محتمل إلى مستخدم آخر            |
| `lead.won`           | يبلغ عميل محتمل مرحلة موسومة بالربح         |
| `lead.lost`          | يبلغ عميل محتمل مرحلة موسومة بالخسارة       |

<Note>
  تعديل مرحلة عميل محتمل يُطلق **كلا** الحدثين `lead.updated` و`lead.stage.changed`. اشترك في ما
  تقصده فعلًا — فأخذ الاثنين يعني معالجة كل نقلة مرتين.

  و`lead.won` و`lead.lost` يتبعان علامتَي الربح/الخسارة على مراحل مسارك لا اسم المرحلة. فإن غيّرت
  اسم «رابح» إلى «مغلقة — موقّعة»، فستظل تعمل.
</Note>

## الحمولة

كل عملية تسليم لها المغلّف نفسه. ولا يختلف سوى `data` بحسب الحدث:

```json theme={null}
{
  "id": "6f1c8e2a-9b4d-4c7e-8a13-2d5f0b7c9e41",
  "event": "lead.stage.changed",
  "source": "crm",
  "tenant_id": "8f6a2b3c-4d5e-4a7b-9c1e-3f2b1a9c0d4e",
  "occurred_at": "2026-07-26T09:14:03.221Z",
  "data": {
    "lead_id": "b21e7d40-3a55-4f89-9c02-77ab1e6d3c88",
    "name": "Nadia Farouk",
    "status": "negotiation",
    "previous_status": "qualified",
    "value": 25000,
    "currency": "EGP"
  }
}
```

| الحقل         | ما هو                                                                                        |
| ------------- | -------------------------------------------------------------------------------------------- |
| `id`          | فريد لكل عملية تسليم. استخدمه لجعل معالجك مقاومًا للتكرار — راجع [إعادة المحاولة](#retries). |
| `event`       | أي حدث وقع.                                                                                  |
| `source`      | `crm` أو `social` — أي جانب من المنتج أنتجه.                                                 |
| `occurred_at` | وقت وقوع الحدث، لا وقت إرسالنا له. وإعادات المحاولة تحتفظ بالأصلي.                           |
| `data`        | حقول الحدث، محصورة بما اشتركت فيه.                                                           |

و`source` يستحق الاستخدام إذا كان سير عملك يكتب مجددًا داخل لينكياسوفت: فهو يتيح لك تمييز التغيير
الذي أحدثته أتمتتك من الذي أحدثه إنسان، وهكذا تتجنب الحلقات المفرغة.

<h2 id="choosing-which-fields-to-send">
  اختيار الحقول المرسلة
</h2>

لكل حدث مجموعة حقول، وأنت تختار ما نُضمّنه. أزل تحديد أي شيء لا يحتاجه النظام المستقبِل —
وخصوصًا `text` و`customer_phone` و`email`، فهي أقل الحقول التي تود بقاءها في سجلات أداة طرف ثالث.

وترك **كل** الحقول محددة ليس كسردها واحدًا واحدًا. فتحديد الكل يعني «أرسل ما يحمله هذا الحدث»،
فيُضمَّن أي حقل نضيفه لاحقًا تلقائيًا. أما إن أزلت تحديد واحد فقط، فستحصل على المجموعة التي اخترتها
بالضبط ولا شيء جديد.

<h2 id="verifying-the-signature">
  التحقق من التوقيع
</h2>

كل طلب يحمل هذه الترويسات:

| الترويسة             | مثال                    |
| -------------------- | ----------------------- |
| `X-Linkia-Signature` | `t=1753500000,v1=5f3a…` |
| `X-Linkia-Event`     | `lead.stage.changed`    |
| `X-Linkia-Delivery`  | قيمة `id` في المغلّف    |
| `X-Linkia-Attempt`   | `1`                     |

و`v1` هو HMAC-SHA256 بمفتاح التوقيع لديك، على النص
`` `${t}.${rawBody}` `` — الطابع الزمني، ثم نقطة حرفية، ثم جسم الطلب **الخام**.

<Warning>
  وقّع الجسم الخام كما ورد تمامًا. فإذا حلّل إطار العمل لديك الـ JSON وأعدت أنت تسلسله قبل التجزئة،
  سيختلف ترتيب المفاتيح أو المسافات وستفشل كل التوقيعات. ومعظم أطر العمل تحتاج أن تُخبرها بالاحتفاظ
  بالجسم الخام.
</Warning>

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from 'node:crypto';

  function verify(rawBody, header, secret, toleranceSeconds = 300) {
    const parts = Object.fromEntries(
      header.split(',').map((p) => p.trim().split('='))
    );

    const timestamp = Number(parts.t);
    if (!Number.isFinite(timestamp)) return false;

    // Reject anything too old to be a live delivery — this is what stops someone
    // replaying a payload they captured earlier.
    if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

    const expected = crypto
      .createHmac('sha256', secret)
      .update(`${timestamp}.${rawBody}`)
      .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(parts.v1 ?? '');
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

  ```python Python theme={null}
  import hashlib, hmac, time

  def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
      parts = dict(p.strip().split("=", 1) for p in header.split(","))

      try:
          timestamp = int(parts["t"])
      except (KeyError, ValueError):
          return False

      if abs(time.time() - timestamp) > tolerance:
          return False

      expected = hmac.new(
          secret.encode(),
          f"{timestamp}.".encode() + raw_body,
          hashlib.sha256,
      ).hexdigest()

      return hmac.compare_digest(expected, parts.get("v1", ""))
  ```

  ```php PHP theme={null}
  function verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
      $parts = [];
      foreach (explode(',', $header) as $piece) {
          [$k, $v] = array_pad(explode('=', trim($piece), 2), 2, null);
          $parts[$k] = $v;
      }

      if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) return false;
      if (abs(time() - (int) $parts['t']) > $tolerance) return false;

      $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
      return hash_equals($expected, $parts['v1']);
  }
  ```
</CodeGroup>

قارن البصمات بدالة ثابتة الزمن — `timingSafeEqual` أو `compare_digest` أو `hash_equals` — لا
بـ`==`.

<Note>
  الطابع الزمني داخل النص الموقّع عمدًا. فتوقيع الجسم وحده كان سيجعل كل عملية تسليم تستقبلها قابلة
  لإعادة التشغيل من أي شخص التقط واحدة، إلى الأبد. وفحص `t` مقابل هامش تسامح هو النصف الذي يجعله
  مفيدًا، فلا تتخطاه.
</Note>

<h2 id="retries">
  إعادة المحاولة
</h2>

رُد بـ`2xx` ونعتبر التسليم منجزًا. وأي شيء آخر:

| الاستجابة           | ما يحدث                                                        |
| ------------------- | -------------------------------------------------------------- |
| `2xx`               | نجاح. ويُصفَّر عدّاد الإخفاقات المتتالية.                      |
| `4xx`               | **نهائي.** فقد أخبرتنا أن الطلب خاطئ — وإعادته لن تغيّر شيئًا. |
| `5xx` و`429` و`408` | يُعاد.                                                         |
| مهلة أو خطأ اتصال   | يُعاد. وننتظر 10 ثوانٍ للاستجابة.                              |

وإعادات المحاولة تتباعد: **30 ثانية، دقيقتان، 10 دقائق، ساعة، 6 ساعات** — خمس محاولات على مدى
ثماني ساعات تقريبًا، ثم نتوقف. وكل محاولة تظهر في السجل برقم `X-Linkia-Attempt` خاص بها، وكل
محاولات التسليم الواحد تتشارك `id` المغلّف نفسه.

<Warning>
  التسليم **مرة واحدة على الأقل**. فعطل شبكة بعد نجاح معالجك وقبل وصول `2xx` إلينا يعني أنك سترى
  الحدث نفسه مجددًا. وإذا كانت إعادة المعالجة ستضاعف خصمًا ماليًا، أو ترسل رسالة مكررة، أو تنشئ
  سجلًا ثانيًا، فأزل التكرار بالاعتماد على `id` المغلّف — فهو ثابت عبر كل المحاولات.
</Warning>

**عشرون إخفاقًا متتاليًا تعطّل نقطة النهاية.** فنتوقف عن استدعائها، وتعرض الصفحة السبب، وتتوقف
الأحداث عن الاصطفاف لها. وهذا موجود حتى لا يولّد رابط اختبار مهجور عمليات تسليم فاشلة إلى الأبد.
أصلح نقطة النهاية واضغط **تفعيل من جديد** — فذلك يصفّر العدّاد. ولا يُعاد إرسال شيء تلقائيًا؛
استخدم السجل لإعادة إرسال ما يهم.

## سجل التسليم

تحتفظ كل نقطة نهاية بـ**آخر 300 محاولة**، مع الجسم الذي أرسلناه بالضبط، وأول 2 كيلوبايت من
استجابتك، ورمز الحالة، والمدة المستغرقة. افتح أي منها لرؤية الطلب والاستجابة جنبًا إلى جنب، أو
اضغط أيقونة الإعادة لإرسالها ثانية.

أما الإجماليات الكلية — كم طلبًا أرسلنا وكم فشل — فتُحسب منفصلة و**لا** تتأثر بحد الـ 300 صفًا.

<Note>
  السجل أداة تصحيح لا أرشيف. فعلى نقطة نهاية مزدحمة قد تستغرق 300 محاولة أقل من ساعة. وإن كنت
  تحتاج تاريخًا دائمًا، فسجّل عمليات التسليم لديك مفهرسة بـ`id` المغلّف.
</Note>

## الترويسات المخصصة

إذا كانت نقطة نهايتك خلف وسيط يريد ترويسة خاصة به، فأضفها من إعدادات نقطة النهاية. أما الترويسات
التي نضبطها نحن — `Content-Type` و`User-Agent` وكل ترويسة `X-Linkia-*` — فلا يمكن تجاوزها، لأن
ترويسة مخصصة تستطيع استبدال التوقيع ستجعل تزوير عمليات التسليم أمرًا تافهًا.

## تدوير المفتاح

**تدوير المفتاح** في صفحة نقطة النهاية يصدر مفتاحًا جديدًا ويعرضه مرة واحدة.

<Warning>
  لا توجد فترة تداخل. فالمفتاح القديم يتوقف عن التحقق لحظة إصدار الجديد، لذا انشر المفتاح الجديد
  على المستقبِل لديك أولًا، أو توقّع إخفاقات بينهما. وتلك الإخفاقات تُحتسب ضمن عدّاد التعطيل التلقائي.
</Warning>

## أمور تراقبها في الإنتاج

**نقطة النهاية البطيئة تكلّفك أحداثًا لا تكلّفنا.** فنحن ننتظر 10 ثوانٍ. وإذا كان معالجك يؤدي عملًا
حقيقيًا — استدعاء واجهات أخرى، أو الكتابة في قاعدة بيانات بطيئة — فرُد بـ`2xx` فورًا وعالج في
الخلفية. فالمعالج الذي يرد في 11 ثانية يبدو مطابقًا لمعالج متوقف.

**لا تُرجع `4xx` لمشكلاتك أنت.** فـ`422` بسبب تحقق مفرط الصرامة نهائي من جانبنا: لن نعيد المحاولة،
والحدث يضيع. أرجع `5xx` لأي شيء تريد محاولة أخرى له.

**عمليات التسليم الاختبارية تبدو كالحقيقية.** فـ`webhook.test` يصل عبر المسار نفسه بتوقيع صالح.
عالج اسم الحدث أو تجاهله؛ ولا تفترض أن كل عملية تسليم حدث حقيقي في نظامك.
