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>
ConditionHTTPerror.code
missing/invalid/revoked token401unauthorized
user.is_blocked403forbidden
maintenance flag503maintenance
RPM exceeded (user or IP)429rate_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

TopicRule
Content-Typeapplication/json for bodies
TimestampsISO-8601 UTC (2026-08-01T12:00:00+00:00)
MoneyRUB, number, 2 decimal places
Pagination?limit=1..200&offset>=0 (default limit 50)
Idempotencycreate/renew are not idempotent — each success charges again
Correlationresponse header + body field X-Request-Id / request_id
CORSAccess-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 по цене каталога.

CallBalance effectFailure
POST /v1/servers-quote atomic402 insufficient_funds; provider fail → refund
POST /v1/servers/{id}/renew-quote(period_months) atomic402 insufficient_funds
power / reinstall / sync0402 expired if past expires_at
DELETE /v1/servers/{id}0
POST /v1/topups+ after crypto paidmin/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

  1. User chooses plan/region/OS in your UI
  2. Your backend calls POST /v1/catalog/quote → show final RUB price
  3. Ensure your VoltHost balance ≥ price (or topup first)
  4. POST /v1/servers → persist returned server.id, order_id
  5. If status === "pending", poll /sync until main_ip
  6. Deliver credentials to end-user from server.password or GET …/credentials
  7. 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

402insufficient_funds
400invalid_catalog / plan_disabled / os_disabled / region_unavailable
503unavailable (provisioning disabled)
502provider_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/healthliveness
GET/v1live endpoint catalog + access matrix
GET/v1/openapi.jsonOpenAPI 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/serverscharge + 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/tokensmetadata only
POST/v1/tokensreturns raw once
DEL/v1/tokens/{id}revoke

Errors

{
  "error": {
    "code": "insufficient_funds",
    "message": "Insufficient balance",
    "details": {}                 // optional
  },
  "request_id": "a1b2c3d4e5f60718"
}
codeHTTPRetry?
unauthorized401no — fix token
forbidden403no
not_found404no
invalid_* / *_disabled400no — fix payload
insufficient_funds402after topup
expired402after renew
rate_limited429yes, backoff
maintenance / unavailable503yes
provider_error502cautious 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