Швидкий старт
П'ять хвилин від нуля до першої відповіді 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: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, domains:manage, invoices:pay |
full | усі скоупи, включно з balance:topup, servers:order, 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 |
# 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-адреси: за адресою повертається ім'я. Потрібен поштовим серверам. |
punycode | ASCII-запис домену з нелатинськими літерами (пример.рф → 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-статусом — так додавання коду не ламає інтеграції.
| Статус | Коди |
|---|---|
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 |
405 / 413 / 415 | method_not_allowed, 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 секунд. Стелю й залишок повертають заголовки 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-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, тому повтор ніколи не спише двічі. Замовлення сервера з таймаутом панелі (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 .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..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.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 ставить доставку в чергу заново з тими самими байтами. Тест і повтор — дорогі виклики. Одну доставку цілком віддає 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/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 config repositories.vdsok vcs https://github.com/VDSok/sdk-php → composer require vdsok/sdk:^1.0 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).
Усі ендпоінти
Повний список операцій 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 /account | account:read | — | — | real |
GET /balance | balance:read | — | — | real |
GET /transactions | balance:read | — | — | real |
GET /balance/topup-info | balance:read | — | — | real |
POST /balance/topup | balance:topup | так | так | fake |
Invoices
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
GET /invoices | invoices:read | — | — | real |
GET /invoices/{invoice_id} | invoices:read | — | — | real |
GET /invoices/{invoice_id}/pdf | invoices:read | — | так | real |
POST /invoices/{invoice_id}/pay | invoices:pay | так | — | fake |
POST /invoices/{invoice_id}/payment-link | invoices: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 /servers | servers:read | — | — | real |
POST /servers | servers: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}/status | servers:read | — | так | real |
GET /servers/{server_id}/refund-quote | servers:read | — | — | real |
POST /servers/{server_id}/renew | servers:order | так | так | fake |
Server actions
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
POST /servers/{server_id}/actions/power | servers:manage | — | — | fake |
POST /servers/{server_id}/actions/reinstall | servers:manage | — | — | fake |
POST /servers/{server_id}/actions/reset-password | servers:manage | — | — | fake |
Orders
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
GET /orders | servers:read | — | — | real |
GET /orders/{invoice_id} | servers:read | — | — | real |
IPs
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
GET /servers/{server_id}/ips | servers:read | — | так | real |
POST /servers/{server_id}/ips | servers:order | так | так | fake |
GET /servers/{server_id}/ips/quote | servers:read | — | так | real |
DELETE /servers/{server_id}/ips/{ip_id} | servers:order | — | — | fake |
PUT /servers/{server_id}/ips/{ip_id}/ptr | servers:manage | — | — | fake |
SSH keys
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
GET /ssh-keys | servers:read | — | — | real |
POST /ssh-keys | servers:manage | — | — | fake |
DELETE /ssh-keys/{key_id} | servers:manage | — | — | fake |
Domains
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
GET /domains/availability | domains:read | — | так | real |
GET /domains | domains:read | — | — | real |
POST /domains | domains:order | так | — | fake |
GET /domains/{domain_id} | domains:read | — | — | real |
PATCH /domains/{domain_id} | domains:manage | — | — | fake |
POST /domains/{domain_id}/renew | domains:order | так | — | fake |
PUT /domains/{domain_id}/nameservers | domains:manage | — | — | fake |
POST /domains/transfers | domains:order | так | — | fake |
Keys
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
GET /keys | keys:read | — | — | real |
GET /keys/{key_id} | keys:read | — | — | real |
DELETE /keys/{key_id} | — | — | — | real |
Webhooks
| Операція | Скоуп | Idem | Дорога | Sandbox |
|---|---|---|---|---|
GET /webhooks | webhooks:manage | — | — | forbidden |
POST /webhooks | webhooks:manage | — | — | forbidden |
GET /webhooks/events | webhooks: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-secret | webhooks:manage | — | — | forbidden |
POST /webhooks/{webhook_id}/test | webhooks:manage | — | так | forbidden |
GET /webhooks/{webhook_id}/deliveries | webhooks:manage | — | — | forbidden |
GET /webhooks/deliveries/{delivery_id} | webhooks:manage | — | — | forbidden |
POST /webhooks/deliveries/{delivery_id}/redeliver | webhooks: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).