# Вебхуки

Вебхук (webhook) - HTTP-уведомление на адрес вашего обработчика. Через вебхуки Platform API доставляет данные об активности в блокчейне. Когда на отслеживаемом адресе появляется транзакция, Vilna отправляет подписанный HTTP `POST` на ваш [канал уведомлений](/ru/guides/channels) - без опроса.

Эта страница - контракт доставки: типы событий, тело уведомления, заголовки, как проверить подлинность доставки, как работают повторы и тестирование. Создание и управление каналом - в разделе [Каналы уведомлений](/ru/guides/channels); сама модель событий - в [События и мониторинг](/ru/guides/events).

Подписанный HTTP POST
HMAC-SHA256
Обработка повторов по `event_id`
Ответ за 10с

## Версии полезной нагрузки 

Каждый webhook-канал указывает `payload_version` - `v1` или `v2` - при создании. Версия замораживается в уведомлении при первой отправке; смена версии позже, через `PATCH` канала, действует только для уведомлений, отправленных после смены. После переключения канала на `v2` повторы уведомлений, уже замороженных на `v1`, продолжают уходить в форме `v1` рядом с новыми отправками `v2`. Канал, созданный до запуска v2, остаётся на `v1`, пока вы не переключите его.

## Типы событий 

`payload_version` канала определяет, какое имя события он получает.

v2

Канал с `payload_version: v2` получает одно событие для каждой транзакции, отобранной подпиской:

| Событие | Когда срабатывает |
|  --- | --- |
| `transaction.notification` | Выполнено условие отправки уведомления, заданное подпиской. |


Тестовая доставка использует то же имя события - отличить её от реальной можно по `is_test` в теле.

v1

Канал с `payload_version: v1` получает одно из двух событий. Он поддерживает только триггеры `block` с `after_blocks: 0` и `network`; для `mempool` требуется `v2`.

| Событие | Когда срабатывает |
|  --- | --- |
| `transaction.detected` | Включена в блок: сработал триггер `block` с `after_blocks: 0`. Это ранний сигнал, а не гарантия необратимости. |
| `transaction.confirmed` | Достигла политики подтверждений своей сети. Это не гарантия необратимости; перед действием сверяйте текущее состояние транзакции. |


Тестовая доставка тоже использует эти имена событий - отличить её можно по `is_test_message` в теле.

Имя события приходит в заголовке `X-Webhook-Event`.

## Доставка 

Каждая доставка - HTTP `POST` с телом JSON и подписанными заголовками.

### Заголовки

| Заголовок | Что несёт |
|  --- | --- |
| `X-Webhook-Signature` | Подпись HMAC-SHA256, `t=<unix>,v1=<hex>` - см. [проверку](#verify) |
| `X-Webhook-Event` | `v2`: всегда `transaction.notification` - тест несёт то же имя, отличить его можно по `is_test` в теле. `v1`: `transaction.detected` или `transaction.confirmed` - тест использует те же имена, отличить его можно по `is_test_message` в теле |
| `X-Webhook-Version` | `payload_version` канала, `v1` или `v2`. Совпадает с `version` в теле `v2`; тело `v1` такого поля не несёт |
| `X-Webhook-Event-Id` | UUIDv5, стабилен между повторами - идентификатор для отсеивания повторов |
| `X-Webhook-Delivery-Id` | UUID, меняется на каждой попытке - для связи с поддержкой |


### Тело уведомления 

Каждая доставка - тело JSON в форме, которую выбрал `payload_version` канала:

v2

```json
{
  "version": "v2",
  "event_id": "550e8400-e29b-41d4-a716-446655440020",
  "subscription_id": "550e8400-e29b-41d4-a716-446655440002",
  "is_test": false,
  "item": {
    "chain_gid": "eip155:1",
    "txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "created_at": "2024-01-15T10:30:00Z",
    "activity": [
      {
        "chain_gid": "eip155:1",
        "txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        "address": "0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4",
        "asset_gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
        "direction": "out",
        "delta": { "base": "-1000000", "formatted": "-1.0" },
        "created_at": "2024-01-15T10:30:00Z",
        "entry_kind": "token_transfer"
      }
    ],
    "details": {
      "family": "evm",
      "events": [
        {
          "event_index": 0,
          "kind": "transfer",
          "asset_gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
          "amount": { "base": "1000000", "formatted": "1.0" },
          "operation": "transfer",
          "from_address": "0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4",
          "to_address": "0x95212290dd7278aa3ddd389cc1e1d165cc4bafe5"
        }
      ],
      "is_success": true,
      "fee": { "base": "21000000000000000", "formatted": "0.021" },
      "block": {
        "block_hash": "0x9f1c2a3b4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8",
        "block_height": 18500000
      },
      "transaction_index": 42,
      "from_address": "0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4",
      "to_address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
      "gas_used": 65000
    }
  },
  "references": {
    "tokens": {
      "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7": {
        "gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
        "symbol": "USDT",
        "name": "Tether USD",
        "decimals": 6
      }
    },
    "blockchains": {
      "eip155:1": {
        "gid": "eip155:1",
        "name": "ethereum",
        "short_name": "eth",
        "image_url": "https://raw.githubusercontent.com/vilna-io/icons/refs/heads/main/chains/ethereum.svg"
      }
    },
    "addresses": {
      "0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4": "Hot wallet"
    }
  }
}
```

- **`version`** - замороженная версия тела вебхука, `v2`. Не зависит от версии HTTP API.
- **`event_id`** - идентификатор этого уведомления. Используйте его для отсеивания повторов при повторах и ручных повторных отправках.
- **`subscription_id`** - подписка, которая отобрала транзакцию. Null только для теста канала.
- **`is_test`** - `true` для теста канала, `false` для реальной транзакции.
- **`item`** - сама транзакция, с разобранными [событиями](/ru/guides/events) и активностью по адресам.
- **`references`** - словари для токенов, сетей и адресов из транзакции, чтобы почти никогда не требовался ещё один запрос для разрешения имён и символов.

v1

```json
{
  "event_id": "f47ac10b-58cc-5372-a567-0e02b2c3d479",
  "item": { "...": "объект Transaction с его событиями и активностью по адресам" },
  "references": {
    "tokens": { "...": "GID токена -> детали" },
    "blockchains": { "...": "GID сети -> детали" },
    "addresses": { "...": "адрес -> метка" }
  },
  "is_test_message": false
}
```

- **`event_id`** - UUIDv5, совпадает с заголовком `X-Webhook-Event-Id`. Входит в подписанное тело, поэтому защищён от подмены. Используйте его для отсеивания повторов.
- **`item`** - полная транзакция, включая её разобранные [события](/ru/guides/events) (переводы, комиссии, одобрения) и дельты по адресам.
- **`references`** - словари для токенов, сетей и адресов из транзакции, чтобы не делать лишних запросов для разрешения имён и символов.
- **`is_test_message`** - `true` для тестовых доставок, `false` для реальных блокчейн-событий.

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

Подпись позволяет проверить, что данные не изменились и отправитель владеет общим секретом канала. Проверяйте каждую доставку, прежде чем действовать по ней. Заголовок `X-Webhook-Signature` состоит из двух частей:

`t=<timestamp>,v1=<signature>`

`t=<timestamp>` (e.g. `t=1714262800`)

**Метка времени** - Unix-секунды, когда Vilna подписала доставку. Задаёт окно защиты от повторов.

`v1=<signature>` (e.g. `v1=5257a8696bc5…`)

**Подпись** - HMAC-SHA256 в hex от подписываемых данных. Во время ротации секрета появляются две записи `v1=`, поэтому получатель с любым из секретов может проверить.

**Подписываемые данные** - это значение `t`, буквальная точка, затем сырые байты тела запроса: `{t}.{rawBody}`. Проверяется в пять шагов - одинаково на любом языке:

**Разберите заголовок** на значение `t` и одно или несколько значений `v1`. Отклоните заголовок с отсутствующей или некорректной меткой времени. Каждая подпись `v1` должна содержать ровно 64 шестнадцатеричных символа; пропускайте некорректные значения.

**Проверьте метку времени.** Отклоните доставку, если `t` отличается от текущего времени больше чем на 5 минут - это блокирует повторы. Не ставьте допуск в ноль; расхождение часов между серверами отбросит легитимные доставки.

**Соберите подписываемые данные** - значение `t`, точка, затем *сырые* байты тела. Никогда не сериализуйте JSON заново; он не совпадёт побайтно.

**Вычислите HMAC-SHA256** этих данных вашим `webhook_secret`. Декодируйте подписи `v1` из hex в 32 байта для сравнения.

**Сравните** результат с каждым `v1` в постоянном времени. Принимайте, если совпал хотя бы один - это покрывает окно ротации.

Три частые ошибки
**Сравнивайте за постоянное время.** Используйте криптографическую функцию сравнения байтов. Некорректная подпись должна приводить к отказу проверки, а не к ошибке обработчика.

**Используйте исходные байты.** Вычисляйте HMAC для `{t}.{rawBody}` с телом ровно в полученном виде, до разбора JSON. Повторная сериализация объекта может дать другие байты.

**Принимайте любой известный `v1=`.** Игнорируйте неизвестные схемы и принимайте запрос только при успешной проверке поддерживаемой подписи.

## Секрет подписи 

У каждого канала свой секрет подписи. Сохраните его сразу после создания канала или ротации секрета.

Ограниченное восстановление секрета
Повторите тот же запрос создания или ротации с исходным `Idempotency-Key`, чтобы восстановить ответ в течение 60 секунд после успешного завершения, при текущем доступе и только пока секрет текущий. Повторы не продлевают срок. Поздние GET возвращают маски и время, а не открытый секрет. Если сохранённый ответ недоступен или срок его получения истёк, проверьте метаданные канала и секрета, затем при необходимости явно выполните ротацию; новый ключ может создать ещё один канал или повторно сменить секрет.

Ротация сохраняет проверяемость доставок без простоя:

- **Ротация** выпускает новый секрет. В течение 24 часов и старый, и новый секрет подписывают каждую доставку (две записи `v1=`), так что получателей можно переразвернуть постепенно.
- **Завершите перекрытие раньше**, когда у всех получателей есть новый секрет - после этого доставки подписываются только текущим секретом.
- Эндпоинт **чтения** возвращает маску текущего секрета и метки времени (а во время перекрытия - метаданные предыдущего секрета) - никогда не открытый текст.


Ротируйте всякий раз, когда из команды уходит человек с доступом к секрету.

## Относитесь к вебхукам как к уведомлениям, а не к истине 

Действительная HMAC-подпись подтверждает, что метку времени и исходное тело подписал владелец общего секрета. Перед необратимым действием проверьте состояние транзакции или [счёта](/ru/guides/invoices) через Platform API или напрямую в сети.

## Доставка и повторы 

- **Отвечайте быстро.** Возвращайте HTTP `2xx` за 10 секунд; реальную работу делайте после.
- **Неудачные доставки повторяются.** Ответ не-`2xx` или таймаут приводит к новой попытке с увеличенной задержкой. Повторы используют тот же `event_id`; получатель должен распознавать их и не выполнять действие повторно.
- **Отсеивайте повторы по `event_id`.** Он стабилен между повторами и есть и в заголовке, и в подписанном теле - пропускайте всё, что уже обработали.
- **Одна доставка на подписку и транзакцию.** Подписка уведомляет о конкретной транзакции не больше одного раза; повторная включённость или более поздняя правка содержимого никогда не повторяют и не исправляют уже отправленное уведомление. Исключение - подписка `mempool`: она может уведомить второй раз, если транзакция ушла из мемпула больше чем на час и появилась снова.
- **Уведомлений об откате нет.** Если транзакция, о которой вам сообщили, позже выпадает из цепочки, повторной доставки не будет - сверяйтесь через API, если нужна уверенность. Полная модель - в разделе [условий отправки уведомлений](/ru/guides/events#triggers).
- **`max_attempts` ограничивает автоматические попытки, считая первую.** Они идут по фиксированному расписанию сервиса - около суток за десять попыток (1-10). Канал отключается сам, когда они исчерпаны, либо при подтверждённо нерабочем адресате; см. [когда канал отключается сам](/ru/guides/channels#disabled).
- **Смотрите [историю уведомлений](/ru/guides/channels#logs).** Каждая попытка фиксирует HTTP-статус, который вернул ваш эндпоинт.


## Тестирование 

Отправьте пробную доставку в канал в любой момент действием test - оно создаёт реальный подписанный запрос:

Откройте [тестирование канала](/ru/apis/platform/api/channel/test-channel), выберите `family: evm` и передайте отдельный `Idempotency-Key`. Примеры запросов и варианты ответа находятся на странице операции.

Оно отвечает `202` с `notification_id`.

Используйте его, чтобы проверить вашу проверку подписи и подключение эндпоинта, прежде чем полагаться на канал в production. Тестовые доставки появляются в [истории уведомлений](/ru/guides/channels#logs) со значением `is_test: true`.

При потере HTTP-ответа повторите запрос с исходным `Idempotency-Key` и тем же телом. Ответ принятия доступен 24 часа после завершения при текущем доступе; повтор не отправляет ещё один тест. Проверьте историю уведомлений, прежде чем явно начать новый тест с новым ключом. После истечения окна ключ остаётся связанным. Повтор изменяющего HTTP-запроса, проверка времени подписи и отсеивание повторных уведомлений по `event_id` решают разные задачи.