logo
logo

Високопродуктивні віртуальні сервери в Росії та Європі.

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

API для розробників

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

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

Огляд і базовий URL

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

Базовий URL

Усі шляхи в цьому посібнику відносні:

https://vdsok.guru/api/v1

Версія живе в шляху. Ламкі зміни підуть у /v2; v1 лише зростає: нові поля й ендпоінти можуть з'явитися будь-коли, тому невідомі поля у відповідях ігноруйте.

Домовленості

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

Швидкий старт

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

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

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

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

Скоупи

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

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

Пресети

Пресети в кабінеті — просто набори скоупів, їх можна змінити чекбоксами після вибору.

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

Kill switch

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

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

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

Sandbox: тестові ключі

Тестовий ключ (vk_test_…) створюється в тому самому кабінеті, що й бойовий, і ходить на той самий базовий URL — окремого стенда немає.

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

Що робить тестовий ключ:

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

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

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

Якщо тестові ключі вимкнено на боці VDSok, будь-який запит відповідає 403 sandbox_disabled.

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

Помилки: конверт і коди

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

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

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

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

Що повторювати: 429, 502, 503, 504 — безпечно для GET і для мутацій з Idempotency-Key. Інші мутації без ключа ідемпотентності не повторюйте наосліп.

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

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

Ліміти запитів і заголовки

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

Дорогі виклики позначено x-expensive: true у специфікації:

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

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

Заголовки

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

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

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

Пагінація

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

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

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

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

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

Ідемпотентність

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

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

СитуаціяВідповідь
той самий ключ, той самий запитзбережена відповідь відтворюється; секрети на кшталт root_password у повторі затерті
той самий ключ, інше тіло або інший ендпоінт409 idempotency_conflict
той самий ключ, поки перший запит ще виконується409 idempotency_in_progress з Retry-After
ключа немає400 idempotency_key_required

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

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

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

Гроші та дати

Гроші

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

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

Дати

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

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

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

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

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

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

1. Каталог і котирування

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

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

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

2. Замовлення

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

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

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

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

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

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

3. Статус

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

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

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

4. Живлення

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

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

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

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

POST /servers/{server_id}/actions/reset-password генерує новий root-пароль і показує його один раз. Обмеження — 5 викликів за 5 хвилин на сервер.

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

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

6. Додаткові IP та PTR

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

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

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

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

7. Продовження та автопродовження

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

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

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

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

8. Видалення з поверненням

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

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

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

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

Домени: повний сценарій

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

Усі грошові операції з доменами (POST /domains, /domains/{id}/renew, /domains/transfers) вимагають Idempotency-Key і скоуп domains:order; читання — domains:read; NS, privacy та автопродовження — domains:manage.

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

GET /catalog/zones — список TLD з цінами реєстрації, продовження та трансферу, допустимими термінами (min_years/max_years) і прапорцями privacy_supported, transfer_supported.

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

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

2. Реєстрація

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

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

3. NS, privacy, автопродовження

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

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

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

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

4. Продовження та трансфер

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

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

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

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

Білінг: баланс, поповнення, рахунки

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

Баланс і транзакції

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

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

GET /account (скоуп account:read) — профіль: логін, e-mail, валюта, група клієнта з її знижкою, знижка лояльності, прапорці email_verified і two_factor_enabled.

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

Поповнення: посилання на оплату

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

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

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

Рахунки: список, PDF, оплата

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

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

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

Вебхуки

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

Каталог подій

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

Актуальний список з описами віддає GET /webhooks/events.

Конверт

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

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

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

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

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

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

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

Підписка

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

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

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

Перевірка підпису

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

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

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

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

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

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

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

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

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

SDK

Офіційні SDK покривають увесь v1 і дотримуються тієї самої специфікації, що й цей сайт (у Node-пакеті типи генеруються з неї напряму). Спільне для всіх трьох:

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

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

# pip install vdsok
from vdsok import Vdsok, ApiError

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

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

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

Changelog

Зміни API документуються тут і в полі info.version специфікації. У v1 усе додається лише сумісно: нові поля, нові ендпоінти, нові коди помилок і події вебхуків. Клієнт зобов'язаний ігнорувати невідомі поля й обробляти невідомі коди за HTTP-статусом.

1.0.0 — 2026-09-15

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

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

Машиночитаний опис API — документ OpenAPI 3.1, той самий, з якого зібрано цей сайт і SDK. Він доступний без ключа:

  • Завантажити openapi.json — кешована копія; живий документ віддає GET /api/v1/openapi.json.
  • Інтерактивний довідник — усі операції, схеми, приклади запитів і відповідей, з можливістю виконати запит своїм ключем просто з браузера.

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

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