logo
logo

Высокопроизводительные виртуальные серверы в России и Европе.

    УСЛУГИ
  • Виртуальные серверы
  • Выделенные серверы
  • Домены
    ПОДДЕРЖКА
  • Написать в Telegram
  • support@vdsok.guru
  • abuse@as202831.network
  • Контакты
    ИНСТРУМЕНТЫ
  • API для разработчиков
  • Looking Glass
    ПРАВОВАЯ ИНФОРМАЦИЯ
  • Пользовательское соглашение
  • Политика конфиденциальности
  • Публичная оферта
  • TrustPilot
© 2026 VDSok.guru. Все права защищены.
CryptoBot Lolz.Guru Heleket FreeKassa Lava.ru 2328.io NOWPayments
DMCA.com Protection Status
Client API v1

API для разработчиков

Всё, что вы делаете в кабинете, — из кода: серверы, домены, баланс, счета и вебхуки. JSON поверх HTTPS, ключи со скоупами, sandbox и SDK для Node, Python и PHP.

Интерактивный справочникСкачать openapi.json
Содержание
Начало
Обзор и базовый URL
Аутентификация, скоупы и пресеты
Sandbox: тестовые ключи
Соглашения
Ошибки: конверт и коды
Лимиты запросов и заголовки
Пагинация
Идемпотентность
Деньги и даты
Сценарии
Серверы: полный сценарий1. Каталог и котировка2. Заказ3. Статус4. Питание5. Переустановка и сброс пароля6. Дополнительные IP и PTR7. Продление и автопродление8. Удаление с возвратом
Домены: полный сценарий1. Зоны и доступность2. Регистрация3. NS, privacy, автопродление4. Продление и трансфер
Биллинг: баланс, пополнение, счетаБаланс и транзакцииПополнение: ссылка на оплатуСчета: список, PDF, оплата
ВебхукиПодпискаПроверка подписиТест, журнал, повтор
Инструменты
SDK
Changelog
Спецификация OpenAPI

Обзор и базовый URL

VDSok Client API — REST-интерфейс поверх HTTPS с JSON-телами. Через него доступно всё, что клиент делает в кабинете, кроме тикетов, выделенных серверов, суб-аккаунтов и редактирования профиля: баланс и счета, каталог VDS, серверы (заказ, продление, питание, переустановка, IP, PTR, SSH-ключи, удаление с возвратом), домены, API-ключи и вебхуки.

Базовый URL

Все пути в этом руководстве относительны:

https://vdsok.guru/api/v1

Версия живёт в пути. Ломающие изменения уйдут в /v2; v1 только растёт: новые поля и эндпоинты могут появиться в любой момент, поэтому неизвестные поля в ответах игнорируйте.

Соглашения

  • Только JSON. Тело POST/PUT/PATCH не в application/json отклоняется с 415, тело больше 64 КБ — с 413.
  • Время — RFC 3339 в UTC с суффиксом Z: 2026-09-15T10:00:00Z.
  • Деньги — десятичная строка с 2–4 знаками после точки, никогда не float, всегда рядом с полем currency (ISO 4217).
  • server_id — id виртуальной машины в панели, то же число, что кабинет показывает в URL.
  • Каждый ответ несёт X-Request-ID; указывайте его в обращениях в поддержку.

Быстрый старт

  • Создайте ключ в кабинете: Настройки → API (/my/api). Ключ показывается один раз.
  • Вызовите GET /me — это самый дешёвый способ проверить ключ и увидеть его скоупы и лимиты.
  • Дальше: аутентификация, ошибки, сценарий работы с серверами.
curl https://vdsok.guru/api/v1/me \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX"

Аутентификация, скоупы и пресеты

Каждый запрос несёт заголовок Authorization: Bearer <ключ>. Ключ начинается с vk_live_ (боевой) или vk_test_ (тестовый, см. Sandbox).

Ключи создаёт владелец аккаунта в кабинете (/my/api): имя, режим live/test, набор скоупов, необязательный список разрешённых IP/CIDR и окно действия (not_before/expires_at). Создание подтверждается кодом TOTP или паролем; секрет показывается один раз. Ключи не зависят от сессий кабинета: смена пароля, «выйти везде» и настройка 2FA их не трогают; бан аккаунта отзывает все ключи. Через API ключи не создаются — только просматриваются (GET /keys) и отзывается свой ключ.

Скоупы

Каждая операция перечисляет требуемые скоупы в x-scopes спецификации; пустой список означает «любой валидный ключ» (каталог, /me). Совсем без ключа доступны только GET /health (всегда 200 со status: ok | disabled — это проба доступности, а не защищённая ручка) и GET /openapi.json. Нехватка скоупа → 403 insufficient_scope, в details.required — чего не хватило.

СкоупЧто открывает
account:readGET /account — профиль, группа, скидка
balance:readбаланс, транзакции, лимиты пополнения
balance:topupсоздание счёта на пополнение и ссылки на оплату
invoices:readсписок счетов, счёт, PDF
invoices:payоплата счёта с баланса, платёжная ссылка
servers:readсерверы, живой статус, IP, SSH-ключи, заказы, котировка возврата
servers:manageпитание, переустановка, сброс пароля, PTR, PATCH, добавление и удаление SSH-ключей
servers:orderзаказ, продление, покупка и освобождение IP
servers:deleteудаление сервера с возвратом
domains:readдомены, проверка доступности
domains:manageавтопродление, WHOIS-privacy, NS
domains:orderрегистрация, продление, трансфер
keys:readсписок ключей аккаунта
webhooks:manageподписки, доставки, тест, повтор

Пресеты

Пресеты в кабинете — просто наборы скоупов, их можно править чекбоксами после выбора.

ПресетСостав
read_onlyвсе *:read
operateread_only + servers:manage, servers:order, domains:manage, invoices:pay
fullвсе скоупы, включая balance:topup, servers:delete, domains:order, webhooks:manage

Kill switch

DELETE /keys/{key_id} отзывает только тот ключ, которым сделан запрос, независимо от его скоупов. Так утёкший ключ можно погасить откуда угодно (из CI, с сервера), не выдавая каждому ключу право уничтожить всю интеграцию. Остальные ключи отзываются в кабинете. Работает и для тестовых ключей.

Храните ключи в секретах CI или менеджере секретов, не в репозитории. Для каждой интеграции заводите отдельный ключ с минимальными скоупами и IP-списком — так утечку видно по last_used_ip и её проще локализовать.
# A key without servers:delete gets 403 with the missing scope in details:
curl -i -X DELETE https://vdsok.guru/api/v1/servers/2001 \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Idempotency-Key: $(uuidgen)"
# HTTP/1.1 403 Forbidden
# {"error":{"code":"insufficient_scope","message":"This key lacks servers:delete",
#           "request_id":"9f1c2a9d-4b7e-4a21-8d3f-0c6e5b2a1d44","details":{"required":["servers:delete"]}}}

# Kill switch: revoke the calling key itself
KEY_ID=$(curl -s https://vdsok.guru/api/v1/me -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq .key.id)
curl -X DELETE https://vdsok.guru/api/v1/keys/$KEY_ID -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX"

Sandbox: тестовые ключи

Тестовый ключ (vk_test_…) создаётся в том же кабинете, что и боевой, и ходит на тот же базовый URL — отдельного стенда нет.

Тестовые ключи читают реальные данные вашего аккаунта. GET /servers, GET /balance, GET /invoices тестовым ключом возвращают настоящие серверы, настоящий баланс и настоящие счета. Отличие только в записи: ни одна мутация не выполняется по-настоящему.

Что делает тестовый ключ:

  • Не двигает деньги: заказ, продление, пополнение, оплата счёта и удаление возвращают правдоподобный ответ, но баланс не меняется, VM и домены не создаются, платёжные шлюзы не вызываются.
  • Заказ сервера возвращает фейковый сервер с id >= 9000000000. Он живёт 24 часа и виден в GET /servers только этому ключу; над ним можно вызывать статус, питание, переустановку, IP и удаление — всё тоже имитируется.
  • Каждый ответ тестовому ключу несёт заголовок X-Sandbox: true. Проверяйте его в тестах, чтобы не спутать режимы.
  • Идемпотентность, лимиты и ошибки валидации работают как в live: 402, 409, 429 можно получить и в песочнице.

Поведение каждой операции под тестовым ключом описано в x-sandbox спецификации:

x-sandboxЗначение
realто же, что live — все чтения, DELETE /keys/{key_id}
fakeимитация записи: заказ, продление, питание, домены, пополнение
forbidden403 sandbox_not_supported — все ручки вебхуков; подписки только для live

Если тестовые ключи выключены на стороне VDSok, любой запрос отвечает 403 sandbox_disabled.

# Same URL, test key: the order is simulated, the header says so
curl -i -X POST https://vdsok.guru/api/v1/servers \
  -H "Authorization: Bearer vk_test_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"tariff_id": 12, "os": "ubuntu-24.04", "name": "sandbox-01", "months": 1}'
# HTTP/1.1 201 Created
# X-Sandbox: true
# {"server":{"id":9000000042,"name":"sandbox-01",...},"root_password":"...","charged":"5.90",...}

Ошибки: конверт и коды

У API одна форма ошибки — на любой статус, включая 404/405/413/500 уровня приложения:

{
  "error": {
    "code": "insufficient_funds",
    "message": "Balance 4.10 USD is below the required 5.90 USD",
    "request_id": "9f1c2a9d-4b7e-4a21-8d3f-0c6e5b2a1d44",
    "details": {"required": "5.90", "balance": "4.10", "shortfall": "1.80", "currency": "USD"}
  }
}
  • code — машинный код из закрытого списка ниже. Ветвитесь по нему, а не по message.
  • message — человекочитаемый английский текст, не для парсинга; формулировки могут меняться.
  • request_id — тот же, что в заголовке X-Request-ID; указывайте его в поддержку.
  • details — дополнение, зависящее от кода: required (скоупы), fields (ошибки валидации по полям), денежные цифры для insufficient_funds, status для service_state.

Список кодов закрытый по каждому статусу, но клиент должен терпеть неизвестные коды и обрабатывать их по HTTP-статусу — так добавление кода не ломает интеграции.

СтатусКоды
400invalid_request, validation_error, invalid_cursor, idempotency_key_required, os_not_allowed, invalid_period, invalid_action, upstream_rejected
401invalid_token, key_expired, key_not_yet_valid
402insufficient_funds — ничего не списано и не создано
403insufficient_scope, ip_not_allowed, account_suspended, api_disabled_for_account, sandbox_not_supported, sandbox_disabled, server_blocked, domain_blocked
404not_found — объекта нет на этом аккаунте (чужие объекты тоже 404, не 403)
409conflict, idempotency_conflict, idempotency_in_progress, operation_in_progress, service_state, no_capacity, tariff_unavailable, ip_limit_reached, domain_taken, domain_exists, cancel_pending
413 / 415payload_too_large, unsupported_media_type
429rate_limited
500server_error — сбой у нас, request id уже в логах
502upstream_error — панель, регистратор или шлюз ответили ошибкой; для денежных операций ничего не списано, если ответ не говорит иного
503api_disabled, upstream_unavailable, temporarily_unavailable — повторите после Retry-After
504upstream_timeout — панель, регистратор или шлюз не ответили вовремя; повторяйте с тем же Idempotency-Key

Что повторять: 429, 502, 503, 504 — безопасно для GET и для мутаций с Idempotency-Key. Остальные мутации без ключа идемпотентности не повторяйте вслепую.

# -f makes curl exit non-zero on 4xx/5xx; -s hides progress; the body still has the envelope
curl -s -w "\n%{http_code}\n" https://vdsok.guru/api/v1/servers/2001 \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX"
# {"error":{"code":"not_found","message":"Server 2001 not found","request_id":"9f1c2a9d-4b7e-4a21-8d3f-0c6e5b2a1d44"}}
# 404

# Extract the code with jq
curl -s https://vdsok.guru/api/v1/servers/2001 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq -r '.error.code // "ok"'

Лимиты запросов и заголовки

По умолчанию — 120 запросов в минуту на ключ плюс отдельная корзина 20 в минуту для дорогих вызовов. Лимиты считаются на ключ (не на аккаунт и не на IP), окно фиксированное — 60 секунд. Сотрудники VDSok могут поднять лимиты конкретному ключу; текущий потолок и остаток приходят в заголовках X-RateLimit-Limit и X-RateLimit-Remaining каждого ответа, а настройки конкретного ключа видны в GET /keys/{id}. Чтения в кабинете в счёт API не идут.

Дорогие вызовы помечены x-expensive: true в спецификации:

  • GET /servers/{server_id}/status и GET /servers/{server_id}?include=live — живой опрос панели;
  • GET /domains/availability — запрос к регистратору;
  • GET /catalog/quote;
  • POST /servers, POST /servers/{server_id}/ips, POST /balance/topup;
  • GET /invoices/{invoice_id}/pdf;
  • POST /webhooks/{webhook_id}/test, POST /webhooks/deliveries/{delivery_id}/redeliver.

Отдельно: POST /servers/{server_id}/actions/reset-password — не больше 5 раз за 5 минут на сервер. Запросы с невалидным ключом ограничены 30 в минуту на IP — против перебора.

Заголовки

ЗаголовокСмысл
X-RateLimit-Limitсколько запросов в минуту разрешено в корзине, в которую попал вызов (обычная или дорогая)
X-RateLimit-Remainingсколько осталось в текущем 60-секундном окне
X-RateLimit-Resetunix-время (секунды) сброса окна
Retry-Afterсекунды до повтора; есть у 429 и 503, а также у 409 idempotency_in_progress

При 429 rate_limited в details указаны bucket (normal/expensive) и limit. Правильная реакция — подождать Retry-After и повторить, а не долбить в цикле: окно фиксированное, раньше оно не откроется.

Не опрашивайте GET /servers/{id}/status чаще, чем нужно: 20 дорогих вызовов в минуту делят между собой все живые статусы, проверки доменов и заказы этого ключа. Для мониторинга десятков серверов используйте GET /servers (не дорогой) и вебхуки server.suspended/server.terminated.
curl -sD - -o /dev/null https://vdsok.guru/api/v1/servers/2001/status \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | grep -i -E "x-ratelimit|retry-after"
# X-RateLimit-Limit: 20          <- expensive bucket
# X-RateLimit-Remaining: 19
# X-RateLimit-Reset: 1789466460

Пагинация

Списки с потенциально большим числом элементов (/servers, /invoices, /transactions, /orders, /domains, /webhooks/{id}/deliveries) отдают страницу и курсор:

{"data": [ … ], "next_cursor": "eyJpZCI6MjAwMSwic2lnIjoi…"}
  • limit — от 1 до 100, по умолчанию 50.
  • next_cursor — передайте обратно как ?cursor=, чтобы получить следующую страницу; null означает, что это последняя.
  • Курсоры непрозрачные и подписанные: не собирайте их руками и не парсите — испорченный курсор даёт 400 invalid_cursor. Курсор привязан к набору фильтров: меняете status — начинайте с первой страницы.
  • Порядок фиксированный: новые сначала. Добавленные во время обхода элементы могут попасть в начало, но дубликатов и пропусков внутри обхода нет.

Короткие справочники (/catalog/*, /servers/{id}/ips, /ssh-keys, /keys, /webhooks) возвращают {"data": [...]} целиком, без курсора.

# First page, 20 active servers
curl -s "https://vdsok.guru/api/v1/servers?status=active&limit=20" \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '{n: (.data|length), next: .next_cursor}'

# Next page: pass next_cursor back verbatim (URL-encode it)
CURSOR=$(curl -s "https://vdsok.guru/api/v1/servers?status=active&limit=20" \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq -r '.next_cursor')
curl -s "https://vdsok.guru/api/v1/servers?status=active&limit=20&cursor=$CURSOR" \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX"

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

Операции, которые двигают деньги или трогают внешние системы, помечены x-idempotent: true и требуют заголовок Idempotency-Key: POST /balance/topup, POST /invoices/{id}/pay, POST /servers, DELETE /servers/{id}, POST /servers/{id}/renew, POST /servers/{id}/ips, POST /domains, POST /domains/{id}/renew, POST /domains/transfers.

Ключ — любая строка 16..128 символов, уникальная для логической операции; UUID v4 подходит. Генерируйте его до первой попытки и переиспользуйте при повторах — в этом весь смысл. Ключ запоминается на 7 дней вместе с дайджестом эндпоинта и тела:

СитуацияОтвет
тот же ключ, тот же запроссохранённый ответ воспроизводится; секреты вроде root_password в повторе затёрты
тот же ключ, другое тело или другой эндпоинт409 idempotency_conflict
тот же ключ, пока первый запрос ещё выполняется409 idempotency_in_progress с Retry-After
ключа нет400 idempotency_key_required

Итог операции сохраняется всегда, включая ответы 5xx, — иначе повтор бесконечно получал бы idempotency_in_progress. Поэтому повтор никогда не спишет дважды. Заказ сервера с таймаутом панели (202 provisioning) при повторе с тем же ключом вернёт тот же 202 с тем же invoice_id — дальше опрашивайте GET /orders/{invoice_id}.

Пароль от нового сервера приходит только в первом 201. Если вы получили сетевую ошибку после успешного создания и повторили запрос, в ответе будет пустой root_password — используйте POST /servers/{id}/actions/reset-password.
# Generate the key once, reuse it for every retry of this renewal
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST https://vdsok.guru/api/v1/servers/2001/renew \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{"months": 3}'

# Sending it again replays the stored answer, nothing is charged twice
curl -X POST https://vdsok.guru/api/v1/servers/2001/renew \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{"months": 3}'

Деньги и даты

Деньги

Все суммы — строки вида "5.90", "0.0083", "-12.00" (шаблон ^-?[0-9]+\.[0-9]{2,4}$), рядом всегда есть currency — ISO 4217 код валюты аккаунта (например, USD). Float в JSON не используется никогда: 0.1 + 0.2 в двоичной арифметике не равно 0.3, а в биллинге такая погрешность превращается в расхождение на счёте.

  • Разбирайте суммы в десятичный тип: Decimal в Python, BigInt в минимальных единицах или decimal.js в Node, bcmath/BigDecimal в PHP.
  • В теле запросов (amount при пополнении) присылайте строку с двумя знаками: "25.00". Число 25 тоже примется, но 25.005 — нет.
  • Часовые цены (price_hourly) имеют до 4 знаков: "0.0083".
  • Транзакции несут amount по модулю и отдельное поле direction (credit/debit); Money со знаком встречается только в корректировках.

Даты

Все моменты времени — RFC 3339 в UTC с суффиксом Z: 2026-09-15T10:00:00Z. Поля, которые могут быть пустыми (next_due_at, expires_at, paid_at), приходят как null, а не как пустая строка или 0000-00-00.

  • В запросах (since/until у /transactions) тоже присылайте RFC 3339 с Z или смещением.
  • X-RateLimit-Reset и X-Webhook-Timestamp — unix-секунды, не миллисекунды.
  • Возвраты при удалении считаются по целым дням неиспользованного периода (days_left в RefundQuote), сравнивайте свои расчёты в днях, а не в секундах.
# Sum debits of the last 30 days with jq: strings -> numbers only at the very end
SINCE=$(date -u -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ)
curl -s "https://vdsok.guru/api/v1/transactions?direction=debit&since=$SINCE&limit=100" \
  -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '[.data[].amount | tonumber] | add'

Серверы: полный сценарий

Ниже — жизненный цикл одного VDS от каталога до удаления. Всё то же, что делает кабинет, с теми же проверками и ценами. server_id везде — id виртуальной машины в панели (id в ответах /servers).

Статусы сервера (status): active — работает или хотя бы не остановлен биллингом; stopped — выключен клиентом; suspended — отключён за неуплату или сотрудником; pending_cancel — одобрена заявка на отмену в конце периода; cancelled — удалён клиентом или по заявке; terminated — удалён после долгой неуплаты. Флаги flags.blocked (заблокирован сотрудником, любая мутация → 403 server_blocked), flags.expired, flags.is_test уточняют, что можно сделать.

Пока над сервером идёт заказ, удаление или смена IP, параллельная мутация того же объекта отвечает 409 operation_in_progress — дождитесь и повторите.

1. Каталог и котировка

GET /catalog/tariffs отдаёт тарифы категории VDS, доступные к заказу, уже с учётом скидки вашей группы (price_monthly; list_price_monthly — публичная цена). in_stock: false означает, что в локации нет мест — заказ ответит 409 no_capacity. GET /catalog/os?tariff_id=12 исключает образы, запрещённые тарифом (excluded_os); slug образа — это значение os при заказе.

GET /catalog/quote считает цену теми же функциями, что и заказ: базовая сумма, скидки группы/лояльности/объёма/периода, промокод — и говорит, хватает ли баланса (balance_sufficient, shortfall). Промокод проверяется, но не тратится. Укажите либо months (1, 3, 6, 12), либо hours (1..720, если у тарифа hourly_available).

curl -s https://vdsok.guru/api/v1/catalog/tariffs -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '.data[] | {id, name, loc: .location.code, price_monthly, in_stock}'
curl -s "https://vdsok.guru/api/v1/catalog/os?tariff_id=12" -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '.data[].slug'
curl -s "https://vdsok.guru/api/v1/catalog/quote?tariff_id=12&months=3" -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '{total, currency, balance_sufficient, shortfall, discounts}'

2. Заказ

POST /servers работает синхронно, как кабинет: проверяет тариф, ОС, имя и наличие мест, считает цену, до любой записи проверяет баланс (402 insufficient_funds не имеет побочных эффектов), затем списывает и создаёт VM. Обязателен Idempotency-Key.

  • 201 — сервер создан и запущен; в ответе server, одноразовый root_password, invoice_id, charged, balance_after. Сохраните пароль сразу: в повторе по тому же ключу идемпотентности он пустой.
  • 202 — панель не ответила вовремя (и только в этом случае: таймаут панели не превращается в 504): деньги списаны, заказ продолжается в фоне. В ответе Order с order_url. Опрашивайте GET /orders/{invoice_id} раз в несколько секунд, пока status остаётся provisioning. Успех — status: active с server_id; любое другое терминальное состояние означает, что сервер не создан, а списание возвращено на баланс автоматически.

SSH-ключи: положите публичные ключи в POST /ssh-keys один раз и передавайте их id в ssh_key_ids при заказе и переустановке. Если password не указан, он будет сгенерирован и возвращён один раз. Промокод передаётся в promo_code; поля формы заказа, настроенные VDSok, — в custom_fields.

# Store an SSH key once
curl -s -X POST https://vdsok.guru/api/v1/ssh-keys -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" -H "Content-Type: application/json" \
  -d '{"name": "laptop", "public_key": "'"$(cat ~/.ssh/id_ed25519.pub)"'"}' | jq .id

# Order: months=1, key id 3 injected into the image
curl -s -X POST https://vdsok.guru/api/v1/servers -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"tariff_id": 12, "os": "ubuntu-24.04", "name": "web-01", "months": 1, "ssh_key_ids": [3]}' \
  | jq '{status: (.server.status // .status), id: (.server.id // .server_id), ip: .server.ip, root_password, invoice_id}'

# On 202 poll the order while it is still provisioning
curl -s https://vdsok.guru/api/v1/orders/10231 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '{status, server_id, invoice_id}'

3. Статус

GET /servers/{server_id} — карточка сервера из биллинга: статус, тариф, IP, ресурсы, billing (цикл, next_due_at, recurring_amount, auto_renew) и refund_quote — сколько вернулось бы при удалении прямо сейчас. Это дёшево и подходит для частого опроса.

GET /servers/{server_id}/status (или ?include=live у карточки) идёт в панель за живыми данными: power (running/stopped/unknown), CPU, память, диск, аптайм. Это дорогой вызов из корзины 20/мин. Серверы, импортированные из истории с синтетическими id (>= 2000000000), записи в панели не имеют — 409 service_state.

curl -s https://vdsok.guru/api/v1/servers/2001 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '{status, ip, next_due: .billing.next_due_at, auto_renew: .billing.auto_renew, refund: .refund_quote.amount}'
curl -s https://vdsok.guru/api/v1/servers/2001/status -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '{power, cpu_percent, mem: .memory, uptime_seconds}'

4. Питание

POST /servers/{server_id}/actions/power с телом {"action": "start" | "stop" | "restart"}. Ответ 202 означает, что панель приняла команду; фактическое состояние проверяйте через живой статус спустя несколько секунд. Неизвестное действие → 400 invalid_action; сервер в suspended или blocked → 409 service_state / 403 server_blocked. Скоуп servers:manage.

curl -s -X POST https://vdsok.guru/api/v1/servers/2001/actions/power -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -d '{"action": "restart"}'
# {"server_id":2001,"action":"restart","status":"accepted","message":null}

5. Переустановка и сброс пароля

POST /servers/{server_id}/actions/reinstall с {"os": "<slug>", "password"?: "...", "ssh_key_ids"?: [...]} уничтожает все данные на диске. Образ должен быть разрешён тарифу, иначе 400 os_not_allowed. Если password не передан, новый генерируется и возвращается один раз в root_password; 202 со status: reinstalling. Когда установка закончится, придёт вебхук server.reinstalled.

POST /servers/{server_id}/actions/reset-password генерирует новый root-пароль и показывает его один раз. Ограничение — 5 вызовов за 5 минут на сервер.

curl -s -X POST https://vdsok.guru/api/v1/servers/2001/actions/reinstall -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -d '{"os": "debian-12", "ssh_key_ids": [3]}'
# {"server_id":2001,"status":"reinstalling","os":"debian-12","root_password":"..."}

curl -s -X POST https://vdsok.guru/api/v1/servers/2001/actions/reset-password -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq -r .password

6. Дополнительные IP и PTR

GET /servers/{server_id}/ips — все адреса сервера (v4 и v6) с primary, ptr, price_monthly. GET /servers/{server_id}/ips/quote заранее говорит, сколько спишется прямо сейчас (prorated_now — за остаток текущего периода) и сколько адресов ещё можно добавить (extra_ips / max_extra_ips).

POST /servers/{server_id}/ips (с Idempotency-Key, скоуп servers:order) покупает один IPv4: списывает prorated_now и поднимает billing.recurring_amount. Ответ — {"success", "server_id", "charged", "currency", "days"}: самого адреса в нём нет, панель выдаёт его асинхронно — прочитайте его через GET /servers/{server_id}/ips. Лимит на сервер → 409 ip_limit_reached. DELETE /servers/{server_id}/ips/{ip_id} освобождает дополнительный адрес без возврата; recurring_amount снижается со следующего периода. Основной адрес освободить нельзя (409 conflict).

PUT /servers/{server_id}/ips/{ip_id}/ptr с {"domain": "mail.example.com"} ставит обратную запись и отвечает {"id", "ptr"}; имя — минимум две метки из букв, цифр, точек и дефисов. Поле называется domain (не ptr), а "" или null снимают запись.

curl -s https://vdsok.guru/api/v1/servers/2001/ips/quote -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '{prorated_now, price_monthly, extra_ips, max_extra_ips}'
curl -s -X POST https://vdsok.guru/api/v1/servers/2001/ips -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" -H "Idempotency-Key: $(uuidgen)" \
  | jq '{success, charged, currency, days}'   # the address arrives asynchronously
curl -s https://vdsok.guru/api/v1/servers/2001/ips -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '.data[] | select(.primary | not) | {id, ip}'
curl -s -X PUT https://vdsok.guru/api/v1/servers/2001/ips/501/ptr -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -d '{"domain": "mail.example.com"}' | jq '{id, ptr}'
curl -s -X DELETE https://vdsok.guru/api/v1/servers/2001/ips/501 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX"

7. Продление и автопродление

POST /servers/{server_id}/renew списывает с баланса и продлевает billing.next_due_at; досрочное продление прибавляется к текущему периоду. Месячные серверы принимают {"months": 1|3|6|12}, почасовые — {"hours": 1..720}. Обязателен Idempotency-Key; при нехватке средств — 402 без побочных эффектов.

PATCH /servers/{server_id} меняет auto_renew, name и notes. При включённом автопродлении биллинг сам выставит и оплатит счёт с баланса перед next_due_at; сумма — billing.recurring_amount. Следите за GET /balance → upcoming_7d/low_balance или подпишитесь на вебхук balance.low.

curl -s -X POST https://vdsok.guru/api/v1/servers/2001/renew -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" -d '{"months": 3}' \
  | jq '{charged, balance_after, next_due_at}'

curl -s -X PATCH https://vdsok.guru/api/v1/servers/2001 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -d '{"auto_renew": true, "notes": "prod, do not stop"}' \
  | jq '.billing.auto_renew'

8. Удаление с возвратом

Сначала GET /servers/{server_id}/refund-quote: amount — сколько вернётся на баланс, refundable и excluded_reason — почему может быть ноль (promo_tariff — тариф помечен как невозвратный, blocked — сервер заблокирован администратором, unpaid — услуга просрочена, no_payment_record — оплаченных счетов нет; так получается и с тестовыми серверами). Возврат считается пропорционально целым дням неиспользованного оплаченного периода по реально оплаченным счетам (breakdown), а не по цене тарифа; выплаченные реферальные бонусы по этим счетам вычитаются (referral_adjustment).

DELETE /servers/{server_id} (скоуп servers:delete, Idempotency-Key) удаляет VM из панели и зачисляет возврат; в ответе DeleteResult с refund, balance_after и cancel_request_withdrawn — если была заявка на отмену, она отзывается. Уже удалённый сервер (cancelled/terminated) отвечает 409 service_state, заблокированный — 403 server_blocked. Если панель ответила ошибкой, отличной от «уже удалён», вернётся 502, и ничего не изменится. Придёт вебхук server.terminated.

Удаление необратимо, а котировка меняется каждый день. Показывайте пользователю refund_quote.amount и требуйте подтверждения перед DELETE.
curl -s https://vdsok.guru/api/v1/servers/2001/refund-quote -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '{amount, currency, refundable, excluded_reason, days_left: .breakdown[0].days_left}'

curl -s -X DELETE https://vdsok.guru/api/v1/servers/2001 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" -H "Idempotency-Key: $(uuidgen)" \
  | jq '{status, refunded: .refund.amount, balance_after, cancel_request_withdrawn}'

Домены: полный сценарий

Домены регистрируются на контактные данные из профиля аккаунта — те же, что использует кабинет; через API их не изменить. Статусы (status): pending — регистратор принял заявку, домен активируется на следующей синхронизации; active; expired; transfer_pending; cancelled. blocked: true — домен заблокирован сотрудником, мутации отвечают 403 domain_blocked. locked — трансфер-лок у регистратора.

Все денежные операции с доменами (POST /domains, /domains/{id}/renew, /domains/transfers) требуют Idempotency-Key и скоуп domains:order; чтения — domains:read; NS, privacy и автопродление — domains:manage.

1. Зоны и доступность

GET /catalog/zones — список TLD с ценами регистрации, продления и трансфера, допустимыми сроками (min_years/max_years) и флагами privacy_supported, transfer_supported.

GET /domains/availability?name=example.com спрашивает регистратора (дорогой вызов): available, reason (taken, premium, reserved, invalid, unsupported_tld), price_register, price_renew. IDN можно передавать и в Unicode, и в punycode.

curl -s https://vdsok.guru/api/v1/catalog/zones -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '.data[] | select(.tld == ".com")'
curl -s "https://vdsok.guru/api/v1/domains/availability?name=example.com" -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '{available, reason, price_register, currency}'

2. Регистрация

POST /domains с {"name", "years"?, "nameservers"?, "privacy"?, "auto_renew"?, "promo_code"?} списывает с баланса и регистрирует домен. По умолчанию years: 1, privacy: true, auto_renew: false, NS — серверы VDSok.

  • 201 со status: registered — регистратор подтвердил сразу.
  • 202 со status: pending — заявка принята, домен станет active на следующей синхронизации; придёт вебхук domain.registered.
  • 409 domain_taken — имя занято; 409 domain_exists — уже есть на аккаунте; 402 — не хватает средств, ничего не списано.
curl -s -X POST https://vdsok.guru/api/v1/domains -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name": "example.com", "years": 1, "privacy": true, "auto_renew": true}' \
  | jq '{status, id: .domain.id, expires_at: .domain.expires_at, charged, balance_after}'

3. NS, privacy, автопродление

PUT /domains/{domain_id}/nameservers с {"nameservers": ["ns1.example.net", "ns2.example.net"]} заменяет весь набор (2..4 уникальных имени). PATCH /domains/{domain_id} переключает auto_renew и privacy; privacy применяется у регистратора синхронно — если он отказал, ответ 400 upstream_rejected и ничего не сохраняется.

GET /domains — постраничный список с фильтром status; GET /domains/{domain_id} — карточка с expires_at, price_renew, nameservers, locked. За 30, 7 и 1 день до истечения приходит вебхук domain.expiring.

curl -s -X PUT https://vdsok.guru/api/v1/domains/77/nameservers -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{"nameservers": ["ns1.example.net", "ns2.example.net"]}' | jq .nameservers

curl -s -X PATCH https://vdsok.guru/api/v1/domains/77 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -d '{"privacy": false, "auto_renew": true}' | jq '{privacy, auto_renew}'

4. Продление и трансфер

POST /domains/{domain_id}/renew с {"years": 1..10} списывает price_renew × years и продлевает expires_at. Ответ status: renewed, либо pending_sync — регистратор принял продление, но новая дата ещё не видна; её подберёт ежедневная синхронизация, и придёт вебхук domain.renewed.

POST /domains/transfers с {"name", "auth_code", "privacy"?, "auto_renew"?} списывает цену трансфера (обычно включает год продления) и запускает перенос от другого регистратора; домен появляется со статусом transfer_pending. auth_code — EPP-код у текущего регистратора; перед трансфером снимите там transfer-lock.

curl -s -X POST https://vdsok.guru/api/v1/domains/77/renew -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" -d '{"years": 1}' \
  | jq '{status, charged, expires_at: .domain.expires_at}'

curl -s -X POST https://vdsok.guru/api/v1/domains/transfers -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name": "example.org", "auth_code": "AbC-123-xyz"}' | jq '{status, charged}'

Биллинг: баланс, пополнение, счета

Все услуги оплачиваются с баланса аккаунта в его валюте (currency). Пополнение идёт через платёжный шлюз по ссылке; API не принимает карты и не хранит платёжные данные. Заказ, продление и покупка IP списывают с баланса синхронно, поэтому перед ними стоит проверить GET /balance или GET /catalog/quote → balance_sufficient.

Баланс и транзакции

GET /balance (скоуп balance:read): balance, upcoming_7d и upcoming_30d — сумма продлений в ближайшие 7 и 30 дней, low_balance — баланс не покрывает ближайшие 7 дней, auto_renew_total_monthly. Тот же объект приходит в вебхуке balance.low.

GET /transactions — история движения средств, новые сначала, с фильтрами direction (credit/debit), since, until и курсорной пагинацией. type: topup, payment, refund, bonus, referral, adjustment, other; invoice_id связывает транзакцию со счётом.

GET /account (скоуп account:read) — профиль: логин, e-mail, валюта, группа клиента с её скидкой, скидка лояльности, флаги email_verified и two_factor_enabled.

curl -s https://vdsok.guru/api/v1/balance -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '{balance, currency, upcoming_7d, low_balance}'
curl -s "https://vdsok.guru/api/v1/transactions?direction=credit&limit=5" -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '.data[] | {created_at, type, amount, gateway, invoice_id}'

Пополнение: ссылка на оплату

GET /balance/topup-info — валюта, пределы суммы min/max, бонус первого пополнения (first_topup_bonus_percent, first_topup_eligible) и gateways — плоский список кодов включённых шлюзов (["cryptobot", …], а не объектов).

POST /balance/topup (скоуп balance:topup, Idempotency-Key, дорогой вызов) с {"amount": "25.00", "gateway": "<код из gateways>"} создаёт неоплаченный счёт типа topup и возвращает payment_url шлюза. Полей ровно два: любое другое (в том числе return_url) — 400 validation_error. Отправьте пользователя по ссылке. Баланс меняется только после подтверждения шлюзом — событие invoice.paid или GET /invoices/{invoice_id} → status: paid. В sandbox возвращается фейковый payment_url.

curl -s https://vdsok.guru/api/v1/balance/topup-info -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '{min, max, currency, gateways}'
curl -s -X POST https://vdsok.guru/api/v1/balance/topup -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"amount": "25.00", "gateway": "cryptobot"}' | jq '{invoice_id, payment_url, gateway}'

Счета: список, PDF, оплата

GET /invoices — счета, новые сначала, с фильтрами status (not_paid, paid, cancelled, refunded) и type (topup, vds_purchase, vds_renewal, ip_purchase, domain_registration, domain_renewal, domain_transfer, other). GET /invoices/{invoice_id} — карточка со строками items (в каждой — server_id или domain_id) и pdf_url. GET /invoices/{invoice_id}/pdf отдаёт application/pdf (дорогой вызов).

POST /invoices/{invoice_id}/pay (скоуп invoices:pay, Idempotency-Key) оплачивает неоплаченный счёт с баланса синхронно и отвечает 200 с {"status": "paid", "amount", "balance_after"}. Если счёт заказывал сервер, тот разворачивается фоном уже после ответа — следите за ним через GET /orders или вебхук server.created, как в сценарии серверов. При нехватке средств — 402, и ничего не меняется. POST /invoices/{invoice_id}/payment-link даёт ссылку на оплату счёта через шлюз (необязательное тело {"gateway"}; других полей ручка не принимает). Просроченные неоплаченные счета порождают вебхук invoice.overdue.

curl -s "https://vdsok.guru/api/v1/invoices?status=not_paid" -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '.data[] | {id, type, amount, due_at}'
curl -s https://vdsok.guru/api/v1/invoices/10231/pdf -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" -o invoice-10231.pdf
curl -s -X POST https://vdsok.guru/api/v1/invoices/10231/pay -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" -H "Idempotency-Key: $(uuidgen)" \
  | jq '{status, amount, balance_after}'
curl -s -X POST https://vdsok.guru/api/v1/invoices/10231/payment-link -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq .payment_url

Вебхуки

Вебхук — это POST от VDSok на ваш https:// URL при событии в аккаунте. Подписки создаются только боевым ключом со скоупом webhooks:manage; тестовому ключу все ручки /webhooks* отвечают 403 sandbox_not_supported. URL должен быть на публичном адресе, редиректы не выполняются.

Каталог событий

СобытиеКогдаdata.object
server.createdсервер создан (заказ или оплаченный счёт)Server
server.suspendedотключён за неуплату или сотрудником; data.previous.status — прежний статусServer
server.unsuspendedвозобновлён после оплаты или сотрудникомServer
server.terminatedудалён (клиентом с возвратом, по заявке или за долгую неуплату)Server
server.reinstalledпереустановка ОС завершенаServer
invoice.createdновый счёт (продление, заказ, пополнение)Invoice
invoice.paidсчёт оплачен (шлюз или баланс)Invoice
invoice.overdueсчёт просрочен и не оплаченInvoice
balance.lowбаланс упал ниже порога ближайших списанийBalance
domain.registeredдомен стал активным у регистратораDomain
domain.expiringдомен скоро истекает (за 30, 7 и 1 день)Domain
domain.renewedдомен продлён (вручную или автопродлением)Domain
key.createdв кабинете создан новый API-ключApiKey
key.revokedAPI-ключ отозван (клиентом, самим ключом, сотрудником или баном)ApiKey
pingтестовое событие от POST /webhooks/{id}/test{subscription_id, message: "pong"}

Актуальный список с описаниями отдаёт GET /webhooks/events.

Конверт

{
  "id": "evt_01J7ZK3Q9X4R",
  "type": "server.suspended",
  "created_at": "2026-09-15T10:00:00Z",
  "livemode": true,
  "account_id": 57,
  "api_version": "1",
  "data": {
    "object": {"id": 2001, "name": "web-01", "status": "suspended", "...": "full Server object"},
    "previous": {"status": "active"}
  },
  "resource": "/api/v1/servers/2001"
}

data.object — полный снимок объекта в том же виде, что отдаёт REST; data.previous — изменившиеся поля или null; resource — путь объекта в API. Конверт сериализуется один раз при постановке в очередь: повторы и переотправки шлют те же байты, поэтому id и created_at не меняются. А вот X-Webhook-Timestamp — это время конкретной попытки, поэтому подпись на каждой попытке считается заново: дедуплицируйте по X-Webhook-Id, а не по подписи.

Заголовки доставки

ЗаголовокЗначение
X-Webhook-Signaturev1=<hex HMAC-SHA256(secret, "{timestamp}.{body}")>
X-Webhook-Timestampunix-секунды отправки этой попытки
X-Webhook-Idevt_… — id события, одинаковый у всех попыток; дедуплицируйте по нему
X-Webhook-Eventтип события, например server.created
User-AgentVDSok-Webhooks/1.0

Доставка и повторы

Ответьте любым 2xx в течение 10 секунд — тело ответа игнорируется. Обрабатывайте событие асинхронно: примите, поставьте в свою очередь, ответьте 200. Иначе — повторы через 60 с, 5 мин, 15 мин, 1 ч и 6 ч; после 6-й неудачи доставка помечается dead. 20 неудач подряд автоматически выключают подписку (причина попадает в disabled_reason текстом) и шлют письмо; включить обратно — PATCH /webhooks/{id} с {"active": true}. Журнал: доставленные записи хранятся 14 дней, мёртвые — 30.

Порядок доставки не гарантируется, а одно событие может прийти дважды (повтор после таймаута, переотправка). Идемпотентный обработчик с дедупликацией по X-Webhook-Id — обязателен.

Подписка

POST /webhooks с {"url", "events": [...], "description"?} создаёт подписку и возвращает secret (whsec_…) один раз — сохраните его в секретах приложения. Потерянный секрет не восстанавливается, только заменяется: POST /webhooks/{id}/rotate-secret выдаёт новый, старый перестаёт работать сразу, а уже поставленные в очередь доставки будут подписаны новым в момент отправки.

curl -s https://vdsok.guru/api/v1/webhooks/events -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '.data[] | .type'

curl -s -X POST https://vdsok.guru/api/v1/webhooks -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.example.com/vdsok",
       "events": ["server.created", "server.suspended", "server.terminated", "invoice.paid", "balance.low"],
       "description": "billing sync"}' \
  | jq '{id, secret, events}'     # secret is shown once

Проверка подписи

Алгоритм: возьмите сырые байты тела запроса (не пересериализованный JSON — любое изменение пробелов ломает подпись), составьте строку "{X-Webhook-Timestamp}.{body}", посчитайте HMAC-SHA256 с секретом подписки, сравните hex-результат со значением после v1= константным по времени сравнением и отклоните доставку, если |now − timestamp| > 300 секунд (защита от повтора). Только после этого разбирайте JSON. SDK делают то же самое в Webhooks.verify() / construct_event().

Отвечайте 2xx быстро и обрабатывайте событие в фоне; ошибку проверки подписи логируйте и отвечайте 400 — такая доставка будет повторена, и по журналу вы увидите проблему.

import hmac, hashlib, json, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = b"whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"   # from POST /webhooks, keep it secret
seen = set()   # use a persistent store (Redis, DB) in production

def verify(secret: bytes, timestamp: str, body: bytes, signature: str, tolerance: int = 300) -> bool:
    try:
        ts = int(timestamp)
    except (TypeError, ValueError):
        return False
    if abs(time.time() - ts) > tolerance:            # replay protection
        return False
    expected = hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    given = signature.removeprefix("v1=")
    return hmac.compare_digest(expected, given)       # constant time

@app.post("/vdsok")
def vdsok_webhook():
    raw = request.get_data()                          # raw bytes, before any JSON parsing
    if not verify(SECRET, request.headers.get("X-Webhook-Timestamp", ""), raw,
                  request.headers.get("X-Webhook-Signature", "")):
        abort(400)
    event_id = request.headers["X-Webhook-Id"]
    if event_id in seen:                              # duplicate retry/redelivery
        return "", 200
    seen.add(event_id)
    event = json.loads(raw)
    if event["type"] == "server.suspended":
        print("server", event["data"]["object"]["id"], "was", event["data"]["previous"]["status"])
    # enqueue heavy work here; answer within 10 s
    return "", 200

Тест, журнал, повтор

POST /webhooks/{id}/test шлёт событие ping синхронно, минуя очередь, и возвращает статус и задержку вашего приёмника (ok, status, latency_ms, detail) — удобно при настройке. GET /webhooks/{id}/deliveries — журнал с фильтрами status (pending, delivered, dead) и event_type; каждая запись несёт payload (тот самый конверт), attempts, last_status, last_error и первые 1000 байт ответа. POST /webhooks/deliveries/{delivery_id}/redeliver ставит доставку в очередь заново с теми же байтами. Тест и повтор — дорогие вызовы.

curl -s -X POST https://vdsok.guru/api/v1/webhooks/5/test -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq '{ok, status, latency_ms, detail}'
curl -s "https://vdsok.guru/api/v1/webhooks/5/deliveries?status=dead" -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  | jq '.data[] | {id, event_type, attempts, last_status, last_error}'
curl -s -X POST https://vdsok.guru/api/v1/webhooks/deliveries/9012/redeliver -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" | jq .status
curl -s -X PATCH https://vdsok.guru/api/v1/webhooks/5 -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" -H "Content-Type: application/json" -d '{"active": true}'

SDK

Официальные SDK покрывают весь v1 и следуют той же спецификации, что и этот сайт (в Node-пакете типы генерируются из неё напрямую). Общее для всех трёх:

  • конструктор (apiKey, {baseUrl, timeout = 30 s, maxRetries = 2}); заголовки Authorization, Accept, User-Agent: vdsok-sdk-<lang>/<ver>;
  • автоматические повторы на 429/502/503/504 с учётом Retry-After — только для GET и мутаций с Idempotency-Key; на денежных ручках SDK сам генерирует ключ идемпотентности и отдаёт его вызывающему;
  • единая ошибка ApiError {status, code, message, requestId, details} и RateLimitInfo из заголовков. В PHP машинный код лежит в errorCode, а не в code: у \Exception уже есть унаследованное $code (HTTP-статус), переобъявить его нельзя;
  • итераторы пагинации (for server in client.servers.list() обходит все страницы);
  • Webhooks.verify(secret, headers, rawBody, tolerance = 300) и construct_event() — проверка подписи из раздела вебхуков;
  • группы account, balance, invoices, catalog, servers, domains, ssh_keys, keys, webhooks; деньги — строками/Decimal, даты — в нативные типы.
ЯзыкПакетУстановкаТребования
Node / TypeScript@vdsok/sdknpm install @vdsok/sdkNode 18+, без runtime-зависимостей (глобальный fetch), ESM и CJS
Pythonvdsokpip install vdsokPython 3.9+, httpx; классы Vdsok и AsyncVdsok
PHPvdsok/sdkcomposer require vdsok/sdk guzzlehttp/guzzlePHP 8.1+, PSR-18/PSR-17 (Guzzle — рекомендуемый провайдер)

Версии SDK 1.x соответствуют API v1; ломающие изменения API выйдут только вместе с /v2 и новой мажорной версией SDK. Исходники и issue-трекер — на GitHub в организации vdsok (sdk-node, sdk-python, sdk-php).

# pip install vdsok
from vdsok import Vdsok, ApiError

client = Vdsok("vk_live_XXXXXXXXXXXXXXXXXXXXXXXX")   # or Vdsok(api_key, base_url=..., timeout=30, max_retries=2)

for server in client.servers.list_all(status="active"):   # walks every page
    print(server.id, server.name, server.billing.next_due_at)

try:
    result = client.servers.renew(2001, months=3)       # Idempotency-Key generated by the SDK
    print("charged", result.charged, "next due", result.next_due_at)
except ApiError as e:
    if e.code == "insufficient_funds":
        print("short by", e.details["shortfall"])
    else:
        raise

Changelog

Изменения API документируются здесь и в поле info.version спецификации. В v1 всё добавляется только совместимо: новые поля, новые эндпоинты, новые коды ошибок и события вебхуков. Клиент обязан игнорировать неизвестные поля и обрабатывать неизвестные коды по HTTP-статусу.

1.0.0 — 2026-09-15

  • Первый публичный выпуск Client API v1: /me, аккаунт, баланс и транзакции, пополнение, счета (список, PDF, оплата с баланса, платёжная ссылка), каталог (тарифы, ОС, локации, зоны, котировка), серверы (заказ, статус, питание, переустановка, сброс пароля, продление, удаление с возвратом, заметки и автопродление), заказы, дополнительные IP и PTR, SSH-ключи, домены (доступность, регистрация, продление, NS, privacy, трансфер), API-ключи (просмотр и kill switch), вебхуки (подписки, журнал, повтор, тест).
  • Ключи vk_live_/vk_test_, скоупы и пресеты, IP-списки, окна действия.
  • Идемпотентность на денежных операциях, курсорная пагинация, лимиты 120/20 в минуту.
  • Sandbox: тестовые ключи читают реальные данные и имитируют записи.
  • 14 типов событий вебхуков с HMAC-SHA256 подписью и повторами.
  • SDK: @vdsok/sdk (Node), vdsok (Python), vdsok/sdk (PHP).

Спецификация OpenAPI

Машиночитаемое описание API — документ OpenAPI 3.1, тот же, из которого собраны этот сайт и SDK. Он доступен без ключа:

  • Скачать openapi.json — кэшируемая копия; живой документ отдаёт GET /api/v1/openapi.json.
  • Интерактивный справочник — все операции, схемы, примеры запросов и ответов, с возможностью выполнить запрос своим ключом прямо из браузера.

В документе используются расширения: x-scopes (требуемые скоупы), x-idempotent (нужен Idempotency-Key), x-expensive (считается в корзину 20/мин), x-sandbox (real | fake | forbidden — поведение под тестовым ключом). Из спецификации можно сгенерировать клиент под любой язык — openapi-typescript, openapi-generator, oapi-codegen — или подключить её в Postman/Insomnia.

curl -s https://vdsok.guru/api/v1/openapi.json -o vdsok-openapi.json
jq '.info.version, (.paths | keys | length)' vdsok-openapi.json