logo
logo

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

    УСЛУГИ
  • Виртуальные серверы
  • Выделенные серверы
  • Домены
  • LIR-услуги
    ПОДДЕРЖКА
  • Написать в 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
Аутентификация, скоупы и пресеты
Sandbox: тестовые ключи
Словарь терминов
Соглашения
Ошибки: конверт и коды
Лимиты запросов и заголовки
Пагинация
Идемпотентность
Деньги и даты
Сценарии
Серверы: полный сценарий1. Каталог и котировка2. Заказ3. Статус4. Питание5. Переустановка и сброс пароля6. Дополнительные IP и PTR7. Продление и автопродление8. Удаление с возвратом
Домены: полный сценарий1. Зоны и доступность2. Регистрация3. NS, privacy, автопродление4. Продление и трансфер
Биллинг: баланс, пополнение, счетаБаланс и транзакцииПополнение: ссылка на оплатуСчета: список, PDF, оплата
ВебхукиПодпискаПроверка подписиТест, журнал, повтор
Инструменты
SDK
Changelog
Все эндпоинты
Спецификация OpenAPI
Поддержка и обратная связь

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

Пять минут от нуля до первого ответа API. Нужны только аккаунт VDSok и терминал.

1. Получите ключ

Ключи выпускает владелец аккаунта в кабинете: Настройки → API (/my/api). При создании выбираются режим (live или test), набор скоупов и, если нужно, список разрешённых IP. Секрет показывается один раз — второй раз его не покажет никто, ключ придётся выпустить заново.

Начните с тестового ключа (vk_test_…): он ходит на тот же адрес и читает ваши настоящие данные, но ни одной записи не выполняет по-настоящему — подробности в разделе Sandbox.

2. Первый запрос

GET /me — самый дешёвый способ убедиться, что ключ жив. Примеры на четырёх языках — прямо под этим разделом.

В ответе смотрите на три вещи: key.mode (live или test), scopes — что именно разрешено этому ключу, и заголовок X-Request-ID, который стоит называть в любом обращении в поддержку. Остаток лимита приходит в X-RateLimit-Remaining.

3. Первая запись — бесплатно

Тестовым ключом можно пройти весь путь заказа, ничего не потратив: баланс не двигается, VM не создаётся, платёжные шлюзы не вызываются. Каждый такой ответ несёт заголовок X-Sandbox: true, а созданный сервер получает фейковый id от 9000000000 и живёт 24 часа.

BASE=https://vdsok.guru/api/v1
KEY=vk_test_XXXXXXXXXXXXXXXXXXXXXXXX

# заказ: вернёт invoice_id заказа и server_id будущей машины
curl -sS -X POST $BASE/servers \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"tariff_id": 12, "os": "ubuntu-22.04", "months": 1}'

# как идёт заказ — по invoice_id из ответа выше, не по server_id
curl -sS $BASE/orders/4242 -H "Authorization: Bearer $KEY"

# уборка: удаление тоже имитируется
curl -sS -X DELETE $BASE/servers/9000000001 \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)"

Когда то же самое заработает боевым ключом, единственная разница — префикс ключа и отсутствие заголовка X-Sandbox.

Куда дальше

  • Аутентификация, скоупы и пресеты — что именно разрешать ключу.
  • Ошибки: конверт и коды — с их разбора начинается любой рабочий клиент.
  • Серверы: полный сценарий — заказ, статус, питание, продление, удаление с возвратом.
  • SDK — если не хочется писать HTTP руками.
  • Словарь терминов — если незнакомо слово «котировка» или неясно, чем заказ отличается от счёта.
curl -sS https://vdsok.guru/api/v1/me -H "Authorization: Bearer vk_live_XXXXXXXXXXXXXXXXXXXXXXXX"

Обзор и базовый 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; указывайте его в обращениях в поддержку.

Если вы здесь впервые — начните с быстрого старта: ключ, первый запрос и бесплатный круг заказа в песочнице.

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

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

Ключи создаёт владелец аккаунта в кабинете (/my/api): имя, режим live/test, набор скоупов, необязательный список разрешённых IP/CIDR и окно действия (not_before/expires_at). Секрет показывается один раз. Ключ не зависит от сессий кабинета и действует, пока не истечёт или не будет отозван. Через 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, domains:manage, invoices:pay
fullвсе скоупы, включая balance:topup, servers:order, 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
# 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 и дальше в этом руководстве.

ТерминЧто значит
tariffКонфигурация с ценой: ядра, память, диск, трафик, локация. Лежит в каталоге, адресуется tariff_id. Купить можно только то, что есть в GET /catalog/tariffs.
serverОплаченная виртуальная машина — экземпляр тарифа. Адресуется server_id, тем же числом, что кабинет показывает в адресе страницы.
orderПроцесс покупки: счёт выставлен, оплачен, машина создаётся. Адресуется invoice_id, а не server_id — GET /orders/{invoice_id}. Это первое, обо что спотыкаются.
invoiceСчёт: на заказ, на продление или на пополнение баланса. У счёта есть статус, PDF и возможность оплаты с баланса.
quoteПредварительный расчёт без списания: GET /catalog/quote — сколько будет стоить заказ, GET /servers/{server_id}/refund-quote — сколько вернётся при удалении.
panelПанель виртуализации, где на самом деле живёт машина. Оттуда приходит живой статус и туда уходят питание и переустановка.
scopeПраво, выданное ключу: servers:read, balance:topup и так далее. Нехватка — 403 insufficient_scope.
presetГотовый набор скоупов в кабинете: read_only, operate, full. Ярлык, а не отдельная сущность.
live key / test keyКлюч с префиксом vk_live_ работает по-настоящему; vk_test_ читает настоящие данные, но записи имитирует.
sandboxРежим, в котором работает тестовый ключ. Ответ помечен заголовком X-Sandbox: true.
cursorНепрозрачная строка-закладка для следующей страницы списка. Не разбирайте её — передавайте как есть.
Idempotency-KeyВаш идентификатор попытки для денежных операций. Повтор с тем же ключом вернёт прежний результат вместо второго списания.
deliveryОдна попытка доставки вебхука на ваш адрес. Повторная отправка той же попытки — redeliver.
PTRОбратная DNS-запись для IP-адреса: по адресу возвращается имя. Нужна почтовым серверам.
punycodeASCII-запись домена с не-латинскими буквами (пример.рф → xn--e1afmkfd.xn--p1ai). API принимает обе формы, возвращает punycode.
expensiveПометка операции, которая дёргает внешнюю систему. Для таких — отдельная, более узкая корзина лимита.
↑ Наверх

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

У 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
405 / 413 / 415method_not_allowed, payload_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 секунд. Потолок и остаток приходят в заголовках X-RateLimit-Limit и X-RateLimit-Remaining каждого ответа, лимиты конкретного ключа — в GET /keys/{key_id}. Если штатного потолка не хватает, напишите в поддержку.

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

  • GET /servers/{server_id}/status и GET /servers/{server_id}?include=live — живой опрос панели;
  • GET /domains/availability — запрос к регистратору;
  • GET /catalog/quote;
  • GET /servers/{server_id}/ips, GET /servers/{server_id}/ips/quote;
  • POST /servers, POST /servers/{server_id}/renew, 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, поэтому повтор никогда не спишет дважды. Заказ сервера с таймаутом панели (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 не используется никогда.

  • Разбирайте суммы в десятичный тип: 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..2160, если у тарифа hourly_available). Один тариф целиком отдаёт GET /catalog/tariffs/{tariff_id}, а список локаций с кодами и странами — GET /catalog/locations.

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. Список ключей аккаунта — GET /ssh-keys, удаление — DELETE /ssh-keys/{key_id}: серверы, где ключ уже установлен, оно не трогает.

# 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/мин. У части серверов живых данных нет — такой вызов отвечает 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..2160}. Обязателен 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, currency, balance_after и refund_quote — тем же расчётом, что и котировка. Ожидающая заявка на отмену отзывается молча, в ответе этого нет. Уже удалённый сервер (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, balance_after}'

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

Домены регистрируются на контактные данные из профиля аккаунта — те же, что использует кабинет; через 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"]} заменяет весь набор — ровно два имени. 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 ставит доставку в очередь заново с теми же байтами. Тест и повтор — дорогие вызовы. Одну доставку целиком отдаёт GET /webhooks/deliveries/{delivery_id}, а DELETE /webhooks/{webhook_id} удаляет саму подписку. Свои подписки видно в GET /webhooks, одну — в GET /webhooks/{webhook_id}.

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 и следуют той же спецификации, что и этот сайт. Общее для всех трёх:

  • конструктор (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-статус), переобъявить его нельзя;
  • пагинация: все страницы обходят client.servers.list_all() в Python, for await (… of await client.servers.list()) в Node и $client->servers->iterate() в PHP; 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 config repositories.vdsok vcs https://github.com/VDSok/sdk-php → composer require vdsok/sdk:^1.0 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).

Все эндпоинты

Полный список операций v1 — то же, что в интерактивном справочнике, но одной таблицей и без JavaScript.

Колонки: Скоуп — что должно быть у ключа (прочерк означает «подойдёт любой валидный ключ»); Idem — обязателен ли Idempotency-Key; Дорогая — считается ли вызов в отдельной корзине лимита; Sandbox — поведение под тестовым ключом (real — как в live, fake — имитация без записи, forbidden — 403).

Meta

ОперацияСкоупIdemДорогаяSandbox
GET /health———real
GET /openapi.json———real
GET /me———real

Account

ОперацияСкоупIdemДорогаяSandbox
GET /accountaccount:read——real
GET /balancebalance:read——real
GET /transactionsbalance:read——real
GET /balance/topup-infobalance:read——real
POST /balance/topupbalance:topupдадаfake

Invoices

ОперацияСкоупIdemДорогаяSandbox
GET /invoicesinvoices:read——real
GET /invoices/{invoice_id}invoices:read——real
GET /invoices/{invoice_id}/pdfinvoices:read—даreal
POST /invoices/{invoice_id}/payinvoices:payда—fake
POST /invoices/{invoice_id}/payment-linkinvoices:pay——fake

Catalog

ОперацияСкоупIdemДорогаяSandbox
GET /catalog/tariffs———real
GET /catalog/tariffs/{tariff_id}———real
GET /catalog/os———real
GET /catalog/locations———real
GET /catalog/zones———real
GET /catalog/quote——даreal

Servers

ОперацияСкоупIdemДорогаяSandbox
GET /serversservers:read——real
POST /serversservers:orderдадаfake
GET /servers/{server_id}servers:read——real
DELETE /servers/{server_id}servers:deleteда—fake
PATCH /servers/{server_id}servers:manage——fake
GET /servers/{server_id}/statusservers:read—даreal
GET /servers/{server_id}/refund-quoteservers:read——real
POST /servers/{server_id}/renewservers:orderдадаfake

Server actions

ОперацияСкоупIdemДорогаяSandbox
POST /servers/{server_id}/actions/powerservers:manage——fake
POST /servers/{server_id}/actions/reinstallservers:manage——fake
POST /servers/{server_id}/actions/reset-passwordservers:manage——fake

Orders

ОперацияСкоупIdemДорогаяSandbox
GET /ordersservers:read——real
GET /orders/{invoice_id}servers:read——real

IPs

ОперацияСкоупIdemДорогаяSandbox
GET /servers/{server_id}/ipsservers:read—даreal
POST /servers/{server_id}/ipsservers:orderдадаfake
GET /servers/{server_id}/ips/quoteservers:read—даreal
DELETE /servers/{server_id}/ips/{ip_id}servers:order——fake
PUT /servers/{server_id}/ips/{ip_id}/ptrservers:manage——fake

SSH keys

ОперацияСкоупIdemДорогаяSandbox
GET /ssh-keysservers:read——real
POST /ssh-keysservers:manage——fake
DELETE /ssh-keys/{key_id}servers:manage——fake

Domains

ОперацияСкоупIdemДорогаяSandbox
GET /domains/availabilitydomains:read—даreal
GET /domainsdomains:read——real
POST /domainsdomains:orderда—fake
GET /domains/{domain_id}domains:read——real
PATCH /domains/{domain_id}domains:manage——fake
POST /domains/{domain_id}/renewdomains:orderда—fake
PUT /domains/{domain_id}/nameserversdomains:manage——fake
POST /domains/transfersdomains:orderда—fake

Keys

ОперацияСкоупIdemДорогаяSandbox
GET /keyskeys:read——real
GET /keys/{key_id}keys:read——real
DELETE /keys/{key_id}———real

Webhooks

ОперацияСкоупIdemДорогаяSandbox
GET /webhookswebhooks:manage——forbidden
POST /webhookswebhooks:manage——forbidden
GET /webhooks/eventswebhooks:manage——forbidden
GET /webhooks/{webhook_id}webhooks:manage——forbidden
DELETE /webhooks/{webhook_id}webhooks:manage——forbidden
PATCH /webhooks/{webhook_id}webhooks:manage——forbidden
POST /webhooks/{webhook_id}/rotate-secretwebhooks:manage——forbidden
POST /webhooks/{webhook_id}/testwebhooks:manage—даforbidden
GET /webhooks/{webhook_id}/deliverieswebhooks:manage——forbidden
GET /webhooks/deliveries/{delivery_id}webhooks:manage——forbidden
POST /webhooks/deliveries/{delivery_id}/redeliverwebhooks:manage—даforbidden

Спецификация 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

Поддержка и обратная связь

Что приложить к обращению

Каждый ответ API несёт заголовок X-Request-ID. По нему мы находим ваш запрос в журналах за секунды. Поэтому в обращении:

  • request_id из заголовка X-Request-ID (он же в теле ошибки);
  • метод и путь: POST /servers/2001/renew, а не «продление не работает»;
  • время в UTC — хотя бы с точностью до минуты;
  • идентификатор ключа из GET /keys и его режим (live или test). Сам ключ присылать не нужно никогда — если он попал в переписку, отзовите его через DELETE /keys/{key_id} и выпустите новый.

Куда писать

  • Тикет в кабинете — основной путь: видно аккаунт, услуги и историю.
  • Почта: support@vdsok.guru.
  • Контакты и время работы: страница контактов.

Доступность API

Публичной страницы статуса нет. Проба доступности — GET /health: она отвечает 200 без ключа и говорит, включён ли API (status: ok | disabled). Это именно проба, а не защищённая ручка: по ней удобно понять, что молчит — API или ваша сторона.

Ошибка в SDK или в документации

  • SDK живут на GitHub: sdk-node, sdk-python, sdk-php — issue заводите там же.
  • Неточность на этой странице — тикетом или письмом, с якорем раздела (например #idempotency).
↑ Наверх