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.

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

RoleWhereNotes
userTelegram + prepaid balanceowner of servers/orders
api-clientHTTP Bearer tokenacts 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":"…"}
TopicRule
Content-Typeapplication/json for bodies
TimestampsISO-8601 UTC (2026-08-01T12:00:00+00:00)
MoneyRUB, number, 2 decimal places; charge before provision (=reserve), refund on fail
Pagination?page=&limit= или ?offset=&limit= (servers/orders/…)
FiltersGET /v1/servers?status=active&region_id=fra&lifecycle=running
Idempotencyheader + stored response · TTL 24h · same body → replay · different → 409
Rate limitX-RateLimit-Limit|Remaining|Reset · 429 rate_limited (=RATE_LIMIT_EXCEEDED) · Retry-After
Correlationresponse header + body field X-Request-Id / request_id
CORSAccess-Control-Allow-Origin: * (Bearer-based)
AuditGET /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

CaseBehavior
Provider timeout on createcharge refunded → 502 provider_error
Duplicate create (retries)send Idempotency-Key
Payment ok, service pendingtopup claimed once; wait server.ready webhook or poll /sync
Expire / zero balanceserver halted (402 expired on power); auto_renew if funded
API spam429 rate_limited (per user + per IP)
Invalid state400 invalid_* / 402 expired

Billing semantics

Аккаунт prepaid + state machine. Create = reserve → provision → confirm (не «сначала сервер, потом деньги»).

  1. Reserve — атомарный charge с баланса (WHERE balance_rub >= amount) → ledger debit с transaction_id
  2. Create — provision у провайдера (sync или job)
  3. Confirm — сервер/job успешен; при fail провайдера → automatic refund credit
StateMeaning
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.

CallBalance effectFailure
POST /v1/serversreserve -quote atomic402 insufficient_funds; 403 quota_exceeded; 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 + Idempotency-Key + async:true → persist job.id / order_id
  5. Poll GET /v1/jobs/{id} (или webhook server.ready); при sync — /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

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/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/serversreserve→create; async202 {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/tokensmetadata + 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/webhookssecret один раз
GET/v1/webhooks/{id}/deliveriesлог доставки
POST…/deliveries/{id}/replayreplay
DEL/v1/webhooks/{id}disable

Endpoints · Audit

GET/v1/audit/logsкто создал/удалил сервер, rotate, topup…
GET/v1/audit · /v1/logsaliases

Errors

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