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

Создавайте подписки на адреса по HTTP, проверяйте подписи исходного тела запроса, дедуплицируйте ID событий и восстанавливайте сохраненные совпадения или пропущенные блоки.

Отслеживайте адрес EVM-кошелька и получайте переводы его нативной монеты, переводы токенов и совпадающие логи смарт-контрактов на ваш HTTPS-эндпоинт для уведомлений об активности кошелька или мониторинга событий смарт-контрактов. Разработчики и AI Agent используют одинаковый HTTP API подписок. Для уведомлений о платежах в ERC-20 USDT / USDC следуйте инструкциям в разделе прием платежей в стейблкоинах.

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

Варианты доступа к Webhook.

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

Подписка объединяет один HTTPS-URL для приема, секрет подписи, отслеживаемые EVM-адреса и обязательный объект chains. Адреса применяются ко всем сетям в этом объекте. Используйте API с заголовком x-api-key; любой активный ключ в вашем аккаунте может управлять всеми его подписками. Получите API key перед началом работы. В Push OpenAPI перечислены все операции и схемы вебхуков.

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

  1. Разверните приемник, который проверяет исходное тело запроса, сохраняет события по id и подтверждает их получение в течение 10 секунд.
  2. Прочитайте GET /v1/push/chains, затем создайте подписку с вашим HTTPS-URL и выбранными сетями. Сохраните возвращенные id и secret.
  3. Добавьте адреса кошельков. Дождитесь выполнения условия applied_version >= change_version и запишите значение applied_from_block для каждой сети; сопоставление начинается с этого блока.
  4. Обрабатывайте переводы и логи, а также восстанавливайте пропуски или замененные блоки. Фильтруйте контракты токенов, получателей и целочисленные суммы перед использованием уведомлений при обработке платежей.

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

  • Webhook отправляет события отслеживаемых адресов на HTTPS-приемник с повторными попытками доставки и повтором (replay) сохраненных совпадений.
  • WebSocket передает поток newHeads и отфильтрованных логов logs через постоянное соединение. Переподключайтесь, подписывайтесь заново и запрашивайте пропущенные блоки после отключения.
  • Опрос запрашивает 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 адресов на подписку; свяжитесь с нами для подключения. У разработчиков и AI Agent одинаковые варианты емкости и тарифы. В обоих вариантах действуют одни и те же ставки за адресо-дни и доставленные события, указанные на странице тарифов.

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

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

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

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

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

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, заменив примеры адресов на те, которые вы отслеживаете:

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

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

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 блоков.

{
  "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 подписок сохраненным секретам:

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. После того как изменение адреса будет применено, дождитесь совпадающей ончейн-активности и убедитесь, что ваш приемник проверяет подпись и надежно сохраняет событие.

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

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

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:

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 удаляет ее. Должна оставаться как минимум одна сеть. Для окончательного удаления подписки:

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) перед повторной попыткой. Руководство по недопустимым диапазонам и повторным попыткам см. в обработке ошибок.

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

Веса берутся из 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 см. в правилах тарификации и на странице тарифов.

Последнее обновление:

На этой странице