VoltHost HTTP API
JSON API над prepaid-аккаунтом VoltHost. Подходит для своего Telegram-бота,
личного кабинета, биллинга реселлера или CI-автоматизации.
BASE
https://api.volthost.pro
- Transport: HTTPS, JSON request/response, UTF-8
- Versioning: все пути под
/v1/… (ломающие изменения → /v2)
- Auth:
Authorization: Bearer vh_live_… (+ scopes, allowed_ips, rotate/revoke)
- Envelope:
{status, data, error, request_id} (кроме /health, openapi.json)
- Rate limit: headers +
429 / Retry-After
- Machine-readable catalog:
GET /v1 · GET /v1/openapi.json
Production guarantees (TOP-5)
Это не «идеи в roadmap» — контракт, который уже в API.
| # | Guarantee | How |
| 1 |
Job / async |
POST /v1/servers + {"async":true} → 202 {job} · poll GET /v1/jobs/{id} |
| 2 |
Idempotency |
header Idempotency-Key на create / renew / topup (replay / 409 conflict) |
| 3 |
Versioning |
только /v1/…; ломающие изменения → /v2 |
| 4 |
Rate limits |
X-RateLimit-Limit|Remaining|Reset, 429 rate_limited, Retry-After (user+IP) |
| 5 |
Anti-IDOR / BOLA |
чужой server_id / order / topup / job → всегда 404 (без утечки факта) |
- Billing reserve: charge before provision, automatic refund on provider fail
- Quotas:
account.quotas.max_servers → 403 quota_exceeded
- Lifecycle:
provisioning | running | stopped | failed | error (+ billing_state)
- Webhooks: HMAC + retry + deliveries +
…/replay
- Audit:
GET /v1/audit/logs
# machine-readable source of truth
curl -sS https://api.volthost.pro/v1 | jq '.guarantees, .reliability, .maturity'
Maturity map (MVP → scalable SaaS)
Честный статус относительно production-чеклиста. Не roadmap-мечты — что уже в контракте.
| Зона | Статус | Как в API |
| 1. Async / jobs |
done |
{"async":true} · ?async=true · Prefer: respond-async → 202 + GET /v1/jobs/{id} |
| 2. Idempotency |
done |
ключ + cached response + TTL 24h · replay header Idempotent-Replayed |
| 3. Billing atomic |
done |
billing.flow=reserve_confirm · phase reserved|confirmed · fail → refund |
| 4. Webhooks delivery |
done |
HMAC · exponential retry · deliveries log · …/replay · disable |
| 5. Rate limit + Audit |
done |
X-RateLimit-* · 429 rate_limited (=RATE_LIMIT_EXCEEDED) · GET /v1/audit/logs |
Score: 9.5 / 10 · machine: GET /v1 → .maturity
Growth (не блокеры запуска)
- SDK (JS / Python packages)
- Sandbox / test keys (
vh_test_…)
- Templates (
telegram-bot и т.п.)
- Snapshots / soft-delete restore
- Reseller / white-label sub-accounts
Quickstart
export VH_BASE=https://api.volthost.pro
export VH_TOKEN=vh_live_xxxxxxxx
export IDEM=$(uuidgen)
# 1) whoami + balance (+ quotas)
curl -sS -H "Authorization: Bearer $VH_TOKEN" "$VH_BASE/v1/account" | jq .data
# 2) quote
curl -sS -X POST "$VH_BASE/v1/catalog/quote" \
-H "Authorization: Bearer $VH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"plan_id":"vc2-1c-1gb","period_months":1}' | jq .data.quote.price_rub
# 3) async provision (recommended) — charge now, provision in job queue
curl -sS -X POST "$VH_BASE/v1/servers" \
-H "Authorization: Bearer $VH_TOKEN" \
-H "Idempotency-Key: $IDEM" \
-H "Content-Type: application/json" \
-d '{"plan_id":"vc2-1c-1gb","region_id":"fra","os_id":2284,"async":true}' | jq .data
# 4) poll job
JOB_ID=123 # from step 3
curl -sS -H "Authorization: Bearer $VH_TOKEN" "$VH_BASE/v1/jobs/$JOB_ID" | jq .data.job.status
Токен: Telegram @VoltHostbot → Меню → Для разработчиков,
либо POST /v1/tokens. Rotate: POST /v1/tokens/{id}/rotate.
Authentication
Все пути под /v1/*, кроме public meta, требуют Bearer token.
Authorization: Bearer vh_live_<32+ hex>
| Condition | HTTP | error.code |
| missing/invalid/revoked token | 401 | unauthorized |
| user.is_blocked | 403 | forbidden |
| maintenance flag | 503 | maintenance |
| RPM exceeded (user or IP) | 429 | rate_limited (= RATE_LIMIT_EXCEEDED) |
Token lifecycle
# list (prefix only, never full secret)
GET /v1/tokens
# create — response includes raw token once
POST /v1/tokens
{"name":"reseller-prod","scopes":["*"]}
# revoke
DELETE /v1/tokens/{id}
- Max 10 active tokens / account
- Optional
scopes (default ["*"])
- Store secret in env/secret manager; never ship to end-user clients
- Cross-account
server_id/order_id/topup_id → always 404
Roles & scopes
| Role | Where | Notes |
user | Telegram + prepaid balance | owner of servers/orders |
api-client | HTTP Bearer token | acts as user; limited by scopes |
account:read · catalog:read · servers:read · servers:write
billing:read · billing:write · tokens:write · webhooks:write · audit:read · *
Missing scope → 403 forbidden with details.required_scope.
Conventions
// success
{"status":"success","data":{…},"error":null,"request_id":"…"}
// error
{"status":"error","data":null,"error":{"code":"insufficient_funds","message":"…"},"request_id":"…"}
| Topic | Rule |
| Content-Type | application/json for bodies |
| Timestamps | ISO-8601 UTC (2026-08-01T12:00:00+00:00) |
| Money | RUB, number, 2 decimal places; charge before provision (=reserve), refund on fail |
| Pagination | ?page=&limit= или ?offset=&limit= (servers/orders/…) |
| Filters | GET /v1/servers?status=active®ion_id=fra&lifecycle=running |
| Idempotency | header + stored response · TTL 24h · same body → replay · different → 409 |
| Rate limit | X-RateLimit-Limit|Remaining|Reset · 429 rate_limited (=RATE_LIMIT_EXCEEDED) · Retry-After |
| Correlation | response header + body field X-Request-Id / request_id |
| CORS | Access-Control-Allow-Origin: * (Bearer-based) |
| Audit | GET /v1/audit/logs · /v1/audit · /v1/logs |
curl -X POST "$VH_BASE/v1/servers" \
-H "Authorization: Bearer $VH_TOKEN" \
-H "Idempotency-Key: ord_$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"plan_id":"vc2-1c-1gb","region_id":"fra","os_id":2284}'
Same key + same body → replayed response (Idempotent-Replayed: true), ключ + response хранятся 24ч (TTL). Same key + different body → 409 idempotency_conflict.
Edge cases
| Case | Behavior |
| Provider timeout on create | charge refunded → 502 provider_error |
| Duplicate create (retries) | send Idempotency-Key |
| Payment ok, service pending | topup claimed once; wait server.ready webhook or poll /sync |
| Expire / zero balance | server halted (402 expired on power); auto_renew if funded |
| API spam | 429 rate_limited (per user + per IP) |
| Invalid state | 400 invalid_* / 402 expired |
Billing semantics
Аккаунт prepaid + state machine. Create = reserve → provision → confirm (не «сначала сервер, потом деньги»).
- Reserve — атомарный charge с баланса (
WHERE balance_rub >= amount) → ledger debit с transaction_id
- Create — provision у провайдера (sync или job)
- Confirm — сервер/job успешен; при fail провайдера → automatic refund credit
| State | Meaning |
active | нормальная работа |
overdue | есть просроченные/замороженные серверы (expire → halt) |
suspended | аккаунт заморожен — create/renew блокируются |
Поля: account.billing_status, server.billing_state. После topup — auto-resume для серверов с auto_renew.
Ledger: GET /v1/account/transactions → { transaction_id, kind, type, status: "success", amount_rub }. Pending живёт на topup: pending | paid | failed.
| Call | Balance effect | Failure |
POST /v1/servers | reserve -quote atomic | 402 insufficient_funds; 403 quota_exceeded; provider fail → refund |
POST /v1/servers/{id}/renew | -quote(period_months) atomic | 402 insufficient_funds |
| power / reinstall / sync | 0 | 402 expired if past expires_at |
DELETE /v1/servers/{id} | 0 | — |
POST /v1/topups | + after crypto paid | min/max/pending limits |
// recommended guard before provision
const { balance } = await api.get('/v1/account/balance').then(r => r.balance);
const { quote } = await api.post('/v1/catalog/quote', { plan_id, period_months });
if (balance.balance_rub < quote.price_rub) throw new Error('topup_required');
Reseller / bot integration flow
- User chooses plan/region/OS in your UI
- Your backend calls
POST /v1/catalog/quote → show final RUB price
- Ensure your VoltHost balance ≥ price (or topup first)
POST /v1/servers + Idempotency-Key + async:true → persist job.id / order_id
- Poll
GET /v1/jobs/{id} (или webhook server.ready); при sync — /sync until main_ip
- Deliver credentials to end-user from
server.password or GET …/credentials
- Schedule renew via
POST …/renew or set auto_renew: true
End-users never talk to VoltHost API. Keep VH_TOKEN only on your server.
You own the customer UX/pricing markup on top if needed — VoltHost always charges catalog price from your balance.
Create instance
POST/v1/servers
Request
{
"plan_id": "vc2-1c-1gb", // required, sellable plan
"region_id": "fra", // required
"os_id": 2284, // required, int
"period_months": 1 // optional, 1..12, default 1
}
Response 201
{
"order_id": 42,
"charged_rub": 665.0,
"server": {
"id": 7,
"label": "vh-1001-ab12",
"plan_id": "vc2-1c-1gb",
"region_id": "fra",
"os_id": 2284,
"main_ip": "203.0.113.10", // null/absent while pending
"password": "…", // only on create/reinstall
"status": "active", // pending | active | stopped | expired | deleted
"price_rub": 665.0,
"period_months": 1,
"expires_at": "2026-08-31T12:00:00+00:00",
"auto_renew": false,
"created_at": "…",
"updated_at": "…"
}
}
Typical errors
402 | insufficient_funds |
400 | invalid_catalog / plan_disabled / os_disabled / region_unavailable |
503 | unavailable (provisioning disabled) |
502 | provider_error (balance refunded) |
Lifecycle & power
GET /v1/servers
GET /v1/servers/{id}
PATCH /v1/servers/{id} // {"label"?, "auto_renew"?}
DELETE /v1/servers/{id}
POST /v1/servers/{id}/start
POST /v1/servers/{id}/stop
POST /v1/servers/{id}/reboot
POST /v1/servers/{id}/reinstall // body: {"os_id"?: int}
POST /v1/servers/{id}/sync
GET /v1/servers/{id}/credentials
- List/get omit
password; use credentials endpoint
- Power/reinstall blocked when expired →
402 expired
- Delete allowed even if expired
PATCH body accepts only label, auto_renew
// wait for IP
async function waitReady(id, { tries = 30, delayMs = 3000 } = {}) {
for (let i = 0; i < tries; i++) {
const { server } = await api.post(`/v1/servers/${id}/sync`);
if (server.main_ip) return server;
await sleep(delayMs);
}
throw new Error('provision_timeout');
}
Renew / expire
POST/v1/servers/{id}/renew
{
"charged_rub": 665.0,
"server": { "id": 7, "expires_at": "…", "status": "active", … }
}
- Charge = catalog price × stored
period_months
- Extends
expires_at from max(current, now)
- If previously expired/halted, platform attempts start after successful renew
auto_renew: true → worker renews on expiry when balance allows; else halt (default) / destroy
Balance topup
POST/v1/topups
// request
{"amount_rub": 1000}
// response 201
{
"topup": {"id": 9, "amount_rub": 1000, "method": "crypto", "status": "pending", …},
"invoice": {
"invoice_id": 123,
"pay_url": "https://…",
"amount_rub": 1000,
"status": "active"
}
}
- Finite amount only; enforced min/max + pending-invoice cap
- Poll
GET /v1/topups/{id} until status=approved (or check balance)
- Promo / manual topup — bot only, not exposed over HTTP
Client snippets
Python (httpx)
import httpx
class VoltHost:
def __init__(self, token: str, base: str = "https://api.volthost.pro"):
self.http = httpx.Client(
base_url=base,
headers={"Authorization": f"Bearer {token}"},
timeout=60,
)
def quote(self, plan_id: str, period_months: int = 1) -> float:
r = self.http.post("/v1/catalog/quote", json={
"plan_id": plan_id, "period_months": period_months
})
r.raise_for_status()
return r.json()["quote"]["price_rub"]
def create_server(self, plan_id: str, region_id: str, os_id: int, months: int = 1):
r = self.http.post("/v1/servers", json={
"plan_id": plan_id,
"region_id": region_id,
"os_id": os_id,
"period_months": months,
})
if r.status_code == 402:
raise RuntimeError("insufficient_funds")
r.raise_for_status()
return r.json()
Node.js (fetch)
const VH_BASE = process.env.VH_BASE || 'https://api.volthost.pro';
const VH_TOKEN = process.env.VH_TOKEN;
async function vh(path, { method = 'GET', body } = {}) {
const res = await fetch(`${VH_BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${VH_TOKEN}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error(data?.error?.message || res.statusText);
err.code = data?.error?.code;
err.status = res.status;
err.requestId = data?.request_id;
throw err;
}
return data;
}
// recommended: async + Idempotency-Key
const created = await vh('/v1/servers', {
method: 'POST',
body: {
plan_id: 'vc2-1c-1gb',
region_id: 'fra',
os_id: 2284,
async: true,
},
});
// created.data.job.id → poll GET /v1/jobs/{id}
Endpoints · Meta
| GET | /health | liveness |
| GET | /v1 | live endpoint catalog + access matrix |
| GET | /v1/openapi.json | OpenAPI 3.0 |
Endpoints · Account
| GET | /v1/account | { account } |
| GET | /v1/account/balance | { balance: { balance_rub, currency } } |
| GET | /v1/account/transactions | ?limit&offset → { transactions, meta } |
Endpoints · Catalog
| GET | /v1/catalog | { regions, plans, os } |
| GET | /v1/catalog/regions | |
| GET | /v1/catalog/regions/{id} | |
| GET | /v1/catalog/plans?region_id= | |
| GET | /v1/catalog/plans/{id} | |
| GET | /v1/catalog/os | |
| GET | /v1/catalog/os/{id} | |
| POST | /v1/catalog/quote | {plan_id, period_months?, region_id?} |
Endpoints · Servers
| GET | /v1/servers | { servers, meta.count } |
| POST | /v1/servers | reserve→create; async → 202 {job} |
| GET | /v1/servers/{id} | lifecycle + billing_state |
| PATCH | /v1/servers/{id} | label, auto_renew |
| DEL | /v1/servers/{id} | |
| POST | …/actions | {action: start|stop|reboot|reinstall} (предпочтительно) |
| POST | …/start|stop|reboot|reinstall|renew|sync | алиасы |
| GET | …/credentials | { credentials } |
Endpoints · Orders
| GET | /v1/orders | ?limit&offset |
| GET | /v1/orders/{id} | owner-scoped |
Endpoints · Topups
| GET | /v1/topups | |
| POST | /v1/topups | {"amount_rub": number} |
| GET | /v1/topups/{id} | |
Endpoints · Tokens
| GET | /v1/tokens | metadata + scopes |
| POST | /v1/tokens | {name?, scopes?} — raw once |
| DEL | /v1/tokens/{id} | revoke |
Jobs (async create)
По умолчанию create синхронный (201). Для масштаба:
curl -X POST "$VH_BASE/v1/servers" \
-H "Authorization: Bearer $VH_TOKEN" \
-H "Idempotency-Key: ord_$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"plan_id":"vc2-1c-1gb","region_id":"fra","os_id":2284,"async":true}'
# → 202
{"job":{"id":123,"status":"queued","order_id":45},"charged_rub":665.0,"order_id":45}
curl -H "Authorization: Bearer $VH_TOKEN" "$VH_BASE/v1/jobs/123"
# status: queued | running | succeeded | failed | retry
Баланс списывается сразу; провижин — в worker. Следи за webhooks server.created / server.failed / server.ready.
Webhooks
POST callback на ваш URL. Подпись: X-VoltHost-Signature: sha256=<hmac_hex> по raw body и webhook.secret.
POST /v1/webhooks
{"url":"https://example.com/vh/hook","events":["server.ready","server.failed","billing.success","balance.low"]}
# delivery headers
X-VoltHost-Event: server.ready
X-VoltHost-Delivery: 42
X-VoltHost-Signature: sha256=…
# payload
{"id":"…","type":"server.ready","created_at":"…","data":{"server_id":7,"main_ip":"…"}}
# delivery log
GET /v1/webhooks/{id}/deliveries
# replay failed/dead delivery
POST /v1/webhooks/{id}/deliveries/{delivery_id}/replay
POST /v1/webhooks/deliveries/{delivery_id}/replay
- Events:
server.created|ready|failed|renewed|expired|deleted, balance.topup|low, billing.success, or *
- Retry with exponential backoff (up to 8 attempts)
- Replay anytime → status back to
pending
- Max 5 active webhooks / account ·
DELETE = disable
- Polling
/sync или GET /v1/jobs/{id} — fallback
Endpoints · Webhooks
| GET | /v1/webhooks | без secret |
| POST | /v1/webhooks | secret один раз |
| GET | /v1/webhooks/{id}/deliveries | лог доставки |
| POST | …/deliveries/{id}/replay | replay |
| DEL | /v1/webhooks/{id} | disable |
Endpoints · Audit
| GET | /v1/audit/logs | кто создал/удалил сервер, rotate, topup… |
| GET | /v1/audit · /v1/logs | aliases |
Errors
{
"status": "error",
"data": null,
"error": {
"code": "insufficient_funds",
"message": "Insufficient balance",
"details": {} // optional
},
"request_id": "a1b2c3d4e5f60718"
}
| code | HTTP | Retry? |
unauthorized | 401 | no — fix token |
forbidden | 403 | no — scopes / blocked |
not_found | 404 | no |
invalid_* / *_disabled | 400 | no — fix payload |
idempotency_conflict / idempotency_in_progress | 409 | no / wait |
insufficient_funds | 402 | after topup |
expired | 402 | after renew |
rate_limited | 429 | yes, backoff |
maintenance / unavailable | 503 | yes |
provider_error | 502 | cautious retry + Idempotency-Key; create already refunded |
Objects
Account
{
"id": 1001,
"username": "reseller",
"full_name": "…",
"balance_rub": 1500.0,
"referral_earned_rub": 0.0,
"referrer_id": null,
"created_at": "…",
"updated_at": "…"
}
Plan
{
"id": "vc2-1c-1gb",
"title": "1 vCPU · 1 GB · 25 GB SSD",
"vcpu": 1,
"ram_mb": 1024,
"disk_gb": 25,
"bandwidth_gb": 1000,
"price_rub": 665.0,
"price_period": "month",
"currency": "RUB",
"locations": ["fra", "ams", …]
}
Credentials
{
"server_id": 7,
"main_ip": "203.0.113.10",
"username": "root",
"password": "…",
"ssh": "ssh [email protected]"
}
Base https://api.volthost.pro · Token via
@VoltHostbot ·
Schema openapi.json