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

# التحقق من تواقيع webhook

> كل طلب webhook وأداة صادر من ThunderPhone موقّع. تحقّق مرة واحدة؛ وأعِد استخدامه في كل مكان.

كل طلب نرسله إلى خادمك — تسليمات webhook واستدعاءات نقاط نهاية الأدوات — يحمل توقيع HMAC-SHA256 في ترويسة
`X-ThunderPhone-Signature`. نفّذ التحقق بشكل صحيح مرة واحدة
وأدرج الدالة المساعدة نفسها في كل معالج.

## الخوارزمية

1. اقرأ نص الطلب **الخام** — البايتات الدقيقة التي أرسلناها إليك عبر POST.
2. احسب `hmac_sha256(secret, body).hexdigest()`.
3. قارنه في **وقت ثابت** مع `X-ThunderPhone-Signature`.
   (تؤدي مقارنة السلاسل النصية البسيطة إلى كشف معلومات التوقيت.)

نوقّع البايتات التي نرسلها بدقة، لذا يعمل التحقق من النص الخام
دائمًا. هذه البايتات هي أيضًا **تسلسل JSON القياسي**
للحمولة — مفاتيح مرتبة أبجديًا، وفواصل مختصرة
(`,` و `:` من دون مسافات)، وترميز UTF-8. يمنحك ذلك طريقة ثانية
مكافئة تمامًا عندما لا يكشف إطار العمل لديك إلا JSON بعد تحليله:
أعِد تسلسله بالشكل القياسي واحسب HMAC له.

```python theme={null}
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
```

فضّل النص الخام — فهو خطوة أقل ومحمي من مشكلات إعادة تحويل أرقام JSON
في بعض اللغات.

## أي سر؟

| المصدر                                                                                   | السر                                                                                                |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [نقطة نهاية webhook](/ar/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)         | `secret` لكل نقطة نهاية (48 حرفًا سداسيًا عشريًا) يُعاد مرة واحدة عند الإنشاء                       |
| [webhook القديم بعنوان URL واحد](/api-reference/organizations#legacy-single-url-webhook) | `secret` لكل مؤسسة يُعاد عند `GET /v1/webhook`                                                      |
| [استدعاء نقطة نهاية أداة](/ar/tools/overview) (استدعاء مباشر إلى `endpoint.url` لديك)    | **سر webhook على مستوى المؤسسة** (نفس سر webhook القديم بعنوان URL واحد) — وليس سرًا لكل نقطة نهاية |

خزّن السر في مدير الأسرار أو متغير البيئة لديك — ولا تُضمّنه في المستودع مطلقًا.

## عمليات تنفيذ مرجعية

تتحقق التطبيقات الأربعة جميعها من نص الطلب الخام:

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac


  def verify(body: bytes, signature: str, secret: str) -> bool:
      """Constant-time HMAC-SHA256 verification."""
      expected = hmac.new(
          secret.encode("utf-8"),
          body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature or "")
  ```

  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  export function verify(body, signature, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex");
    if (!signature || expected.length !== signature.length) return false;
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature),
    );
  }
  ```

  ```go Go theme={null}
  package webhook

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
  )

  func Verify(body []byte, signature, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(body)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(expected), []byte(signature))
  }
  ```

  ```ruby Ruby theme={null}
  require "openssl"

  def verify(body, signature, secret)
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
    Rack::Utils.secure_compare(expected, signature.to_s)
  end
  ```
</CodeGroup>

## التوصيل الخاص بإطار العمل

<CodeGroup>
  ```python FastAPI theme={null}
  from fastapi import FastAPI, HTTPException, Request

  app = FastAPI()

  @app.post("/thunderphone-webhook")
  async def hook(request: Request):
      body = await request.body()           # raw bytes, NOT request.json()
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify(body, sig, SECRET):
          raise HTTPException(status_code=401)

      import json
      event = json.loads(body)
      # … dispatch on event["type"] …
      return {"ok": True}
  ```

  ```javascript Express theme={null}
  import express from "express";

  const app = express();

  app.post(
    "/thunderphone-webhook",
    // IMPORTANT: parse as raw; do NOT use express.json() here.
    express.raw({ type: "application/json" }),
    (req, res) => {
      const sig = req.header("X-ThunderPhone-Signature") || "";
      if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
        return res.sendStatus(401);
      }
      const event = JSON.parse(req.body.toString("utf8"));
      // … dispatch on event.type …
      res.sendStatus(204);
    },
  );
  ```

  ```python Django theme={null}
  import json

  from django.http import JsonResponse, HttpResponseForbidden
  from django.views.decorators.csrf import csrf_exempt
  from django.views.decorators.http import require_POST


  @csrf_exempt
  @require_POST
  def hook(request):
      body = request.body  # raw bytes
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify(body, sig, SECRET):
          return HttpResponseForbidden("invalid signature")
      event = json.loads(body)
      # … dispatch on event["type"] …
      return JsonResponse({"ok": True})
  ```
</CodeGroup>

## التحقق من استدعاءات الأدوات

عندما يستدعي الوكيل إحدى
[أدوات الدوال](/ar/tools/overview) مباشرةً (تحتوي الأداة على
`endpoint`)، يتضمن الطلب رأسي ThunderPhone التاليين إلى جانب
`endpoint.headers` الذي أعددته:

* `X-ThunderPhone-Call-ID` — المعرّف الرقمي للمكالمة المباشرة.
* `X-ThunderPhone-Signature` — HMAC-SHA256، باستخدام
  **سر webhook على مستوى المؤسسة**، على بايتات نص الطلب الدقيقة.

تعمل دالة المساعدة `verify()` نفسها دون تغيير، مع اختلافين:

1. **لا تحتوي أدوات `GET` / `DELETE` على نص.** تنتقل الوسائط كمعلمات
   استعلام، ويُحتسب التوقيع على **سلسلة البايتات الفارغة** — لذا استخدم
   `verify(b"", sig, secret)` ‏(Python) أو
   `verify(Buffer.alloc(0), sig, secret)` ‏(Node). لا تُجزّئ
   سلسلة الاستعلام.
2. **المؤسسات التي ليس لديها webhook قديم مُعدّ لا تملك سر مؤسسة.** في
   هذه الحالة، لا تتضمن استدعاءات الأدوات سوى `X-ThunderPhone-Call-ID`
   ولا تتضمن رأس توقيع. أعدّ webhook القديم
   (`PUT /v1/webhook`) للحصول على سر توقيع، أو صادق استدعاءات الأدوات
   باستخدام رأسك الخاص عبر `endpoint.headers`.

```python theme={null}
@app.post("/tools/search-appointments")
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    if not verify(body, sig, ORG_WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    args = json.loads(body)
    ...
```

إن إرسال الأدوات في وضع webhook (الأدوات التي لا تحتوي على `endpoint`، وتُسلَّم
إلى webhook مؤسستك بصيغة `telephony.tool` / `web.tool`) هو webhook عادي
موقّع — تنطبق الوصفة القياسية أعلاه. راجع
[أدوات الدوال](/ar/tools/overview) لكلا شكلي الطلب.

## الأخطاء الشائعة

<AccordionGroup>
  <Accordion title="إعادة التسلسل بالتنسيق الافتراضي">
    يؤدي تحليل النص وإعادة تفريغه باستخدام الإعدادات الافتراضية لمكتبة JSON
    (مسافات بعد `,` / `:`، ومفاتيح مرتبة حسب الإدراج) إلى إنتاج
    بايتات مختلفة وكسر HMAC. تحقّق من النص الخام — أو إذا اضطررت إلى
    إعادة التسلسل، فطابق صيغتنا القياسية تمامًا: مفاتيح مرتبة، وفواصل مضغوطة، وترميز UTF-8.
  </Accordion>

  <Accordion title="إطار العمل يحلل JSON تلقائيًا">
    تستهلك برمجية Express الوسيطة `express.json()` تدفق النص
    وتفقد البايتات الخام. استخدم `express.raw()` تحديدًا على مسار الويبهوك،
    أو خزّن النص الخام مؤقتًا في برمجية وسيطة مسبقة.
    وينطبق الأمر نفسه على NestJS / Koa — راجع وثائقهما حول "النص الخام".
  </Accordion>

  <Accordion title="مقارنة غير آمنة زمنيًا">
    تمثل `expected === signature` في JS أو `expected == signature` في
    Python مقارنات يتغير توقيتها. استخدم `crypto.timingSafeEqual`
    أو `hmac.compare_digest` على التوالي. فرق الأداء
    معدوم.
  </Accordion>

  <Accordion title="سر غير صحيح لنقاط نهاية الأدوات">
    تُوقَّع استدعاءات نقاط نهاية الأدوات المباشرة باستخدام **سر الويبهوك
    على مستوى المؤسسة** (`GET /v1/webhook`) — وليس باستخدام أي سر لكل نقطة نهاية
    من `/v1/developer/webhook-endpoints`. أعد استخدام دالة `verify()`
    نفسها، لكن تأكد من تمرير سر المؤسسة إليها في مسارات الأدوات.
  </Accordion>

  <Accordion title="تجزئة سلسلة الاستعلام في أدوات GET/DELETE">
    بالنسبة إلى أساليب الأدوات التي لا تحتوي على نص، يغطي التوقيع سلسلة البايتات
    الفارغة، مما يحافظ على وصفة عامة واحدة: طبّق HMAC على نص الطلب الخام،
    مهما كان. لن تتطابق تجزئة عنوان URL أو سلسلة الاستعلام أبدًا.
  </Accordion>

  <Accordion title="عدم إرجاع 401 عند عدم التطابق">
    يؤدي إرجاع 200 عند فشل التحقق إلى جعل المعالج هدفًا لإعادة الإرسال.
    أعد دائمًا استجابة غير 2xx إذا فشل التحقق.
  </Accordion>
</AccordionGroup>

***

## الخطوات التالية

<CardGroup cols={2}>
  <Card title="نظرة عامة على الويبهوكات" icon="bolt" href="/ar/webhooks/overview">
    دلالات التسليم، وإعادات المحاولة، وعناوين IP المصدر.
  </Card>

  <Card title="نقاط نهاية الويبهوك" icon="plug" href="/ar/webhooks/endpoints">
    أدر عناوين URL متعددة، ودوّر الأسرار.
  </Card>

  <Card title="أدوات الدوال" icon="screwdriver-wrench" href="/ar/tools/overview">
    مسارا استدعاء الأدوات وأشكال طلباتهما.
  </Card>

  <Card title="تكاملات الأدوات" icon="wrench" href="/ar/guides/build-tool-integration">
    أنشئ تكاملًا كاملًا مدعومًا بأداة من البداية إلى النهاية.
  </Card>
</CardGroup>
