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

# Обзор вебхуков

> Как ThunderPhone доставляет события в реальном времени, как проверять подписи и чем отличаются устаревшая и основанная на конечных точках модели доставки.

ThunderPhone отправляет HTTP-запросы `POST` на ваш сервер, когда во время звонка
происходят события: начинается входящий звонок, заканчивается звонок, завершается
запуск оценки, срабатывает оповещение и так далее. Есть **две модели доставки**:

<CardGroup cols={2}>
  <Card title="Конечные точки вебхуков (рекомендуется)" icon="bolt" href="/ru/webhooks/endpoints">
    Несколько URL, секреты для каждой конечной точки, фильтры событий для каждой
    конечной точки и автоматические повторные попытки.
    Управление через `GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints`.
  </Card>

  <Card title="Устаревший вебхук с одним URL" icon="link" href="/api-reference/organizations#legacy-single-url-webhook">
    Один URL на организацию. Передаёт события жизненного цикла звонка, включая
    **блокирующие** обмены конфигурацией. Управляется через `GET/PUT /v1/webhook`.
  </Card>
</CardGroup>

Все десять типов событий из [каталога событий](/ru/webhooks/events)
доставляются через конечные точки вебхуков. Шесть событий жизненного цикла звонка
(`telephony.incoming`, `telephony.complete`, `telephony.tool`,
`web.incoming`, `web.complete`, `web.tool`) **также** отправляются в
устаревший вебхук с одним URL — если у вас есть и устаревший URL, и
подходящая конечная точка, вы получите событие по **обоим** путям. Блокирующее
поведение (обмен конфигурацией
[`telephony.incoming` / `web.incoming`](/ru/webhooks/call-incoming) и
[диспетчеризация инструментов](/ru/tools/overview) в режиме вебхуков)
доступно исключительно по устаревшему пути; каждая доставка на конечную точку — это
уведомление без ожидания ответа.

## Формат полезной нагрузки

Доставки на конечные точки представляют собой JSON-объект с полями `data`, `event_id` и
`type`:

```json theme={null}
{
  "data": {
    "call_id": 987654321,
    "from_number": "+14155550199",
    "to_number": "+15551234567"
  },
  "event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
  "type": "telephony.incoming"
}
```

`event_id` уникален для каждого отправленного события. Он остаётся одинаковым при повторных попытках
**и** для каждой конечной точки, получающей событие — используйте его для дедупликации.

Устаревший вебхук с одним URL отправляет те же `type` и `data`, но
**без** `event_id`:

```json theme={null}
{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}
```

При передаче каждое тело сериализуется канонически — ключи отсортированы
по алфавиту, без пробелов, в UTF-8. Примеры с форматированием в
этой документации приведены только для удобства чтения.

Полный список типов событий и полей полезной нагрузки смотрите в
[каталоге событий](/ru/webhooks/events).

## Проверка подписи

Каждый запрос содержит подпись HMAC-SHA256 для **исходного тела
запроса** в заголовке `X-ThunderPhone-Signature`. Ключ подписи — это
`secret` конечной точки (или `secret` вебхука на уровне организации для устаревших
доставок).

### Шаги

1. Прочитайте исходное тело запроса **до** любого парсинга.
2. Вычислите `hmac_sha256(secret, body).hexdigest()`.
3. Сравните за константное время с заголовком `X-ThunderPhone-Signature`.

Мы подписываем в точности те байты, которые отправляем, и эти байты представляют собой
каноническую сериализацию JSON (отсортированные ключи, компактные разделители). Поэтому
проверка по исходному телу всегда работает — а если ваш фреймворк
предоставляет только распарсенный JSON, повторная сериализация с отсортированными ключами и
компактными разделителями создаёт идентичные байты. Оба способа
описаны в [руководстве по проверке](/ru/guides/verify-webhook-signatures).

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

  def verify_signature(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode("utf-8"),
          body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature or "")

  # Example Flask handler
  from flask import Flask, request, abort
  app = Flask(__name__)

  @app.post("/thunderphone-webhook")
  def handle():
      body = request.get_data()
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify_signature(body, sig, WEBHOOK_SECRET):
          abort(401)
      event = request.get_json()
      # dispatch on event["type"] …
      return "", 204
  ```

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

  function verifySignature(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),
    );
  }

  const app = express();
  app.post(
    "/thunderphone-webhook",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const sig = req.header("X-ThunderPhone-Signature") || "";
      if (!verifySignature(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);
    },
  );
  ```
</CodeGroup>

## Семантика доставки

Эта семантика применяется к доставкам в **конечные точки**. Устаревший вебхук с одним URL выполняет одну синхронную попытку без повторов.

<AccordionGroup>
  <Accordion title="Повторные попытки">
    Каждое событие немедленно отправляется один раз. Любой ответ `2xx`
    подтверждает доставку. При любом другом результате (не-2xx,
    ошибка соединения, тайм-аут) мы повторяем попытку через **1 мин, 5 мин, 30 мин, 2 ч, 6 ч,
    12 ч и 24 ч после первой попытки** — всего 8 попыток в течение
    24 часов. Если все попытки завершатся неудачно, доставка прекращается, а конечная точка
    помечается как `status="failing"` в
    [конечных точках вебхуков](/ru/webhooks/endpoints). Верните `2xx`, как только
    полезная нагрузка будет надёжно принята; обрабатывайте её асинхронно.
  </Accordion>

  <Accordion title="Порядок">
    Порядок доставки обеспечивается по мере возможности. На практике мы доставляем события
    в порядке их создания, но повторные попытки могут изменить порядок при сбоях.
    Всегда выполняйте дедупликацию и сверяйте состояние по `call_id` / идентификатору объекта.
  </Accordion>

  <Accordion title="Дубликаты">
    Доставка выполняется **как минимум один раз**: повторная попытка после ответа, который мы не
    получили, может создать дубликат события. Каждая повторная попытка содержит тот же
    `event_id`, поэтому сохраняйте обработанные идентификаторы и пропускайте повторы. `event_id`
    также используется всеми конечными точками — две конечные точки, подписанные на
    одно событие, получают одинаковый `event_id`.
  </Accordion>

  <Accordion title="Тайм-ауты">
    Для доставки в конечные точки действует тайм-аут **30 с** на каждую попытку. В
    устаревшем пути блокирующие запросы, определяющие поведение активного звонка —
    обмен конфигурацией [`telephony.incoming` / `web.incoming`](/ru/webhooks/call-incoming) —
    завершаются по тайм-ауту через **10 с**, однако медленный ответ задерживает приём звонка,
    поэтому стремитесь отвечать в течение нескольких секунд. Для [вызова инструментов](/ru/tools/overview)
    в режиме вебхука доступно 20 с.
  </Accordion>

  <Accordion title="Исходные IP-адреса">
    Исходящие вебхуки поступают из диапазона облачных IP-адресов ThunderPhone.
    Если для вашего файрвола требуется список разрешённых адресов, обратитесь в поддержку, и мы
    предоставим актуальные диапазоны.
  </Accordion>
</AccordionGroup>

## Выбор между устаревшими вебхуками и вебхуками на основе конечных точек

| Функция                         | Устаревшие (`/v1/webhook`)                                               | Конечные точки (`/v1/developer/webhook-endpoints`) |
| ------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------- |
| Количество URL                  | 1 на организацию                                                         | Несколько на организацию                           |
| Охват событий                   | Только `telephony.*` / `web.*`                                           | Все 10 типов событий                               |
| Фильтр событий                  | —                                                                        | Для каждой конечной точки                          |
| Повторные попытки               | Нет                                                                      | 8 попыток в течение 24 ч                           |
| Обёртка                         | `type` + `data`                                                          | `type` + `data` + `event_id`                       |
| Ротация секрета                 | Заменяет единый секрет                                                   | Секрет для каждой конечной точки                   |
| Отключение без удаления         | —                                                                        | `status=disabled`                                  |
| Отображение статуса             | —                                                                        | `active` / `disabled` / `failing`                  |
| Блокирующий обмен конфигурацией | Да ([`telephony.incoming` / `web.incoming`](/ru/webhooks/call-incoming)) | Никогда — только уведомления                       |
| Лучше всего подходит для        | Динамической конфигурации звонков                                        | Получения событий в продакшене                     |

Новые интеграции должны получать события через вебхуки на основе конечных
точек. Сохраняйте (или добавляйте) устаревший URL, только если вы настраиваете звонки
динамически в момент приёма или используете вызов инструментов в режиме вебхука — эти
обмены запросами и ответами работают только через устаревший путь.

***

## Связанные материалы

<CardGroup cols={2}>
  <Card title="Каталог событий" icon="list" href="/ru/webhooks/events">
    Все типы событий и их полезные нагрузки.
  </Card>

  <Card title="Конечные точки вебхуков" icon="bolt" href="/ru/webhooks/endpoints">
    Управляйте несколькими конечными точками, фильтрами событий и секретами.
  </Card>

  <Card title="telephony.incoming / web.incoming" icon="phone" href="/ru/webhooks/call-incoming">
    Блокирующий запрос, на который ваш сервер должен ответить для настройки звонков.
  </Card>

  <Card title="telephony.complete / web.complete" icon="phone" href="/ru/webhooks/call-complete">
    Полезная нагрузка после звонка с расшифровкой, записью и метриками.
  </Card>
</CardGroup>
