# Настройка блокчейн-вебхуков: подписи, дедупликация и replay

> Source: https://docs.blockvectra.com/ru/guides/webhook-push/

Отслеживайте адрес EVM-кошелька и получайте переводы его нативной монеты, переводы токенов и совпадающие логи смарт-контрактов на ваш HTTPS-эндпоинт для уведомлений об активности кошелька или мониторинга событий смарт-контрактов. Разработчики и AI Agent используют одинаковый HTTP API подписок. Для уведомлений о платежах в ERC-20 USDT / USDC следуйте инструкциям в разделе [прием платежей в стейблкоинах](https://docs.blockvectra.com/ru/guides/stablecoin-payments/#receive-payments-with-webhooks).

* **Первый шаг:** [Разверните приемник, проверяющий подписи исходного тела запроса](#verify-signatures), используя приведенный ниже пример проверки.
* **Критерий завершения:** после достижения `applied_version >= change_version` совпадающая ончейн-активность поступает на ваш приемник, проходит проверку подписи и сохраняется по `id` события; создание подписки не отправляет тестовое сообщение.

[Варианты доступа к Webhook](https://blockvectra.com/ru/webhooks/).

## Задачи, которые помогает решить это руководство

* [Получение активности адреса кошелька](#connect-wallet-address-activity) путем создания аутентифицированной подписки, добавления отслеживаемых адресов и проверки входящих событий.
* [Мониторинг совпадающих логов контрактов](#event-format) путем анализа событий `log` для отслеживаемых адресов и фильтрации `address`, `topics` и `data` в вашем приемнике.
* [Восстановление прерванной доставки](#delivery-retries-and-replay) путем проверки прогресса подписки и повторного воспроизведения (replay) сохраненных совпадений с последующей довыгрузкой пропусков за пределами окна replay.

Подписка объединяет один HTTPS-URL для приема, секрет подписи, отслеживаемые EVM-адреса и обязательный объект `chains`. Адреса применяются ко всем сетям в этом объекте. Используйте API с заголовком `x-api-key`; любой активный ключ в вашем аккаунте может управлять всеми его подписками. [Получите API key](https://blockvectra.com/ru/get-api-key/) перед началом работы. В [Push OpenAPI](https://docs.blockvectra.com/openapi/push.yaml) перечислены все операции и схемы вебхуков.

## Подключение активности адреса кошелька

1. Разверните приемник, который [проверяет исходное тело запроса](#verify-signatures), сохраняет события по `id` и подтверждает их получение в течение 10 секунд.
2. Прочитайте `GET /v1/push/chains`, затем [создайте подписку](#create-a-subscription) с вашим HTTPS-URL и выбранными сетями. Сохраните возвращенные `id` и `secret`.
3. [Добавьте адреса кошельков](#add-and-list-addresses). Дождитесь выполнения условия `applied_version >= change_version` и запишите значение `applied_from_block` для каждой сети; сопоставление начинается с этого блока.
4. Обрабатывайте переводы и логи, а также [восстанавливайте пропуски или замененные блоки](#delivery-retries-and-replay). Фильтруйте контракты токенов, получателей и целочисленные суммы перед использованием уведомлений при обработке платежей.

## Выбор Webhook, WebSocket или опроса

* **Webhook** отправляет события отслеживаемых адресов на HTTPS-приемник с повторными попытками доставки и повтором (replay) сохраненных совпадений.
* **[WebSocket](https://docs.blockvectra.com/ru/guides/websocket-subscriptions/)** передает поток `newHeads` и отфильтрованных логов `logs` через постоянное соединение. Переподключайтесь, подписывайтесь заново и запрашивайте пропущенные блоки после отключения.
* **[Опрос](https://docs.blockvectra.com/ru/guides/stablecoin-payments/)** запрашивает `eth_getLogs` в ограниченных диапазонах блоков с использованием вашего собственного курсора; используйте его для мониторинга платежей или довыгрузки отсутствующих логов.

Проверьте поля `ws` и `subscriptions` в ответе `GET /v1/chains` для оценки поддержки WebSocket. Если `ws` имеет значение false, адресные Webhooks остаются доступным вариантом, если эта сеть присутствует в аутентифицированном списке `GET /v1/push/chains`. Поддержка только RPC не гарантирует поддержку Push.

## Емкость адресов

Тариф самообслуживания поддерживает до 1 000 000 адресов на подписку и доступен сразу при регистрации. Подписка охватывает несколько сетей с одним URL для приема. Корпоративная емкость поддерживает 10 000 000 / 100 000 000 адресов на подписку; [свяжитесь с нами для подключения](https://blockvectra.com/ru/contact/). У разработчиков и AI Agent одинаковые варианты емкости и тарифы. В обоих вариантах действуют одни и те же ставки за адресо-дни и доставленные события, указанные на [странице тарифов](https://blockvectra.com/ru/pricing/).

## Создание подписки

Выполните `GET /v1/push/chains` для получения доступных сетей, а также их минимального, стандартного и максимального количества подтверждений. Блок передается в обработку, когда `head - block + 1 >= confirmations`. Каждая сеть может использовать значение по умолчанию, если передать `{}`. Требуется как минимум одна сеть; новые сети не добавляются автоматически в существующие подписки.

Сохраните следующий пример как `create.json`, заменив URL на адрес вашего приемника и выбрав сети из списка сетей. URL должен использовать HTTPS на порту 443, имя хоста вместо литерала IP-адреса, и не содержать информации о пользователе или фрагмента (#).

```json
{
  "url": "https://hooks.example.com/push",
  "chains": {
    "bsc_mainnet": {
      "confirmations": 1
    },
    "base_mainnet": {}
  }
}
```

Установите переменную окружения `BLOCKVECTRA_API_KEY`, затем выполните:

```bash
PUSH_URL='https://api.blockvectra.com/v1/push'
curl --fail-with-body -sS "$PUSH_URL/chains" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
curl --fail-with-body -sS "$PUSH_URL/subscriptions" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d @create.json > subscription.json
```

Успешное создание возвращает HTTP 201 и подписку в статусе `online` без адресов. Надежно сохраните ее числовой `id` и `secret`. Секрет возвращается только при создании или вызове `POST /subscriptions/{subscription_id}/rotate-secret`; ротация вступает в силу немедленно для всех сетей, без периода перекрытия. Тестовые сообщения не отправляются.

## Добавление и просмотр списка адресов

Сохраните пакет адресов как `addresses.json`, заменив примеры адресов на те, которые вы отслеживаете:

```json
{
  "addresses": [
    "0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
    "0x99d47bB552ae095159C251836De6A5d524076872"
  ]
}
```

Установите `SUBSCRIPTION_ID` в значение возвращенного ID подписки:

```bash
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses/add" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' -d @addresses.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
```

Каждый вызов добавления принимает не более 10 000 адресов. Входные адреса должны быть в нижнем регистре или в валидном смешанном регистре EIP-55; некорректный ввод отклоняет весь пакет. Повторяющиеся адреса считаются как `unchanged`, поэтому повторная отправка того же запроса на добавление безопасна. Списки адресов используют `limit` и `page_token`; `next_page_token: null` обозначает последнюю страницу.

Добавление адресов возвращает `change_version`. Опрашивайте или проверяйте `GET /subscriptions/{subscription_id}`, пока `applied_version >= change_version`; применение изменений обычно занимает около 1 секунды. Значение `applied_from_block` для каждой сети указывает действительный блок, начиная с которого сопоставляются ончейн-транзакции и логи. Новые адреса не сопоставляются задним числом.

Создание подписки возвращает HTTP 201 в подтверждение того, что ресурс подписки создан; HTTP 201 не означает, что ваш приемник получил push-уведомление webhook. Платформа не отправляет проверочные или тестовые сообщения при создании или регистрации адресов. Вы должны дождаться возникновения совпадающей ончейн-активности по отслеживаемым адресам и сетям, чтобы убедиться в доставке на ваш приемник.

## Формат событий

Каждый POST-запрос содержит `type: push.events`, `created_at` и `data`. Объект `data` содержит `subscription_id`, одно значение `chain`, `complete_through_block` и `events`. Фиксируйте прогресс отдельно для каждой сети: один блок может быть разбит на несколько сообщений, поэтому номера блоков отдельных событий не являются маркером завершения. Каждое сообщение содержит не более 1 000 событий, 1 МиБ и 50 блоков.

```json
{
  "type": "push.events",
  "created_at": "2026-10-02T03:00:05Z",
  "data": {
    "subscription_id": 48213,
    "chain": "bsc_mainnet",
    "complete_through_block": 64000121,
    "events": [
      {
        "id": "evt_payvsqb6ogymhmehrs2wl5xcky",
        "type": "native.transfer",
        "ref": "eip155:56:0x7b19944dc683c33e8dedba259cb6939f7271f70f2eeb6ca5e456cccb57cc1eff:tx",
        "from": "0xe0a2100d7dad33f70c4bb765323cb96b2400c844",
        "to": "0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
        "amount": "150000000000000000",
        "block_number": 64000120,
        "block_hash": "0x327892a3e5699a43981f0fbcc5e490628641d92c040eb0429fb550ba3a73c3bf",
        "block_timestamp": "2026-10-02T03:00:00Z",
        "tx_hash": "0x7b19944dc683c33e8dedba259cb6939f7271f70f2eeb6ca5e456cccb57cc1eff",
        "tx_index": 3,
        "matched": [
          {
            "address": "0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
            "role": "to"
          }
        ]
      },
      {
        "id": "evt_lgcdattb6l2k3ejuhe4mtdljkm",
        "type": "token.transfer",
        "ref": "eip155:56:0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76:7",
        "standard": "erc20",
        "token": "0x55d398326f99059ff775485246999027b3197955",
        "from": "0x0f94e5283c41c29a8f4dff8c17f68bdfb59f07df",
        "to": "0x99d47bb552ae095159c251836de6a5d524076872",
        "token_id": null,
        "amount": "25000000000000000000",
        "batch_index": null,
        "block_number": 64000121,
        "block_hash": "0x6a8146159162f182c091d17eac7d03e95dc92ce80de704c958ca2528306aff15",
        "block_timestamp": "2026-10-02T03:00:00Z",
        "tx_hash": "0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76",
        "tx_index": 5,
        "log_index": 7,
        "matched": [
          {
            "address": "0x99d47bb552ae095159c251836de6a5d524076872",
            "role": "to"
          }
        ]
      },
      {
        "id": "evt_sgliw3ficdf6gaa6zzx4ew6vni",
        "type": "log",
        "ref": "eip155:56:0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76:8",
        "address": "0xb54ffbe723264b84cf74947127a6914cf87fc593",
        "topics": [
          "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
          "0x00000000000000000000000099d47bb552ae095159c251836de6a5d524076872",
          "0x000000000000000000000000b54ffbe723264b84cf74947127a6914cf87fc593"
        ],
        "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "block_number": 64000121,
        "block_hash": "0x6a8146159162f182c091d17eac7d03e95dc92ce80de704c958ca2528306aff15",
        "block_timestamp": "2026-10-02T03:00:00Z",
        "tx_hash": "0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76",
        "tx_index": 5,
        "log_index": 8,
        "matched": [
          {
            "address": "0x99d47bb552ae095159c251836de6a5d524076872",
            "role": "topic1"
          }
        ]
      }
    ]
  }
}
```

| Тип события        | Что обрабатывать                                                                                                                                                                                                                                                       |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `native.transfer`  | Успешные переводы нативной монеты верхнего уровня с участием отслеживаемого адреса; `amount` — целочисленная десятичная строка. Внутренние переводы нативной монеты исключены.                                                                                         |
| `token.transfer`   | Переводы ERC-20, ERC-721 и ERC-1155 с участием отслеживаемых адресов; проверяйте `standard`, `token`, `token_id`, `amount` и `batch_index`. Пакетные переводы ERC-1155 генерируют по одному событию на каждый элемент.                                                 |
| `log`              | Другие логи, в которых отслеживаемый адрес указан в качестве контракта-источника или в topics 1–3; проверяйте `address`, `topics`, `data` и `matched`.                                                                                                                 |
| `subscription.gap` | Диапазон от `from_block` до `to_block` недоступен для доставки с причиной `reason: retention_expired`; выполните довыгрузку с помощью Data API или `eth_getLogs`.                                                                                                      |
| `chain.reorg`      | Бесплатное уведомление о реорганизации: доставленные блоки в диапазоне `from_block`–`to_block` были заменены. Пометьте или отбросьте их старые события по `ref`, затем сохраните автоматически повторно доставленные канонические события и дедуплицируйте их по `id`. |

В пределах одной подписки выполняйте дедупликацию по `id` события; между разными подписками используйте `ref` и `type`. Игнорируйте неизвестные поля и типы событий. Проверяйте ончейн-факты перед совершением финансовых действий.

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

Заголовками являются `webhook-id`, `webhook-timestamp`, `webhook-signature` и `bv-subscription-id`. Выбирайте секрет только для подписок, созданных вами; отклоняйте неизвестные ID. Проверяйте HMAC-SHA256 для строки `webhook-id.webhook-timestamp.raw-body`, используя байты исходного тела запроса, до парсинга JSON. Подпись имеет формат `v1,<base64>`; допускайте расхождение временных меток примерно до пяти минут и сравнивайте значения за константное время.

Эта функция Node.js принимает исходное тело запроса как `Buffer`, заголовки запроса и словарь соответствия ID подписок сохраненным секретам:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyPush(rawBody, headers, secrets) {
  const subscriptionId = headers['bv-subscription-id'];
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signature = headers['webhook-signature'];
  if ([subscriptionId, id, timestamp, signature].some(v => typeof v !== 'string')) return false;
  const secret = secrets.get(subscriptionId);
  if (typeof secret !== 'string' || !secret.startsWith('whsec_')) return false;
  if (!/^\d{10}$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const match = /^v1,([A-Za-z0-9+/]{43}=)$/.exec(signature);
  if (!match) return false;
  const received = Buffer.from(match[1], 'base64');
  const expected = createHmac('sha256', Buffer.from(secret.slice(6), 'base64'))
    .update(`${id}.${timestamp}.`).update(rawBody).digest();
  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

После проверки выполните парсинг тела запроса, сохраните результат обработки и верните статус 2xx в течение 10 секунд. Заголовок ID подписки считается ненадежным до тех пор, пока подпись не будет проверена.

## Проверка вашего первого события

Держите подписку в статусе online. После того как изменение адреса будет применено, дождитесь совпадающей ончейн-активности и убедитесь, что ваш приемник проверяет подпись и надежно сохраняет событие.

## Остановка прослушивания после проверки

<a id="stop-listening-and-clean-up" />

Чтобы прекратить отслеживание адресов, сохраните удаляемые адреса в `addresses.json` и вызовите `POST /subscriptions/{subscription_id}/addresses/remove`:

```bash
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses/remove" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' -d @addresses.json
```

Каждый вызов на удаление принимает не более 10 000 адресов. Адрес, который в данный момент не отслеживается, считается `unchanged`. Вызов возвращает `change_version`. Как только будет выполнено условие `applied_version >= change_version`, блоки, начиная с этого действительного блока, больше не будут сопоставляться с удаленными адресами. Ранее сопоставленные события (находящиеся в пути, в процессе повтора или в очереди) все равно доставляются; уже доставленные события не отзываются.

Чтобы временно приостановить прослушивание без удаления конфигурации или адресов, установите `status` в значение `offline`:

```bash
curl --fail-with-body -sS -X PATCH "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"offline"}'
```

Подписка в статусе `offline` останавливает прослушивание и доставку, выгружает адреса из индекса сопоставления и не влечет за собой платы за адреса за любой полный день по UTC, в течение которого она остается оффлайн. Вся конфигурация (URL, секрет, адреса, сети и подтверждения) сохраняется. Применение патча с `{"status":"online"}` возобновляет прослушивание с текущего действительного блока и не выполняет довыгрузку за период оффлайн.

Используйте JSON Merge Patch для `PATCH /subscriptions/{subscription_id}`, чтобы изменить `url`, `key_id`, `status` или `chains`: объект сети добавляет или обновляет ее, а `null` удаляет ее. Должна оставаться как минимум одна сеть. Для окончательного удаления подписки:

```bash
curl --fail-with-body -sS -X DELETE "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
```

`DELETE` навсегда удаляет подписку, немедленно прекращает доставку по всем сетям и уничтожает секрет и адреса.

## Доставка, повторные попытки и replay

Доставка выполняется по модели как минимум один раз (at least once). Сеть каждой подписки упорядочена по блоку и позиции внутри блока; пакеты, завершившиеся ошибкой, блокируют последующие события в этой сети. Различные сети имеют независимый прогресс и могут выполнять POST-запросы параллельно. При повторе идентичного пакета сохраняется `webhook-id`, однако измененный пакет может иметь новый ID: дедуплицируйте события, а не пакеты.

Любой код ответа 2xx в течение 10 секунд подтверждает надежное сохранение и обработку. Перенаправления не обрабатываются; коды 3xx и 410 считаются ошибками. После сбоя повторные попытки выполняются с интервалами: немедленно, 5 секунд, 30 секунд, 2 минуты, 10 минут, 30 минут и 1 час, затем каждый час. Заголовок `Retry-After` при коде 429 может продлить ожидание до одного часа. Проверяйте поля `condition`, `last_error` и `next_attempt_at` для каждой сети, когда доставка останавливается. Возможные состояния condition: `receiver_failing`, `insufficient_balance` и `key_revoked`; в последнем случае требуется обновить `key_id` на другой активный ключ аккаунта через PATCH.

Недоставленные события истекают за пределами окна хранения и порождают событие `subscription.gap`. Запрос `POST /subscriptions/{subscription_id}/replay` принимает параметры `chain` и `from_block`; сверяйтесь со значением `replayable_from_block` в `GET /push/chains` и текущим прогрессом подписки. Replay доставляет существующие совпадения и не может восстановить события, произошедшие до добавления адреса или сети.

Событие `chain.reorg` уведомляет о том, что уже доставленные блоки были заменены; оно не указывает на пропуск в доставке. Реорганизации меньшей глубины, чем ваше количество подтверждений, незаметны. При реорганизациях, затрагивающих доставленные блоки глубиной до 1 024 блоков, канонические события автоматически доставляются повторно с новыми `id`. Пометьте или отбросьте замененные события по `ref`, сохраните канонические события и дедуплицируйте их по `id`; для записей платежей выполняйте сверку по `ref` и `tx_hash`. Более глубокая реорганизация приостанавливает сеть: проверяйте флаг `halted` в `GET /push/chains`; каноническая повторная доставка произойдет после восстановления сети. Это управляющее событие не продвигает значение `complete_through_block`.

Запрашивайте доставленные события данных с помощью `GET /subscriptions/{subscription_id}/events?chain=...`, при необходимости добавляя параметры `from_block`, `to_block`, `limit` и `page_token`. Строки истории содержат `event`, `replay_epoch`, `orphaned` и `delivered_at`; значение `orphaned: true` обозначает блок, замененный позднее. Запрос к истории может вернуть 402 `insufficient_balance` (`data.reason`: `balance_exhausted` или `free_grant_exhausted`), 403 `key_cap_exhausted` (`data.cu_cap`) или 429 `rate_limited` (`key_rate_limit` или `free_plan_call_limit`). Ошибка 429 `cost_exceeds_burst` содержит причину `request_exceeds_burst` и `data.max`: увеличьте емкость пиковой нагрузки (burst capacity) перед повторной попыткой. Руководство по недопустимым диапазонам и повторным попыткам см. в [обработке ошибок](https://docs.blockvectra.com/ru/errors/).

## Тарификация и пример

Веса берутся из `GET /v1/plans`. Доставленные события данных, успешные запросы к истории и адресо-дни имеют раздельные веса; вызовы управления, кроме истории, управляющие события, неудачные доставки и автоматические повторные попытки бесплатны. Каждое доставленное событие оплачивается один раз; пользовательский replay и повторная доставка канонических событий влекут за собой новые начисления за доставку.

Тарификация адресов использует наибольшее количество адресов каждой подписки за время ее нахождения online в течение дня по UTC за вычетом бесплатного лимита адресов аккаунта, разделяемого между подписками (в порядке приоритета более старых подписок). Один и тот же адрес в двух подписках учитывается дважды; добавление сетей меняет плату за события, а не плату за адреса. Подписка, находившаяся offline в течение всего дня по UTC, не тарифицируется по адресам.

| Использование | Расчетная единица | CU |
| --- | --- | --- |
| `push.address_day` | Тарифицируемый адрес-день | 33 |
| `push.history` | Успешный запрос истории | 25 |
| `push.log` | Доставленное событие данных | 150 |
| `push.native_transfer` | Доставленное событие данных | 150 |
| `push.token_transfer` | Доставленное событие данных | 150 |

Бесплатные адреса на аккаунт в сутки UTC: 1000

Суточный лимит бесплатных адресов на аккаунт (UTC), общий для всех групп подписок независимо от тарифного плана. Для каждой группы подсчитывается максимальное количество адресов во время нахождения онлайн в течение этих суток; лимит распределяется в порядке возрастания ID группы. Один и тот же адрес в двух группах учитывается дважды; количество сетей в группе не умножает количество адресов. Группа, находившаяся офлайн или удаленная в течение всего дня, ничего не добавляет. Для каждой группы остаток после вычета ее доли бесплатного лимита умножается на вес CU `push.address_day` в `method_weights`. Текущий настроенный лимит исходит из той же ценовой политики, что и плата за адрес-день; это не лимит емкости аккаунта и не отдельный лимит на каждую группу.

Пример: 10 доставленных событий native.transfer, 2 успешных запроса истории и 10 тарифицируемых адрес-дней стоят 10 × 150 + 2 × 25 + 10 × 33 = 1880 CU. Тарифицируемые адрес-дни учитываются после исчерпания бесплатного лимита адресов аккаунта.

Информацию об учете и конвертации CU см. в [правилах тарификации](https://docs.blockvectra.com/ru/guides/billing-rules/) и на [странице тарифов](https://blockvectra.com/ru/pricing/).

## Связанные ресурсы

* Сравните поддерживаемые события, покрытие сетей и тарифы в [обзоре Blockchain Webhook API](https://blockvectra.com/ru/webhooks/).
