VPN Reseller API

Подписки по вашей закупочной цене, оплата с депозита, розница ваша. Ключ выпускается в кабинете партнёра.

22 эндпоинтовJSON, Bearerhttps://api.relayhub.surf/api/v1/vpn

Публичный API для партнёров платформы: выдавайте VPN-подписки своим пользователям из своего сайта, бота или биллинга. Оплата с предоплаченного депозита по вашей закупочной цене, розницу назначаете вы. Ключ выпускается в кабинете партнёра: hexpartners.pro/api/vpn/app.

Авторизация

Каждый запрос несёт заголовок Authorization: Bearer <ключ>. Ключ один на аккаунт, показывается один раз при выпуске; потерянный перевыпускается в кабинете, старый перестаёт работать сразу. Ключ тратит ваши деньги: храните его в переменной окружения, не в коде и не в логах.

Лимит: 60 запросов в минуту на ключ. Превышение отвечает 429 с заголовком Retry-After.

Деньги

Все суммы в долларах США и приходят парой: *_minor (целые центы, по ним считайте) и *_usd (строка «10.25», её показывайте). Цена тарифа это ваша закупка: себестоимость поставщика плюс наценка платформы. Курс рубля в ответах не участвует, цены не меняются от курса.

Депозит состоит из двух карманов: paid (внесено пополнениями, вывести нельзя) и earned (заработано на продажах в ботах платформы, выводится). Покупки через API списываются сначала с paid, потом с earned; spendable это их сумма. Нехватка средств отвечает 402 insufficient_balance до обращения к поставщику: подписка при этом не создаётся.

Идемпотентность

У каждой денежной операции есть custom_id, который придумываете вы (удобнее всего UUID). Повтор запроса с тем же custom_id и тем же телом возвращает результат первой попытки с idempotent_replay: true и денег не списывает. Тот же custom_id с другим телом отбивается 409 custom_id_conflict. Пока первая попытка выполняется, повтор получает 409 operation_in_progress: подождите секунду и повторите.

Правило одно: новая операция это новый custom_id, ретрай упавшего запроса это тот же custom_id. При 503 upstream_unavailable и таймауте повторяйте с тем же custom_id, двойного списания не будет.

Пользователь и Telegram

external_user_id это идентификатор человека в вашей системе, один человек = один id. Подписок у него может быть несколько. Больше о пользователе знать не нужно: ни телефона, ни почты.

Если ваш пользователь есть в Telegram, приложите telegram_id и bot_id вашего бота из кабинета. Тогда он увидит подписку в мини-приложении бота и получит его уведомления. Один Telegram нельзя привязать к двум разным external_user_id: второй получит 409 external_user_conflict.

Подписка и её ссылка

subscription_url это ссылка подписки для VPN-клиента, она не меняется при продлении и апгрейде. Статус подписки: active, frozen или expired. Истёкшая продлевается с текущего момента, действующая получает дни к концу срока. Замороженную сначала разморозьте: продлить её нельзя.

Пробный период бесплатный, один на external_user_id, доступен, если включён в кабинете. Пробную подписку нельзя продлить или заморозить, только перевести на платный тариф апгрейдом.

Продление на N дней

POST /subscriptions/{id}/renew-custom добавляет от 3 до 1095 дней. Цена пропорциональна тарифу: цена_тарифа / дней_в_тарифе × дней, округление вверх до цента. Трафик на лимитных серверах прибавляется в той же пропорции.

Вебхуки

Укажите https-адрес в кабинете или через PUT /webhook. Секрет подписи выдаётся только в кабинете. Каждое событие приходит POST с JSON-телом и заголовками X-Signature: sha256=<hex>, X-Event, X-Delivery-Id. Подпись: HMAC-SHA256 от сырых байтов тела на вашем секрете, сравнивайте constant-time до разбора JSON.

Отвечайте 2xx быстро, работу уносите в фон. При ошибке доставка повторяется с растущим интервалом (1, 2, 4 … 512 минут, всего 10 попыток), потом уходит в dead, и вы получаете сообщение от бота платформы. Одно и то же событие может прийти повторно: дедупите по event_id. Незнакомое событие игнорируйте, список пополняется. Источник истины остаётся за GET /subscriptions/{id}.

Депозит через API

POST /deposit выставляет счёт на сумму в центах, ответ содержит invoice_url. Комиссия провайдера прибавляется сверху, на депозит зачислится ровно указанная сумма. Одновременно открытых счетов не больше пяти. Зачисление приходит вебхуком deposit.credited и видно в GET /deposit/{custom_id}.

Версионирование

Все пути начинаются с /api/v1/vpn. Новые поля в ответах добавляются без смены версии: не ломайтесь на незнакомых ключах. Удаление или переименование поля выйдет только под /v2 с предупреждением в кабинете.

Тарифы

Депозит

Подписки

Устройства

Пополнение

Вебхуки

Статусы

activeПодписка действует, конфиг отдаётся.
frozenЗаморожена: срок не идёт, конфиг не отдаётся. Терминальным не является.
expiredСрок вышел. Продление возобновляет подписку с текущего момента.
Пополнение депозита:
pendingСчёт выставлен, ждём оплату. Ссылка живёт до expires_at.
creditedОплачен, деньги на депозите. Терминальный.терминальный
expiredНе оплачен вовремя. Терминальный, выставьте новый счёт.терминальный
cancelledОтменён. Терминальный.терминальный
failedПровайдер отклонил. Терминальный.терминальный
refundedВозврат. Терминальный.терминальный

События вебхуков

subscription.createdПодписка выдана (покупка или пробный период).
subscription.renewedПодписка продлена: по тарифу, на N дней или автопродлением.
subscription.upgradedТариф изменён.
subscription.traffic_purchasedДокуплен трафик.
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Пополнение зачислено на депозит.
Пример тела события
{
  "event": "subscription.expires_in_24h",
  "event_id": "01a06321-0000-7000-8000-000000000002",
  "occurred_at": "2026-10-01T12:00:00Z",
  "source": "network",
  "subscription": {
    "subscription_id": "01a06315-1270-705a-8d4c-58fea607bf2c",
    "external_user_id": "user-42",
    "telegram_id": null,
    "bot_id": null,
    "plan_uuid": "550e8400-e29b-41d4-a716-446655440000",
    "plan_name": "Standart 30d",
    "is_trial": 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,
    "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, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(raw).digest('hex')
  const a = Buffer.from(expected), b = Buffer.from(header ?? '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Коды ошибок

КодHTTPЧто случилосьЧто делатьПовтор
unauthorized401Неверный или отсутствующий API-ключПроверьте заголовок Authorization: Bearer <ключ>. Ключ выпускается в кабинете.нет, нужны изменения
api_disabled403Доступ к API для этого аккаунта закрытНапишите в поддержку площадки.нет, нужны изменения
partner_blocked403Аккаунт партнёра заблокированНапишите в поддержку площадки.нет, нужны изменения
forbidden403Объект принадлежит другому аккаунтуПроверьте идентификатор: бот или подписка не ваши.нет, нужны изменения
trial_disabled403Пробный период выключен в настройках партнёраВключите пробный период в кабинете, раздел «Маркетинг».нет, нужны изменения
not_found404Объект не найденПроверьте идентификатор.нет, нужны изменения
plan_not_found404Тариф не найден или недоступенВозьмите plan_uuid из GET /plans.нет, нужны изменения
subscription_not_found404Подписка не найденаПроверьте subscription_id. Чужие подписки тоже отвечают этим кодом.нет, нужны изменения
deposit_not_found404Пополнение не найденоПроверьте custom_id пополнения.нет, нужны изменения
device_not_found404Устройство не найденоСписок устройств: GET /subscriptions/{id}/devices.нет, нужны изменения
trial_plan_missing404Пробный тариф временно недоступенПовторите позже или продайте платный тариф.да, тот же запрос
insufficient_balance402Недостаточно средств на депозитеПополните депозит: POST /deposit или кабинет. В ответе есть required_minor и balance_minor.нет, нужны изменения
custom_id_conflict409Этот custom_id уже использован с другими параметрамиДля новой операции возьмите новый custom_id.нет, нужны изменения
operation_in_progress409Операция с этим custom_id ещё выполняетсяПовторите тот же запрос через секунду: вернётся результат первой попытки.да, тот же запрос
subscription_state409Операция не подходит к текущему состоянию подпискиПроверьте статус: GET /subscriptions/{id}.нет, нужны изменения
subscription_frozen409Подписка замороженаСначала разморозьте: POST /subscriptions/{id}/unfreeze.нет, нужны изменения
subscription_expired409Подписка истеклаПродлите подписку, после этого операция станет доступна.нет, нужны изменения
trial_already_used409Этот пользователь уже получал пробный периодПробный период выдаётся один раз на external_user_id.нет, нужны изменения
trial_not_renewable409Пробную подписку нельзя продлитьКупите платный тариф: POST /subscriptions или апгрейд.нет, нужны изменения
trial_not_freezable409Пробную подписку нельзя заморозитьЗаморозка доступна только платным подпискам.нет, нужны изменения
special_plan_used409Спецтариф этому пользователю уже выдавалсяВыберите обычный тариф.нет, нужны изменения
external_user_conflict409telegram_id уже привязан к другому external_user_idИспользуйте тот external_user_id, под которым этот Telegram уже заведён.нет, нужны изменения
webhook_not_configured409Адрес вебхука не заданСохраните URL: PUT /webhook.нет, нужны изменения
too_many_open_deposits409Слишком много неоплаченных пополненийОплатите или дождитесь истечения открытых счетов.да, тот же запрос
validation_error422Неверные параметры запросаСмотрите details: там поле и причина.нет, нужны изменения
upstream_rejected422Поставщик VPN отклонил запросПроверьте параметры. Если всё верно, напишите в поддержку.нет, нужны изменения
rate_limited429Слишком много запросовПодождите Retry-After секунд. Лимит: 60 запросов в минуту на ключ.да, тот же запрос
upstream_unavailable503Поставщик VPN временно недоступенПовторите через минуту с тем же custom_id: двойного списания не будет.да, тот же запрос
provider_unavailable503Платёжный способ временно недоступенВыберите другой способ из GET /deposit/providers.да, тот же запрос
maintenance503API на обслуживанииПовторите через несколько минут.да, тот же запрос
internal_error500Внутренняя ошибкаПовторите с тем же custom_id. Если повторяется, напишите в поддержку.да, тот же запрос

Грабли

custom_id это ваш ключ идемпотентности

Придумываете его вы. Ретрай упавшего запроса с тем же custom_id безопасен, новая операция требует новый custom_id. Один custom_id на все типы операций: повторно использовать его для другого действия нельзя.

Проверяйте депозит до продажи

GET /balance перед оформлением у себя: 402 после того, как покупатель у вас уже заплатил, оставляет его без подписки. Держите запас и подписку на deposit.credited.

Ссылка подписки не меняется

Продление и апгрейд не выдают новую subscription_url. Отдайте её один раз и не просите пользователя переустанавливать конфиг.

Не продлевайте замороженную

Срок замороженной подписки стоит на месте, купленные дни сгорели бы незаметно. API отвечает 409 subscription_frozen: сначала unfreeze.

Считайте в центах

Складывайте *_minor, показывайте *_usd. Арифметика во float даёт 0.30000000000000004 и расхождение в цент с вашим биллингом.

Вебхук не единственный источник

Доставка может опоздать или не дойти. Перед действием по событию перечитайте GET /subscriptions/{id}: там актуальный статус и end_date.

Не показывайте пользователю ошибки API дословно

Поле error написано для интегратора. Переведите error_code в своё сообщение и решите сами, повторять ли запрос (столбец «повтор» в таблице кодов).

Бриф для ИИ

Бриф для ИИ-агента

Полная спецификация одним текстом. Вставьте её в чат с нейросетью вместе с задачей, и она напишет клиент по этой доке, а не по догадкам.

Собираем анонимную аналитику посещений. Подробнее