Обзор и базовый 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:read | GET /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 |
operate | read_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, с сервера), не выдавая каждому ключу право уничтожить всю интеграцию. Остальные ключи отзываются в кабинете. Работает и для тестовых ключей.
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 | имитация записи: заказ, продление, питание, домены, пополнение |
forbidden | 403 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-статусу — так добавление кода не ломает интеграции.
| Статус | Коды |
|---|---|
400 | invalid_request, validation_error, invalid_cursor, idempotency_key_required, os_not_allowed, invalid_period, invalid_action, upstream_rejected |
401 | invalid_token, key_expired, key_not_yet_valid |
402 | insufficient_funds — ничего не списано и не создано |
403 | insufficient_scope, ip_not_allowed, account_suspended, api_disabled_for_account, sandbox_not_supported, sandbox_disabled, server_blocked, domain_blocked |
404 | not_found — объекта нет на этом аккаунте (чужие объекты тоже 404, не 403) |
409 | conflict, idempotency_conflict, idempotency_in_progress, operation_in_progress, service_state, no_capacity, tariff_unavailable, ip_limit_reached, domain_taken, domain_exists, cancel_pending |
413 / 415 | payload_too_large, unsupported_media_type |
429 | rate_limited |
500 | server_error — сбой у нас, request id уже в логах |
502 | upstream_error — панель, регистратор или шлюз ответили ошибкой; для денежных операций ничего не списано, если ответ не говорит иного |
503 | api_disabled, upstream_unavailable, temporarily_unavailable — повторите после Retry-After |
504 | upstream_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-Reset | unix-время (секунды) сброса окна |
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 .password6. Дополнительные 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.revoked | API-ключ отозван (клиентом, самим ключом, сотрудником или баном) | 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-Signature | v1=<hex HMAC-SHA256(secret, "{timestamp}.{body}")> |
X-Webhook-Timestamp | unix-секунды отправки этой попытки |
X-Webhook-Id | evt_… — id события, одинаковый у всех попыток; дедуплицируйте по нему |
X-Webhook-Event | тип события, например server.created |
User-Agent | VDSok-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/sdk | npm install @vdsok/sdk | Node 18+, без runtime-зависимостей (глобальный fetch), ESM и CJS |
| Python | vdsok | pip install vdsok | Python 3.9+, httpx; классы Vdsok и AsyncVdsok |
| PHP | vdsok/sdk | composer require vdsok/sdk guzzlehttp/guzzle | PHP 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:
raiseChangelog
Изменения 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