---
title: "Проверка на подписите на webhook заявки"
description: "Всяка 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
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
```

Предпочитайте суровото тяло — това е с една стъпка по-малко и не се влияе от
особености при повторното преобразуване на JSON числа в някои езици.

## Коя тайна?

| Източник | Тайна |
|--------|--------|
| [Webhook крайна точка](/bg/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | `secret` за конкретната крайна точка (48 шестнадесетични знака), върната еднократно при създаване |
| [Остарял webhook с един URL](/api-reference/organizations#legacy-single-url-webhook) | `secret` за организацията, върната при `GET /v1/webhook` |
| [Извикване на крайна точка за инструмент](/bg/tools/overview) (директно извикване към вашия `endpoint.url`) | **Webhook тайната на ниво организация** (същата като за остарелия webhook с един URL) — не тайна за конкретна крайна точка |

Съхранявайте тайната във вашия мениджър за тайни или променлива на средата — никога не я записвайте в хранилището с код.

## Референтни имплементации

И четирите проверяват суровото тяло на заявката:

<CodeGroup>
```python Python
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
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
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
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
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
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
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>

## Проверка на извиквания на инструменти

Когато агентът извика директно един от вашите
[инструменти с функции](/bg/tools/overview) (инструментът има
`endpoint`), заявката съдържа два заглавни реда на ThunderPhone наред
с конфигурираните от вас `endpoint.headers`:

- `X-ThunderPhone-Call-ID` — числовият идентификатор на текущото обаждане.
- `X-ThunderPhone-Signature` — HMAC-SHA256, с ключ вашата
  **тайна за уебхук на ниво организация**, върху точните байтове на тялото на заявката.

Същият помощен метод `verify()` работи без промени, с две особености:

1. **Инструментите `GET` / `DELETE` нямат тяло.** Аргументите се предават като параметри на заявката, а подписът се изчислява върху **празния байтов низ** — тоест `verify(b"", sig, secret)` (Python) или `verify(Buffer.alloc(0), sig, secret)` (Node). **Не** хеширайте низа на заявката.
2. **Организациите без конфигуриран наследен уебхук нямат тайна на организацията.** В този случай извикванията на инструменти съдържат само `X-ThunderPhone-Call-ID` и нямат заглавен ред за подпис. Конфигурирайте наследения уебхук (`PUT /v1/webhook`), за да получите тайна за подписване, или удостоверявайте извикванията на инструменти със собствен заглавен ред чрез `endpoint.headers`.

```python
@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)
    ...
```

Изпращането на инструменти в режим на уебхук (инструменти без `endpoint`, доставяни
до уебхука на вашата организация като `telephony.tool` / `web.tool`) е обикновен
подписан уебхук — приложете стандартната процедура по-горе. Вижте
[Инструменти с функции](/bg/tools/overview) за двата формата на заявки.

## Често срещани проблеми

<AccordionGroup>
  <Accordion title="Повторна сериализация с форматиране по подразбиране">
    Анализирането на тялото и повторното му записване с настройките по
    подразбиране на вашата JSON библиотека (интервали след `,` / `:`, ключове
    в реда на вмъкване) създава различни байтове и нарушава HMAC. Проверявайте
    необработеното тяло — или ако трябва да го сериализирате повторно, спазвайте
    точно нашата канонична форма: сортирани ключове, компактни разделители, UTF-8.
  </Accordion>

  <Accordion title="Рамката автоматично анализира JSON">
    Междинният обработчик `express.json()` на Express консумира потока на тялото
    и губите необработените байтове. Използвайте `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="/bg/webhooks/overview">
    Семантика на доставката, повторни опити, IP адреси на източника.
  </Card>
  <Card title="Крайни точки за уебхукове" icon="plug" href="/bg/webhooks/endpoints">
    Управлявайте множество URL адреси, сменяйте секретни ключове.
  </Card>
  <Card title="Функционални инструменти" icon="screwdriver-wrench" href="/bg/tools/overview">
    Двата начина за извикване на инструменти и форматите на заявките им.
  </Card>
  <Card title="Интеграции с инструменти" icon="wrench" href="/bg/guides/build-tool-integration">
    Създайте цялостна интеграция с инструменти от край до край.
  </Card>
</CardGroup>
