Ваш сайт или программа сама выдаёт подписки из нашего VPN. Нужен разработчик.

Вебхуки

События, тело и подпись, повторы доставки.

Вебхуки

Укажите https-адрес в кабинете (раздел «Ключ и вебхук») или через PUT /webhook. Секрет подписи выдаётся только в кабинете. Каждое событие приходит POST с JSON-телом и заголовками X-Event, X-Delivery-Id, X-Webhook-Timestamp (unix-время отправки), X-Signature-V2 и X-Signature.

Проверяйте X-Signature-V2: это sha256= и hex от HMAC-SHA256 на вашем секрете по строке <X-Webhook-Timestamp>.<сырое тело>. Сравнивайте constant-time до разбора JSON и отбивайте доставку, если метка расходится с вашими часами больше чем на 5 минут: так перехваченную доставку не переиграть позже. X-Signature (HMAC только от тела) остаётся для тех, кто проверяет её сейчас; её подделать тоже нельзя, но от повтора она не защищает.

В теле всегда есть event (имя события), event_id (уникален для события, по нему дедупьте), occurred_at, schema_version (версия формата тела, сейчас 1) и source, откуда пришло событие: api (ваш вызов), platform (бот или мини-приложение платформы), network (событие сети VPN), test (тестовая отправка). События подписки несут объект subscription в том же виде, что GET /subscriptions/{id}, но без traffic_used_bytes. deposit.credited несёт deposit и balance_after_minor.

События могут прийти не по порядку: повтор старой доставки придёт после свежей. Чтобы не откатить подписку к устаревшему состоянию, храните у себя occurred_at последнего применённого события по каждой подписке и пропускайте более старые.

Догнать пропущенное

Если ваш сервер лежал, ждать повторов не обязательно. GET /events?since=<момент> отдаёт события, которые готовились вашему вебхуку, по порядку и вместе со статусом доставки, тело в payload ровно такое же, как у вебхука. Листайте по next_cursor, пока он не null. События хранятся 30 дней; если вебхук не задан, их нет.

Журнал доставок с ответами вашего сервера есть и в API: GET /webhook/deliveries (фильтры status и event). Повторить одну доставку прямо сейчас: POST /webhook/deliveries/{delivery_id}/redeliver, ответ скажет, что ответил ваш сервер.

События

subscription.created
Подписка выдана (покупка или пробный период).
subscription.renewed
Подписка продлена: по тарифу, на N дней или автопродлением.
subscription.upgraded
Тариф изменён.
subscription.traffic_purchased
Докуплен трафик.
subscription.addon_purchased
Докупка к бессрочной подписке: трафик для лимитных серверов или +15 устройств, что именно, в поле addon_id.
subscription.frozen
Заморожена.
subscription.unfrozen
Разморожена, новый end_date.
subscription.expires_in_72h
До конца срока трое суток.
subscription.expires_in_48h
До конца срока двое суток.
subscription.expires_in_24h
До конца срока сутки.
subscription.expired
Срок вышел, конфиг больше не отдаётся.
subscription.traffic_threshold
Израсходовано 80% трафика на лимитных серверах.
deposit.credited
Пополнение зачислено на депозит.

Тело события

eventstring

Имя события.

event_iduuid

Уникальный id события: по нему дедупьте повторы.

occurred_atstring

Когда событие произошло.

sourcestring

Откуда событие: api, platform, network или test.

одно из:apiplatformnetworktest
schema_versioninteger

Версия формата тела. Растёт только при несовместимой смене полей; новые поля добавляются без неё.

subscriptionobject

Подписка на момент события, в том же виде, что в GET /subscriptions/{id}, но без traffic_used_bytes. Есть у событий subscription.*.

Пример тела события
{
  "event": "subscription.expires_in_24h",
  "event_id": "01a06321-0000-7000-8000-000000000002",
  "occurred_at": "2026-10-01T12:00:00Z",
  "source": "network",
  "schema_version": 1,
  "subscription": {
    "subscription_id": "01a06315-1270-705a-8d4c-58fea607bf2c",
    "client_id": "01a06310-8f2b-7c11-9a3e-2b5c7d9e1f00",
    "external_user_id": "user-42",
    "telegram_id": null,
    "bot_id": null,
    "source": "api",
    "plan_uuid": "550e8400-e29b-41d4-a716-446655440000",
    "plan_name": "Standard 30d",
    "is_trial": false,
    "product": "term",
    "is_lifetime": false,
    "status": "active",
    "subscription_url": "https://api.relayhub.surf/sub/wnT9NRf0Zk6McRRnp2nggc3K",
    "days": 30,
    "days_left": 1,
    "devices": 2,
    "traffic_limit_bytes": null,
    "auto_renew": false,
    "renewal_count": 0,
    "renewal_progress": 0,
    "created_at": "2026-09-02T12:00:00Z",
    "end_date": "2026-10-02T12:00:00Z",
    "frozen_at": null,
    "price_minor": 450,
    "price_usd": "4.50"
  }
}

Как проверить подпись

import crypto from 'node:crypto'

// raw: сырые байты тела (Buffer), а не распарсенный JSON
function verify(raw, headers, secret) {
  const ts = headers['x-webhook-timestamp'] ?? ''
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
  const signed = Buffer.concat([Buffer.from(ts + '.'), raw])
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(signed).digest('hex')
  const a = Buffer.from(expected), b = Buffer.from(headers['x-signature-v2'] ?? '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Собираем анонимную аналитику посещений. Подробнее