Подписки по вашей закупочной цене, оплата с депозита, розница ваша. Ключ выпускается в кабинете партнёра.
https://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, двойного списания не будет.
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, доступен, если включён в кабинете. Пробную подписку нельзя продлить или заморозить, только перевести на платный тариф апгрейдом.
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}.
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 | Что случилось | Что делать | Повтор |
|---|---|---|---|---|
unauthorized | 401 | Неверный или отсутствующий API-ключ | Проверьте заголовок Authorization: Bearer <ключ>. Ключ выпускается в кабинете. | нет, нужны изменения |
api_disabled | 403 | Доступ к API для этого аккаунта закрыт | Напишите в поддержку площадки. | нет, нужны изменения |
partner_blocked | 403 | Аккаунт партнёра заблокирован | Напишите в поддержку площадки. | нет, нужны изменения |
forbidden | 403 | Объект принадлежит другому аккаунту | Проверьте идентификатор: бот или подписка не ваши. | нет, нужны изменения |
trial_disabled | 403 | Пробный период выключен в настройках партнёра | Включите пробный период в кабинете, раздел «Маркетинг». | нет, нужны изменения |
not_found | 404 | Объект не найден | Проверьте идентификатор. | нет, нужны изменения |
plan_not_found | 404 | Тариф не найден или недоступен | Возьмите plan_uuid из GET /plans. | нет, нужны изменения |
subscription_not_found | 404 | Подписка не найдена | Проверьте subscription_id. Чужие подписки тоже отвечают этим кодом. | нет, нужны изменения |
deposit_not_found | 404 | Пополнение не найдено | Проверьте custom_id пополнения. | нет, нужны изменения |
device_not_found | 404 | Устройство не найдено | Список устройств: GET /subscriptions/{id}/devices. | нет, нужны изменения |
trial_plan_missing | 404 | Пробный тариф временно недоступен | Повторите позже или продайте платный тариф. | да, тот же запрос |
insufficient_balance | 402 | Недостаточно средств на депозите | Пополните депозит: POST /deposit или кабинет. В ответе есть required_minor и balance_minor. | нет, нужны изменения |
custom_id_conflict | 409 | Этот custom_id уже использован с другими параметрами | Для новой операции возьмите новый custom_id. | нет, нужны изменения |
operation_in_progress | 409 | Операция с этим custom_id ещё выполняется | Повторите тот же запрос через секунду: вернётся результат первой попытки. | да, тот же запрос |
subscription_state | 409 | Операция не подходит к текущему состоянию подписки | Проверьте статус: GET /subscriptions/{id}. | нет, нужны изменения |
subscription_frozen | 409 | Подписка заморожена | Сначала разморозьте: POST /subscriptions/{id}/unfreeze. | нет, нужны изменения |
subscription_expired | 409 | Подписка истекла | Продлите подписку, после этого операция станет доступна. | нет, нужны изменения |
trial_already_used | 409 | Этот пользователь уже получал пробный период | Пробный период выдаётся один раз на external_user_id. | нет, нужны изменения |
trial_not_renewable | 409 | Пробную подписку нельзя продлить | Купите платный тариф: POST /subscriptions или апгрейд. | нет, нужны изменения |
trial_not_freezable | 409 | Пробную подписку нельзя заморозить | Заморозка доступна только платным подпискам. | нет, нужны изменения |
special_plan_used | 409 | Спецтариф этому пользователю уже выдавался | Выберите обычный тариф. | нет, нужны изменения |
external_user_conflict | 409 | telegram_id уже привязан к другому external_user_id | Используйте тот external_user_id, под которым этот Telegram уже заведён. | нет, нужны изменения |
webhook_not_configured | 409 | Адрес вебхука не задан | Сохраните URL: PUT /webhook. | нет, нужны изменения |
too_many_open_deposits | 409 | Слишком много неоплаченных пополнений | Оплатите или дождитесь истечения открытых счетов. | да, тот же запрос |
validation_error | 422 | Неверные параметры запроса | Смотрите details: там поле и причина. | нет, нужны изменения |
upstream_rejected | 422 | Поставщик VPN отклонил запрос | Проверьте параметры. Если всё верно, напишите в поддержку. | нет, нужны изменения |
rate_limited | 429 | Слишком много запросов | Подождите Retry-After секунд. Лимит: 60 запросов в минуту на ключ. | да, тот же запрос |
upstream_unavailable | 503 | Поставщик VPN временно недоступен | Повторите через минуту с тем же custom_id: двойного списания не будет. | да, тот же запрос |
provider_unavailable | 503 | Платёжный способ временно недоступен | Выберите другой способ из GET /deposit/providers. | да, тот же запрос |
maintenance | 503 | API на обслуживании | Повторите через несколько минут. | да, тот же запрос |
internal_error | 500 | Внутренняя ошибка | Повторите с тем же 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.
Поле error написано для интегратора. Переведите error_code в своё сообщение и решите сами, повторять ли запрос (столбец «повтор» в таблице кодов).
Полная спецификация одним текстом. Вставьте её в чат с нейросетью вместе с задачей, и она напишет клиент по этой доке, а не по догадкам.