Перейти к содержанию
STONS
Разделы документации

Вебхуки

Сервис сообщает мерчанту о событиях — платёж замечен, платёж подтверждён, инвойс просрочен — запросом POST на его URL. События формирует сканер блокчейна и записывает их в очередь в базе в той же транзакции, что и само изменение; отправляет их API. Устройство доставки — ADR-0011.

Главное правило

Не доверяйте телу вебхука. Получив вебхук, проверьте подпись, затем запросите актуальное состояние объекта у API и действуйте по ответу API. Тело вебхука — сигнал «сходи проверь», а не источник истины.

Причина: если секрет вебхуков утечёт или в проверке подписи окажется ошибка, злоумышленник сможет прислать поддельное «оплачено». Если товар выдаётся только по ответу API, подделка ничего не даст.

События

Событие Когда приходит
invoice.seen До дедлайна в блоке появился перевод на адрес инвойса. Подтверждений ещё нет: товар не отдавать
invoice.confirmed Подтверждённые переводы, пришедшие до дедлайна, покрыли сумму инвойса. Отдать товар, проверив статус через API
invoice.overpaid Вместе с invoice.confirmed, если подтверждено больше ожидаемого
invoice.underpaid Дедлайн прошёл, подтверждена только часть суммы
invoice.expired Дедлайн прошёл, подтверждённых переводов нет
invoice.paid_late После invoice.expired или invoice.underpaid подтвердился перевод, пришедший после дедлайна
deposit.confirmed Платёж набрал нужное число подтверждений. В режиме per_user приходит и для пополнения без инвойса: тогда invoice_id равен null
deposit.orphaned Замеченный платёж исчез из цепи при реорганизации или его транзакция оказалась неуспешной. Отменяет ожидание, которое начал invoice.seen
payout.confirmed Выплата подтверждена сетью: средства ушли получателю. Окончательное состояние
payout.failed Транзакция выплаты попала в блок неуспешной: средства не ушли. Выплата не повторяется автоматически — новая попытка требует нового reference

Статусы инвойса и переходы между ними — в описании инвойсов. Переводы другого актива и пыль событий не порождают.

Тело

{
  "id": "0b7e6f2a-8c1d-4e5f-9a3b-2c4d6e8f0a1b",
  "type": "invoice.confirmed",
  "created_at": "2026-09-11T20:05:41.118Z",
  "data": {
    "invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
    "order_id": "ord_1001",
    "status": "confirmed",
    "asset": "USDT_TRON",
    "amount_requested": "49.900000",
    "amount_expected": "49.900000",
    "amount_received": "49.900000",
    "amount_credited": "49.401000",
    "fees": {
      "service": { "payer": "merchant", "percent": "1.00", "amount": "0.499000" },
      "network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "pending" }
    },
    "external_user_id": null
  }
}
Поле Описание
id Идентификатор события. Повторная доставка приходит с тем же id: по нему обработчик отбрасывает дубли
type Событие из таблицы выше
created_at Когда событие произошло, ISO 8601 в UTC
data Объект события, поля ниже

Поля data у событий invoice.*:

Поле Описание
invoice_id Идентификатор инвойса
order_id Идентификатор заказа мерчанта
status Статус инвойса после изменения
asset Актив
amount_requested Цена мерчанта
amount_expected Сумма, которую должен заплатить покупатель, со всеми знаками актива: цена и комиссия сервиса, если её платит покупатель
amount_received Подтверждённая сумма на момент события
amount_credited Подтверждённая сумма за вычетом комиссий, удержанных на момент события
fees Комиссия сервиса и комиссия сети, как в инвойсе
external_user_id Пользователь мерчанта или null

Комиссия платежа проводится в той же транзакции, что его подтверждение, поэтому invoice.confirmed уже несёт удержанную комиссию и amount_credited после неё.

Поля data у событий deposit.*:

Поле Описание
tx_hash Хэш транзакции
log_index Номер перевода в транзакции: вместе с tx_hash однозначно определяет платёж
asset Актив
amount Сумма перевода со всеми знаками актива
address Адрес приёма, целиком
invoice_id Инвойс, который оплачивает перевод, или null для пополнения в режиме per_user
external_user_id Пользователь мерчанта или null
block_time Время блока с переводом, ISO 8601 в UTC
confirmations Сколько блоков набрано над блоком перевода

Пример deposit.confirmed:

{
  "id": "5d2c9e41-7b3a-4c8f-a1e6-9f0b2d4c6e8a",
  "type": "deposit.confirmed",
  "created_at": "2026-09-11T20:05:41.118Z",
  "data": {
    "tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
    "log_index": 0,
    "asset": "USDT_TRON",
    "amount": "49.900000",
    "address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
    "invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
    "external_user_id": null,
    "block_time": "2026-09-11T20:04:38.000Z",
    "confirmations": 20
  }
}

Поля data у событий payout.*:

Поле Описание
payout_id Идентификатор выплаты
reference Идентификатор выплаты на стороне мерчанта
status confirmed или failed
asset Актив
amount Сумма выплаты со всеми знаками актива
to_address Адрес получателя, целиком
tx_hash Хэш транзакции
reason Только у payout.failed: что именно произошло

Пример payout.confirmed:

{
  "id": "9c4b1a2e-7d3f-4a5b-8c6d-0e1f2a3b4c5d",
  "type": "payout.confirmed",
  "created_at": "2026-09-12T12:15:03.402Z",
  "data": {
    "payout_id": "4c2f1a7e-93d5-4c0b-8a2e-5f6b7c8d9e01",
    "reference": "wd_501",
    "status": "confirmed",
    "asset": "USDT_TRON",
    "amount": "100.500000",
    "to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
    "tx_hash": "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0"
  }
}

Комиссию сети платит горячий кошелёк сервиса, и она не вычитается из суммы: получатель получает ровно amount (ADR-0023).

Подпись

Каждый вебхук содержит два заголовка:

X-Timestamp: <время отправки, секунды Unix>
X-Signature: sha256=<HMAC-SHA256 в hex>

Подпись — это HMAC-SHA256 по строке <X-Timestamp>.<сырое тело запроса> с секретом вебхуков мерчанта в качестве ключа. Подписывается именно сырое тело: байты в том виде, в каком они пришли, до разбора JSON. Разобранный и заново сериализованный JSON может отличаться порядком ключей и пробелами, и подпись не сойдётся.

Получатель обязан:

  • отклонять вебхук, если X-Timestamp отличается от текущего времени больше чем на 5 минут: так перехваченный вебхук нельзя отправить повторно позже;
  • сравнивать подписи за постоянное время, а не обычным сравнением строк.

Проверка на Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

// rawBody — Buffer с сырым телом запроса, до JSON.parse.
export function verifyWebhook({ rawBody, signatureHeader, timestampHeader, secret, nowMs = Date.now() }) {
  if (typeof timestampHeader !== "string" || !/^\d{1,12}$/.test(timestampHeader)) return false;
  if (Math.abs(nowMs / 1000 - Number(timestampHeader)) > TOLERANCE_SECONDS) return false;

  const match = /^sha256=([0-9a-f]{64})$/.exec(signatureHeader ?? "");
  if (match === null) return false;

  const expected = createHmac("sha256", secret).update(`${timestampHeader}.`).update(rawBody).digest();
  return timingSafeEqual(Buffer.from(match[1], "hex"), expected);
}

Доставка

  • Вебхук считается доставленным, если URL мерчанта ответил любым кодом 2xx за 10 секунд.
  • Иначе отправка повторяется через 10 секунд, 1 минуту, 5 минут, 30 минут, 2 часа и 6 часов после предыдущей попытки. После последней неудачи вебхук помечается недоставленным, и оператор получает сигнал.
  • Доставка «хотя бы один раз»: одно и то же событие может прийти несколько раз. Обработчик должен быть идемпотентным — повторная обработка события не должна повторно выдавать товар.
  • Событие записывается в той же транзакции базы, что и изменение, о котором оно сообщает. Поэтому событие не теряется при сбое между изменением и отправкой, но и не гарантирует порядок доставки: invoice.confirmed может прийти раньше invoice.seen. Ориентируйтесь на состояние из API.
  • Проекту без адреса вебхуков — например, заведённому в кабинете для счетов, выставляемых вручную, — события не записываются вовсе. Инвойсы при этом оплачиваются и зачисляются как обычно. События, случившиеся до того, как адрес задан, после его появления не досылаются: состояние берите из API. Заданный адрес можно заменить, но не удалить.

Отвечайте 2xx сразу после проверки подписи, а долгую обработку выполняйте асинхронно: иначе таймаут в 10 секунд приведёт к лишним повторам.

Ответ 3xx успехом не считается, и сервис по нему не переходит: подписанное тело не должно уходить на другой адрес. Меняете URL — сообщите оператору, он поменяет его в настройках мерчанта.

Каждая попытка доставки записывается: код ответа, длительность и ошибка. Оператор видит недоставленные события и может поставить их в очередь заново — см. застрявшие вебхуки.