vdsok — Python SDK for the VDSok Client API
Official Python client for the VDSok Client API v1: balance and invoices, the VDS catalog, servers (order, renew, power, reinstall, IPs, PTR, SSH keys, delete with refund), domains, API keys and webhooks.
- Sync
Vdsokand asyncAsyncVdsokon top of httpx - Typed dataclass models mirroring the OpenAPI schemas,
Decimalmoney, tz-awaredatetime - Automatic
Idempotency-Keyon money endpoints, safe retries withRetry-After - Cursor pagination helpers (
list_all()),X-Request-IDand rate-limit info on every result - Webhook signature verification (
verify,construct_event) - Python 3.9+, one dependency (
httpx)
Source: https://github.com/VDSok/sdk-python · Docs: https://vdsok.guru/developers · Issues: https://github.com/VDSok/sdk-python/issues
Install
pip install vdsok
Quick start
from vdsok import Vdsok
client = Vdsok("vk_live_...") # create keys in the cabinet at /my/api
me = client.me()
print(me.account_id, me.scopes, me.livemode) # livemode is False for vk_test_… keys
balance = client.balance.get()
print(balance.balance, balance.currency) # Decimal('42.15') USD
for server in client.servers.list_all(status="active"):
print(server.id, server.name, server.primary_ip, server.billing.next_due_at)
Async:
import asyncio
from vdsok import AsyncVdsok
async def main():
async with AsyncVdsok("vk_live_...") as client:
tariffs = await client.catalog.tariffs()
async for server in client.servers.list_all():
print(server.name)
asyncio.run(main())
Use a vk_test_... key to try everything without spending money: reads return
real data, writes are simulated and every response is flagged
result.sandbox == True.
Configuration
Vdsok(
api_key,
base_url="https://vdsok.guru/api/v1",
timeout=30.0, # seconds, per request
max_retries=2, # on 429/502/503/504 and network errors
max_retry_after=60.0, # longer Retry-After is not waited for
http_client=None, # bring your own httpx.Client (proxies, certificates, ...)
user_agent=None, # replaces "vdsok-sdk-python/<version> ..."
default_headers=None, # extra headers on every request
)
Every request carries Authorization: Bearer <key>, Accept: application/json,
User-Agent: vdsok-sdk-python/<version> and a fresh X-Request-ID (UUID v4).
The key is never logged; repr(client) shows only its last characters.
default_headers is merged under those four, so it can add headers but never
replace the credential — change the agent string with user_agent= instead.
Resources
| Group | Methods |
|---|---|
client.me() / health() / openapi() |
calling key, liveness, the spec |
client.account |
get() |
client.balance |
get(), transactions(), transactions_all(), topup_info(), topup(amount, gateway) |
client.invoices |
list(), list_all(), get(id), pdf(id), pay(id), payment_link(id) |
client.catalog |
tariffs(), tariff(id), os_images(), locations(), zones(), quote(tariff_id, months=…) |
client.servers |
list(), list_all(), create(tariff_id, os, …), get(id, include_live=…), update(id, …), delete(id), status(id), refund_quote(id), renew(id, months=…) |
client.servers.actions |
power(id, action), start(id), stop(id), restart(id), reinstall(id, os, …), reset_password(id) |
client.servers.ips |
list(id), quote(id), add(id), delete(id, ip_id), set_ptr(id, ip_id, ptr) |
client.servers.orders |
list(), list_all(), get(invoice_id), wait(invoice_id) |
client.domains |
check_availability(name), list(), list_all(), register(name, …), get(id), update(id, …), renew(id, years), set_nameservers(id, [...]), transfer(name, auth_code) |
client.ssh_keys |
list(), create(name, public_key), delete(id) |
client.keys |
list(), get(id), revoke(id), revoke_current() |
client.webhooks |
list(), create(url, events), events(), get(id), update(id, …), delete(id), rotate_secret(id), test(id), verify(...), construct_event(...) |
client.webhooks.deliveries |
list(webhook_id), list_all(webhook_id), get(delivery_id), redeliver(delivery_id) |
Anything the API adds later is reachable through the escape hatch:
client.request("GET", "/servers", params={"limit": 5}) # -> dict
Ordering a server
from vdsok import InsufficientFundsError, ConflictError, Order
quote = client.catalog.quote(12, months=3)
if not quote.balance_sufficient:
print("top up", quote.shortfall, quote.currency)
try:
created = client.servers.create(12, "ubuntu-24.04", name="web-01", months=3, ssh_key_ids=[3])
except InsufficientFundsError as e:
print("need", e.shortfall, e.currency, "more") # nothing was charged
except ConflictError as e:
print(e.code) # no_capacity, tariff_unavailable, ...
else:
if isinstance(created, Order): # 202: charged, still provisioning
created = client.servers.orders.wait(created.invoice_id)
print(created.status, created.server_id) # active / cancelled (refunded)
else: # 201: up and running
print(created.server.primary_ip, created.root_password) # password is shown once
servers.create() returns a ServerCreated on 201 and an Order on
202 provisioning (the charge is kept, the panel was slow). orders.wait()
polls GET /orders/{invoice_id} until the order is active or cancelled
(a failed order is refunded and reported as cancelled).
An hourly order prepays exactly one hour: pass hours=1 (anything else is a
400) and buy the rest with servers.renew(id, hours=…), up to 2160 at a
time.
Money, idempotency and retries
Money is always a Decimal ("5.90" on the wire, never a float). Amounts you
pass in may be Decimal, int or a numeric string; floats are converted
through their repr and anything beyond 4 fraction digits is rejected.
Endpoints that move money (servers.create, servers.renew, servers.delete,
servers.ips.add, invoices.pay, balance.topup, domains.register,
domains.renew, domains.transfer) require an Idempotency-Key. The SDK
generates a UUID for you and exposes it on the result and on any error:
result = client.servers.renew(2001, months=1)
print(result.idempotency_key)
try:
client.servers.renew(2001, months=1, idempotency_key="renew-2001-2026-09")
except vdsok.ServerError as e:
# replaying with the same key returns the stored outcome instead of charging twice
client.servers.renew(2001, months=1, idempotency_key=e.idempotency_key)
Retries (default 2) happen only where they are safe: GET requests and
mutations that carry an Idempotency-Key. They trigger on 429, 502, 503,
504, 409 idempotency_in_progress and network errors, honour Retry-After
(seconds or HTTP date) and fall back to exponential backoff. A plain POST
like servers.actions.restart() is never retried automatically.
Errors
import vdsok
try:
client.servers.get(999)
except vdsok.NotFoundError as e:
print(e.status, e.code, e.message, e.request_id) # 404 not_found ... 9f1c2a9d-4b7e-...
except vdsok.RateLimitError as e:
time.sleep(e.retry_after or 1)
except vdsok.ApiError as e: # any other 4xx/5xx
print(e.details)
except vdsok.TransportError as e: # no HTTP response at all
print(e.request_id) # the X-Request-ID the SDK sent
| Class | Status |
|---|---|
InvalidRequestError |
400, 413, 415 (.fields for validation_error) |
AuthenticationError |
401 |
InsufficientFundsError |
402 (.required, .balance, .shortfall, .currency) |
PermissionDeniedError |
403 (.required_scopes) |
NotFoundError |
404 |
ConflictError |
409 |
RateLimitError |
429 (.retry_after) |
ServerError |
5xx |
Every result and error exposes request_id, rate_limit
(RateLimitInfo(limit, remaining, reset)), sandbox and, on money endpoints,
idempotency_key. Results also keep the original JSON in .raw.
Pagination
page = client.invoices.list(limit=50, status="not_paid")
page.data, page.next_cursor, page.has_more
nxt = client.invoices.list(cursor=page.next_cursor)
for invoice in client.invoices.list_all(status="not_paid"): # lazy, all pages
...
Webhooks
Both entry points take the same arguments in the same order —
(secret, headers, raw_body, tolerance=300):
from vdsok import construct_event, verify, WebhookSignatureError
# Flask
@app.post("/vdsok-webhook")
def hook():
try:
event = construct_event(WEBHOOK_SECRET, request.headers, request.get_data())
except WebhookSignatureError:
abort(400)
if event.type == "server.suspended":
server = vdsok.models.parse(vdsok.Server, event.data.object)
...
return "", 204
# same check, without exceptions
if not verify(WEBHOOK_SECRET, request.headers, request.get_data()):
abort(400)
Always pass the raw request body: the signature covers the exact bytes VDSok
sent, so a re-serialized JSON will not match. Deliveries are retried, so
deduplicate by event.id (X-Webhook-Id).
Development
pip install -e ".[dev]"
pytest
The test suite mocks HTTP with respx and never touches the network.
Versioning
SDK 1.x tracks API v1. The API only grows additively; breaking changes go to
/v2 and a new SDK major. See CHANGELOG.md.
License
MIT — see LICENSE.
Metadata
Release files for vdsok 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vdsok-1.0.0.tar.gz | 67.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vdsok-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 117.6 kB
Release files / vdsok-1.0.0.tar.gz
| Download URL | vdsok-1.0.0.tar.gz |
|---|---|
| Size | 67.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
df14b590c0c3922428291ee41409db2d7be8da400f5bda3a9afde9e967035b8d
|
|
BLAKE2b-256 checksum How to use checksums |
3ede4f81a98483eeef0c03ea40ab34a98e26b8113c1e7829757603f46d5e68c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|
Release files / vdsok-1.0.0-py3-none-any.whl
| Download URL | vdsok-1.0.0-py3-none-any.whl |
|---|---|
| Size | 50.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b92ed03f1ba96aa5fdf5494ae515ca522c87528cd839c9ecf6a23d58ffdd32e4
|
|
BLAKE2b-256 checksum How to use checksums |
00317296bc46d177d7bf4bbbd3c9c86a513220f53ed851e0eabb43106345aa60
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|