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

Подписки и пользователи

Пользователь и Telegram, ссылка подписки, статусы, продление, трафик и устройства.

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

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

Если ваш пользователь есть в Telegram, приложите telegram_id и bot_id вашего бота: тогда он увидит подписку в мини-приложении бота и получит его уведомления. bot_id это UUID бота, его можно скопировать в кабинете: раздел «Ключ и вебхук», карточка «Ваши боты». Без Telegram хватает одного external_user_id, bot_id не нужен. Один Telegram нельзя привязать к двум разным external_user_id: второй получит 409 external_user_conflict.

Если у вашего бота включён «Пробник только подписчикам канала», POST /subscriptions/trial с telegram_id и bot_id выдаёт пробник только подписчику каналов бота, иначе 403 trial_channel_required с полем channels (ссылки и названия каналов). Без bot_id условие не действует: оно привязано к боту.

Переход с бота на API

Подписки, проданные в ваших ботах, уже видны в GET /subscriptions (поле source: bot, miniapp, web) и управляются по subscription_id: продление, заморозка, трафик, устройства, вебхуки по ним приходят как по своим. Чтобы находить их по своему external_user_id, привяжите людей из бота к своим id.

1. Посмотрите, кто есть. GET /clients?linked=false отдаёт людей с Telegram, но без external_user_id, у каждого до 20 последних подписок. GET /clients?telegram_id=... найдёт одного.

2. Привяжите. POST /clients/link с external_user_id и telegram_id, без покупки и без списаний. Повтор безопасен (already_linked). Перенести всю базу: POST /clients/link/batch, до 1000 строк за запрос, ответ по каждой строке.

3. Работайте как обычно. После привязки GET /subscriptions?external_user_id=... находит подписки, купленные в боте, а POST /subscriptions и POST /subscriptions/trial с тем же external_user_id выдают новые тому же человеку: он увидит их в мини-приложении бота.

Конфликты отвечают 409 external_user_conflict с полем conflict: telegram_taken значит, что этот Telegram уже под другим external_user_id (он в поле external_user_id ответа), external_id_taken значит, что ваш external_user_id уже у другого человека. Частый случай второго: раньше вы выдавали подписку этому id без telegram_id, и у нас уже есть отдельный пользователь API без Telegram. Двух пользователей с подписками API не склеивает, такую пару разберёт поддержка.

Человеку, которого у вас ещё нет (result: created), бот не записывается, даже если вы передали bot_id: он появится у бота, когда человек сам его откроет. В песочнице людей из ботов нет, поэтому linked=false там всегда пустой, а привязка и конфликты работают как на боевом.

Название подписки в VPN-клиенте (Happ, v2rayTun) вы задаёте в кабинете, раздел «Ключ и вебхук», карточка «Название подписки». Оно действует на подписки без bot_id, подключать для этого бота не нужно. Если передать bot_id, бот подключается к платформе целиком: мы забираем его апдейты и снимаем вебхук, который стоял у вас, а название берётся из настроек бота.

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

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

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

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

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

Белые списки: докупка трафика

Обход белых списков в нашем API это трафик на лимитных серверах. Он докупается пакетами, и вход один для подписки любого вида: POST /subscriptions/{id}/traffic с телом {"custom_id": "...", "gb": 50}.

Какие gb можно купить, видно в GET /plans: подписке на срок пакеты из traffic_packs, подписке «Навсегда» из lifetime.traffic_packs. Объём не из списка отвечает 422 со списком допустимых значений. Лимит traffic_limit_bytes вырастает сразу, деньги списываются с депозита. У «Навсегда» купленный трафик не сгорает и прибавляется к остатку.

Подписке на срок пакет продаётся, только пока она действует: замороженной или истёкшей ответ 409 subscription_frozen или subscription_expired. Пробной «Навсегда» докупки не продаются: 409 addon_unavailable.

Вебхук о покупке разный: у подписки на срок subscription.traffic_purchased, у «Навсегда» subscription.addon_purchased с addon_id (например gb_50). Подпишитесь на оба, если продаёте оба вида.

Пошаговый пример есть в сценарии «Докупить трафик (белые списки)».

Трафик и счётчик продлений

«Лимитные серверы» в этом API это серверы обхода белых списков: на обычных серверах трафик безлимитный, а на них действует лимит. Поэтому «купить белые списки» значит «докупить трафик».

traffic_limit_bytes это накопленный лимит на лимитных серверах, а не квота тарифа: полное продление прибавляет квоту тарифа к текущему лимиту, продление на N дней прибавляет её долю N / дней_в_тарифе, апгрейд прибавляет трафик за оставшиеся дни, пакет трафика прибавляет купленные гигабайты. GET /subscriptions/{id} отдаёт живое значение поставщика вместе с traffic_used_bytes, список и вебхуки отдают значение на момент последней операции. null значит, что лимита нет.

renewal_count считает полные периоды тарифа. Продление на N дней добавляет N / дней_в_тарифе в renewal_progress (от 0 до 1, не включая 1), и каждый набранный целый период переходит в renewal_count. Пример: 30-дневный тариф продлили на 45 дней, счётчик вырос на 1, а renewal_progress стал 0.5. Продление на 3 дня того же тарифа даёт renewal_progress 0.1 и не меняет renewal_count.

Синхронизация подписок

Держать свою базу в актуальном виде можно без запроса на каждую подписку. GET /subscriptions?updated_since=<момент> отдаёт подписки, изменившиеся с этого момента, по возрастанию updated_at, страницы по next_cursor. Запомните updated_at последней подписки и в следующий раз начните с него. updated_at меняется при любом изменении подписки, в том числе когда VPN-клиент подключается (не чаще раза в час).

В списке есть фильтры telegram_id, plan_uuid, product (term или lifetime) и is_trial. Живого расхода трафика в списке нет: он в карточке GET /subscriptions/{id} вместе с usage_updated_at, моментом, на который он верен.

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

Устройства

GET /subscriptions/{id}/devices показывает устройства, которые подключали конфиг, лимит задаёт тариф. Работает для подписок обоих видов: и на срок, и «Навсегда». У «Навсегда» last_seen значит последнее обновление конфига устройством, а отключить пользователя целиком поставщик не умеет, заморозки у такой подписки нет. DELETE /subscriptions/{id}/devices/{device_id} освобождает слот и по умолчанию отключает устройство: по этой ссылке оно больше не получит конфиг. Переустановка VPN-клиента тут не помогает, идентификатор устройства остаётся прежним. Если нужно только освободить место, чтобы подключить то же устройство заново, передайте ?block=false.

Отключённые устройства отдаёт GET /subscriptions/{id}/devices/blocked. Вернуть одно можно вызовом DELETE /subscriptions/{id}/devices/blocked/{block_id}: при следующем обновлении подписки в VPN-клиенте оно подключится снова и займёт слот, если он свободен. rotate-link тоже снимает все отключения, но меняет ссылку.

Бессрочная подписка «Навсегда»

Кроме подписок на срок продаётся бессрочная: 15 устройств, безлимитный трафик на обычных серверах и лимит на лимитных серверах, который не сгорает. В GET /plans она лежит отдельным блоком lifetime, а не в plans: available, plan_uuid, devices, limited_traffic_bytes, цена price_minor и рублёвая price_display_minor, докупки addons. available: false значит, что сейчас она не продаётся.

Покупается тем же POST /subscriptions с plan_uuid из блока lifetime. Сколько списано, смотрите в charged_minor ответа: доллары считаются по курсу рубля в момент покупки. У такой подписки product: "lifetime" и is_lifetime: true, end_date стоит в 2100 году ради совместимости, показывать его как дату не нужно. Продление, заморозка и апгрейд к ней не применяются: 409 unsupported_for_product. Трафик и устройства работают как у подписки на срок: /traffic и /devices.

Ещё 15 устройств добавляет POST /subscriptions/{id}/addons с addon_id: "devices_15" из lifetime.addons. Трафик тем же методом тоже покупается (gb_50, gb_200, gb_1000, gb_2000), но удобнее /traffic: он один для обоих видов подписки. Цена докупки это ваша закупка, без наценки.

Если ответ на покупку или докупку потерялся, повторите запрос с тем же custom_id: мы сначала выясняем у поставщика, прошла ли первая попытка, и повторно не покупаем, пока это не ясно. 503 upstream_unavailable на таком повторе значит, что результат ещё выясняется: повторите через несколько минут. Новый custom_id после потерянного ответа может купить вторую подписку, поэтому прежде спросите GET /operations/{custom_id}. Событие subscription.addon_purchased несёт рядом с subscription поле addon_id: что именно докуплено.

Статусы подписки

active
Подписка действует, конфиг отдаётся.
frozen
Заморожена: срок не идёт, конфиг не отдаётся. Терминальным не является.
expired
Срок вышел. Продление возобновляет подписку с текущего момента.
Собираем анонимную аналитику посещений. Подробнее