VoltHost HTTP API
JSON API над prepaid-аккаунтом VoltHost. Подходит для своего Telegram-бота,
личного кабинета, биллинга реселлера или CI-автоматизации.
BASE
https://api.volthost.pro
- Transport: HTTPS, JSON request/response, UTF-8
- Auth:
Authorization: Bearer vh_live_…
- Scope: только ресурсы владельца токена
- Machine-readable catalog:
GET /v1, schema: GET /v1/openapi.json
- Out of scope (HTTP
404): admin, promo, support tickets
Quickstart
export VH_BASE=https://api.volthost.pro
export VH_TOKEN=vh_live_xxxxxxxx
# 1) whoami + balance
curl -sS -H "Authorization: Bearer $VH_TOKEN" "$VH_BASE/v1/account" | jq .
# 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 .quote.price_rub
# 3) provision (charges balance)
curl -sS -X POST "$VH_BASE/v1/servers" \
-H "Authorization: Bearer $VH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"plan_id":"vc2-1c-1gb","region_id":"fra","os_id":2284,"period_months":1}' | jq .
Токен выпускается один раз: Telegram @VoltHostbot → Кабинет → API ключи,
либо POST /v1/tokens при наличии уже активного ключа.
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 |
Token lifecycle
# list (prefix only, never full secret)
GET /v1/tokens
# create — response includes raw token once
POST /v1/tokens
{"name":"reseller-prod"}
# revoke
DELETE /v1/tokens/{id}
- Max 10 active tokens / account
- Store secret in env/secret manager; never ship to end-user clients
- Cross-account
server_id/order_id/topup_id → always 404
Conventions
| 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 |
| Pagination | ?limit=1..200&offset>=0 (default limit 50) |
| Idempotency | create/renew are not idempotent — each success charges again |
| Correlation | response header + body field X-Request-Id / request_id |
| CORS | Access-Control-Allow-Origin: * (Bearer-based) |
Webhooks отсутствуют. После POST /servers при status=pending
полли POST /v1/servers/{id}/sync или GET /v1/servers/{id} до появления main_ip.
Billing semantics
Аккаунт prepaid. Charge происходит на стороне VoltHost по цене каталога.
| Call | Balance effect | Failure |
POST /v1/servers | -quote atomic | 402 insufficient_funds; 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 → persist returned server.id, order_id
- If
status === "pending", poll /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 (admin setting)
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;
}
// example
const created = await vh('/v1/servers', {
method: 'POST',
body: { plan_id: 'vc2-1c-1gb', region_id: 'fra', os_id: 2284 },
});
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 | charge + provision |
| GET | /v1/servers/{id} | |
| PATCH | /v1/servers/{id} | label, auto_renew |
| DEL | /v1/servers/{id} | |
| 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 only |
| POST | /v1/tokens | returns raw once |
| DEL | /v1/tokens/{id} | revoke |
Errors
{
"error": {
"code": "insufficient_funds",
"message": "Insufficient balance",
"details": {} // optional
},
"request_id": "a1b2c3d4e5f60718"
}
| code | HTTP | Retry? |
unauthorized | 401 | no — fix token |
forbidden | 403 | no |
not_found | 404 | no |
invalid_* / *_disabled | 400 | no — fix payload |
insufficient_funds | 402 | after topup |
expired | 402 | after renew |
rate_limited | 429 | yes, backoff |
maintenance / unavailable | 503 | yes |
provider_error | 502 | cautious retry; 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