> ## 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.

# אימות חתימות של webhooks

> כל בקשת 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](/he/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)             | `secret` לכל נקודת קצה (48 תווי hex) המוחזר פעם אחת בעת היצירה                                             |
| [webhook מדור קודם עם כתובת URL יחידה](/api-reference/organizations#legacy-single-url-webhook) | `secret` לכל ארגון המוחזר ב-`GET /v1/webhook`                                                              |
| [הפעלה של נקודת קצה לכלים](/he/tools/overview) (קריאה ישירה אל `endpoint.url` שלכם)            | **סוד ה-webhook ברמת הארגון** (אותו סוד כמו ב-webhook מדור קודם עם כתובת URL יחידה) — לא סוד לכל נקודת קצה |

אחסנו את הסוד במנהל הסודות או במשתנה הסביבה שלכם — לעולם אל תבצעו לו commit.

## מימושי עזר

כל ארבעת המימושים מאמתים את גוף הבקשה הגולמי:

<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>

## אימות קריאות לכלים

כאשר הסוכן מפעיל ישירות אחד מ-
[כלי הפונקציות](/he/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 חתום
רגיל — ההנחיות הסטנדרטיות שלמעלה חלות. ראו
[כלי פונקציות](/he/tools/overview) עבור שני מבני הבקשות.

## מלכודות נפוצות

<AccordionGroup>
  <Accordion title="סריאליזציה מחדש עם עיצוב ברירת מחדל">
    ניתוח הגוף וייצואו מחדש עם הגדרות ברירת המחדל של ספריית ה-JSON שלכם
    (רווחים אחרי `,` / `:`, מפתחות בסדר ההוספה) יוצר
    בתים שונים ושובר את ה-HMAC. אמתו את הגוף הגולמי — או אם
    אתם חייבים לבצע סריאליזציה מחדש, התאימו בדיוק לצורה הקנונית שלנו: מפתחות
    ממוינים, מפרידים קומפקטיים, UTF-8.
  </Accordion>

  <Accordion title="המסגרת מנתחת JSON אוטומטית">
    תוכנת התווך `express.json()` של Express צורכת את זרם הגוף
    ואתם מאבדים את הבתים הגולמיים. השתמשו ב-`express.raw()` במיוחד בנתיב
    ה-webhook, או אחסנו את הגוף הגולמי במאגר בתוכנת תווך מוקדמת.
    אותו הדבר נכון לגבי NestJS / Koa — עיינו בתיעוד שלהם בנושא "raw body".
  </Accordion>

  <Accordion title="השוואה שאינה בטוחה מבחינת תזמון">
    `expected === signature` ב-JS או `expected == signature` ב-
    Python הן השוואות שזמן הביצוע שלהן משתנה. השתמשו ב-`crypto.timingSafeEqual`
    או ב-`hmac.compare_digest` בהתאמה. ההבדל בביצועים
    זניח.
  </Accordion>

  <Accordion title="סוד שגוי עבור נקודות קצה של כלים">
    קריאות ישירות לנקודות קצה של כלים נחתמות באמצעות **סוד ה-webhook
    ברמת הארגון** (`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="סקירת webhooks" icon="bolt" href="/he/webhooks/overview">
    סמנטיקת מסירה, ניסיונות חוזרים, כתובות IP של המקור.
  </Card>

  <Card title="נקודות קצה של webhook" icon="plug" href="/he/webhooks/endpoints">
    נהלו כמה כתובות URL, החליפו סודות.
  </Card>

  <Card title="כלי פונקציות" icon="screwdriver-wrench" href="/he/tools/overview">
    שני נתיבי הפעלת הכלים ומבני הבקשות שלהם.
  </Card>

  <Card title="שילובי כלים" icon="wrench" href="/he/guides/build-tool-integration">
    בנו שילוב מלא המבוסס על כלים מקצה לקצה.
  </Card>
</CardGroup>
