# Вебхуки

> Уведомления о завершении генерации на ваш адрес: подписка, три события, форма тела, проверка подписи, повторные попытки, журнал доставок и требования к приёмнику.

Источник: https://strophe.app/docs/product/developers/webhooks
Обновлено: 2026-08-02

Вебхук избавляет от опроса: как только генерация завершилась, мы сами шлём POST-запрос на ваш адрес с её итоговым состоянием. Опрос при этом остаётся рабочим способом — вебхук его дополняет, а не заменяет. Как генерация запускается и что возвращает опрос, описано в статье [Запуск генерации](https://strophe.app/docs/product/developers/generation).

## Подписка

Подписки заводятся в настройках, раздел «Интеграции». У подписки два поля:

- **Адрес** — куда слать. Только публичный адрес; внутренние и приватные отвергаются при сохранении. Он же называет подписку в списке.
- **События** — какие исходы вам нужны.

При создании выдаётся секрет подписи. Он показывается один раз — как и [ключ API](https://strophe.app/docs/product/developers/authentication), дальше он хранится только у вас.

Адрес проверяется в момент сохранения: если имя не разрешается в публичный адрес, подписка не создастся.

## События

Событий три, по числу конечных состояний генерации:

- `generation.completed` — работа закончена, результат готов;
- `generation.failed` — работа провалилась;
- `generation.cancelled` — генерация отменена.

Промежуточных переходов (постановка в очередь, начало работы) в рассылке нет намеренно: интегратору важен исход, а поток всех переходов пришлось бы фильтровать на своей стороне.

## Тело запроса

Приходит JSON:

```json
{
  "id": "9f1c…",
  "event": "generation.completed",
  "createdAt": "2026-08-02T10:15:31.204Z",
  "data": { "generation": { "generationId": "3f2a…", "state": "ready", "…": "…" } }
}
```

Генерация внутри `data` — в той же форме, что и ответ `GET /api/v1/generations/{id}`. Одна и та же работа не должна выглядеть по-разному в зависимости от того, пришла она сама или её спросили.

Поле `id` — идентификатор доставки. Он одинаков у всех попыток одного события: по нему приёмник глушит повторы.

`createdAt` — момент события, а не момент попытки. У повторной доставки он тот же, что у первой.

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

Каждый запрос несёт заголовок `X-Strophe-Signature`:

```http
X-Strophe-Signature: t=1785974131,v1=6b1f…
```

`t` — момент подписи в секундах Unix, `v1` — HMAC-SHA256 от строки `t.тело` с вашим секретом, в шестнадцатеричном виде. Проверка на стороне приёмника:

```python
import hashlib, hmac, time

def valid(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = f"{parts['t']}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts["v1"])
```

Считать подпись нужно от сырого тела запроса, до разбора JSON: повторная сборка объекта в строку даёт другие пробелы и другой порядок ключей, а значит другую подпись.

Момент `t` стоит сравнивать с текущим временем и отвергать слишком старые запросы: без этого перехваченный когда-то запрос можно проиграть заново.

Запрос без действительной подписи обрабатывать нельзя. Адрес приёмника рано или поздно становится известен, и подпись — единственное, что отличает наше уведомление от чужого.

## Требования к приёмнику

**Успех — только код 2xx.** Всё остальное считается неудачей и приводит к повтору. Редиректы не выполняются: приёмник обязан отвечать по тому адресу, на который подписался.

**Ответить нужно за 10 секунд.** Долгая работа по событию выносится в свою очередь: сначала подтвердите приём, потом обрабатывайте.

**Повторы возможны.** Сеть не гарантирует однократной доставки: приёмник должен переживать повтор того же `id` без последствий.

**Порядок не гарантирован.** Событие по одной генерации может обогнать событие по другой; опираться нужно на содержимое, а не на очерёдность прихода.

## Повторные попытки

Неудачная доставка повторяется до шести раз с удваивающейся паузой, начиная с десяти секунд, — это около двадцати минут на приёмник, упавший под нагрузкой. Исчерпав попытки, доставка закрывается как неудачная.

Первые несколько байт ответа приёмника мы сохраняем: если он объяснил отказ текстом, этот текст будет виден в журнале.

## Журнал доставок

У каждой подписки есть журнал: строка на событие с временем, номером события, числом попыток, кодом ответа и текстом отказа. Он открывается из меню подписки в настройках и отвечает на вопрос «почему мне не пришло» без обращения в поддержку.

Журнал хранится 30 дней — хвост чистится сам.

В строке подписки в списке видны день последней доставки и её исход: так поломка заметна раньше, чем о ней сообщит журнал.