api v1

Void API — your domains, servers and hosting, over REST.

Register and renew domains, put them behind Cloudflare and manage DNS, order and run VPS servers and web hosting — from your own code, on the same account and balance as the bot.

https://api.void.cd/v1 Bearer void_… JSON · UTC · USD strings

Servers

KVM VPS: order, power, billing, firewall

The same servers as in the bot, on the same balance. An order charges the first term and queues provisioning: the answer is 201 with the server in PENDING; poll GET /servers/{id} until it is ACTIVE (about a minute for Linux; Windows takes longer and passes through PROVISIONING). Secrets — the root / Administrator password and the login of a pre-installed app — never appear in lists, details or the order response: read them from GET /servers/{id}/credentials only.

Power actions, reinstall, upgrade and firewall changes need an ACTIVE server (400 SERVER_NOT_ACTIVE otherwise) and run one at a time: while another operation is queued or running the server answers 409 SERVER_BUSY.

# 1. what can be ordered right now: images, apps, regions, configurator limits
curl -s "$API/servers/catalogue" -H "Authorization: Bearer $VOID_KEY"

# 2. order (charges the first term, returns 201 + the server in PENDING)
curl -s -X POST "$API/servers" -H "Authorization: Bearer $VOID_KEY" \
  -H "Content-Type: application/json" \
  -d '{"spec":{"vcpu":2,"ramGb":4,"diskGb":80},"os":"ubuntu-24","period":"MONTH"}'

# 3. poll until "status":"ACTIVE", then read the root password
curl -s "$API/servers/$ID" -H "Authorization: Bearer $VOID_KEY"
curl -s "$API/servers/$ID/credentials" -H "Authorization: Bearer $VOID_KEY"
// call() as in the Quickstart
const server = await call("POST", "/servers", {
  spec: { vcpu: 2, ramGb: 4, diskGb: 80 }, os: "ubuntu-24", period: "MONTH",
});

let s = server;
while (s.status === "PENDING" || s.status === "PROVISIONING") {
  await new Promise((r) => setTimeout(r, 15_000));
  s = await call("GET", `/servers/${server.id}`);
}
if (s.status !== "ACTIVE") throw new Error(`provisioning ended in ${s.status}`);

const { ip, login, password } = await call("GET", `/servers/${server.id}/credentials`);
# call() as in the Quickstart
import time

server = call("POST", "/servers", spec={"vcpu": 2, "ramGb": 4, "diskGb": 80}, os="ubuntu-24", period="MONTH")

s = server
while s["status"] in ("PENDING", "PROVISIONING"):
    time.sleep(15)
    s = call("GET", f"/servers/{server['id']}")
if s["status"] != "ACTIVE":
    raise RuntimeError(f"provisioning ended in {s['status']}")

creds = call("GET", f"/servers/{server['id']}/credentials")   # ip, login, password, app

Order & manage

GET/servers/catalogue

What can be ordered right now. os[].code and apps[].code go into the order; custom holds the configurator limits — max already reflects what the roomiest node can still take; periods are price multipliers against one month (term discounts are baked in); proxy says whether the front-proxy add-on can be ordered now. plans are the bot's fixed plans, listed for reference — the API orders by spec.

{ "ok": true, "data": {
    "plans":   [ { "code": "…", "nameEn": "…", "nameRu": "…", "vcpu": 1, "ramMb": 1024, "diskGb": 20, "trafficGb": null, "bandwidthMbps": …,
                   "prices": { "MONTH": "…", "QUARTER": "…", "HALF_YEAR": "…", "YEAR": "…" } }, … ],
    "os":      [ { "code": "ubuntu-24", "name": "Ubuntu 24.04" }, { "code": "windows-2022", "name": "Windows Server 2022" }, … ],
    "apps":    [ { "code": "3x-ui", "name": "3x-ui", "descEn": "…", "descRu": "…", "baseOs": "ubuntu-24", "minRamMb": 1024, "minutes": 2 }, … ],
    "regions": ["PL"],
    "custom":  { "cpu":    { "min": 1,  "max": 16,   "step": 1,  "monthly": 1.25 },
                 "ramGb":  { "min": 1,  "max": 64,   "step": 1,  "monthly": 1.5 },
                 "diskGb": { "min": 20, "max": 1000, "step": 20, "monthlyPer10Gb": 0.25 },
                 "base": { "monthly": 3 }, "defaults": { "vcpu": 2, "ramGb": 4, "diskGb": 80 } },
    "periods": { "MONTH": 1, "QUARTER": 2.925, "HALF_YEAR": 5.7, "YEAR": 11.1 },
    "proxy":   { "available": true, "monthly": 3, "free": 12, "total": 16 } } }

Rates in custom and proxy are plain numbers, USD per month; the values above are an example — read them from the call. Monthly price = base + vcpu × cpu.monthly + ramGb × ramGb.monthly + diskGb / 10 × monthlyPer10Gb (+ proxy.monthly with the add-on); the price of a term = monthly × periods[term], rounded to cents. The order response carries the exact periodPrice you paid.

GET/servers?offset=0&limit=20

Your servers, newest first, deleted ones excluded. At most 50 per page.

{ "ok": true, "data": { "items": [ Server, … ], "total": 2, "offset": 0, "limit": 20 } }
POST/servers201 · charges balance

Charges the first term, reserves capacity and an IPv4 address, and queues provisioning. Returns the server in PENDING. Counts against the 20 / minute limit.

bodytype
specrequiredobject{ "vcpu", "ramGb", "diskGb" }, integers inside the catalogue's custom limits and steps (disk in 20 GB steps)
osstringimage code from catalogue.os; required unless app is given
appstringpre-installed app from catalogue.apps; forces the app's baseOs (any os is ignored) and needs at least minRamMb of RAM
periodrequiredMONTH QUARTER HALF_YEAR YEARthe paid term; renewals use the same term unless you change it
regionstringone of catalogue.regions; omitted = any region with room
proxybooleanfront-proxy add-on: web traffic through a separate front address; its price is added to every term. Only while catalogue.proxy.available
sshKeyIdsuuid[] · up to 10SSH keys saved on the account (managed in the bot), installed at first boot; ids that are not yours are ignored
{ "spec": { "vcpu": 2, "ramGb": 4, "diskGb": 80 }, "os": "ubuntu-24", "period": "QUARTER", "region": "PL" }
{ "spec": { "vcpu": 2, "ramGb": 2, "diskGb": 40 }, "app": "3x-ui", "period": "MONTH" }
402 INSUFFICIENT_FUNDS400 NO_CAPACITY · NO_IP_AVAILABLE · NO_TEMPLATE nothing charged400 PROXY_NO_CAPACITY400 VALIDATION_ERROR spec out of range, unknown app, too little RAM for the app

Not idempotent: every successful call is another paid server. After a timeout, look at GET /servers before you retry. If provisioning fails for good, the server ends in FAILED and the first term is refunded to the balance.

GET/servers/{id}

One server — see Server. For a pre-installed app, app.state goes PENDING → INSTALLING → READY (or FAILED) after the server itself is ACTIVE; app.url and app.note appear once it is ready. power is refreshed periodically and can lag a little behind an action.

GET/servers/{id}/credentials

The only place secrets are returned. login is root, or Administrator on Windows; app adds the panel URL, login and password of a pre-installed app once it is READY. Treat the response like the API key itself — keep it out of logs.

{ "ok": true, "data": { "ip": "203.0.113.25", "login": "root", "password": "…", "app": null } }

// with an app:
{ "ok": true, "data": { "ip": "203.0.113.25", "login": "root", "password": "…",
    "app": { "code": "3x-ui", "name": "3x-ui", "state": "READY", "url": "https://…", "login": "…", "password": "…" } } }
PATCH/servers/{id}

Send only what you change; returns the server.

bodytype
autoRenewbooleanat expiry, renew for the current term from the balance; if the balance is short the server is suspended instead. ACTIVE / SUSPENDED servers only
aliasstring ≤ 32 · nullyour own name for the server; null or "" clears it
{ "autoRenew": true, "alias": "api-prod" }

Power & reinstall

POST/servers/{id}/actions202

Queues a power or lifecycle action and answers { "queued": true } right away; watch the result in GET /servers/{id}.

bodytype
actionrequiredstart stop reboot reinstall
osstringreinstall only: switch to another image from the catalogue
appstringreinstall only: reinstall with a pre-installed app (its base image is implied; RAM minimum applies)
{ "action": "reboot" }
{ "action": "reinstall", "os": "ubuntu-24" }

reinstall erases the disk. The server is cloned again from a clean image — the same one if you pass neither os nor app. The IP address, size and password stay (a Windows image may need a stronger one — read /credentials again), and your firewall rules are re-applied. Only images available on the server's node can be chosen: 400 NO_TEMPLATE otherwise.

400 SERVER_NOT_ACTIVE409 SERVER_BUSY another operation is queued or running

Renewal, term & upgrade

POST/servers/{id}/renewcharges balance

Charges periodPrice now and extends expiresAt by one term, counted from the later of the current expiry and now — renewing early never loses paid time. A SUSPENDED server is started again. Each successful call adds another term. Returns the server.

402 INSUFFICIENT_FUNDS400 SERVER_NOT_ACTIVE failed or deleted
GET/servers/{id}/term

Every renewal term with its price at today's rates; current marks the server's term.

{ "ok": true, "data": [ { "period": "MONTH", "price": "13.50", "current": true }, { "period": "QUARTER", "price": "39.49", "current": false },
                        { "period": "HALF_YEAR", "price": "76.95", "current": false }, { "period": "YEAR", "price": "149.85", "current": false } ] }
PUT/servers/{id}/term

{ "period": "YEAR" } — switches the renewal term. Nothing is charged now and the current expiresAt does not move: the next renewal, automatic or manual, charges the new term's price and extends by the new term. Returns the same list as GET. ACTIVE / SUSPENDED servers only.

POST/servers/{id}/upgrade/quote

{ "spec": { "vcpu", "ramGb", "diskGb" } } — what an upgrade to this size costs. Upgrades only grow: every value the same or bigger, at least one bigger, inside the configurator limits (a smaller server goes through support). charge is due now: the difference in term price, pro rata for the rest of the paid term — (new periodPrice − old periodPrice) × remaining / term length. periodPrice is what each renewal costs afterwards.

{ "spec": { "vcpu": 4, "ramGb": 8, "diskGb": 80 } }

{ "ok": true, "data": { "serverId": "3b0c…", "from": { "vcpu": 2, "ramGb": 4, "diskGb": 80 }, "to": { "vcpu": 4, "ramGb": 8, "diskGb": 80 },
    "charge": "5.48", "periodPrice": "22.00", "period": "MONTH", "expiresAt": "2026-12-01T10:00:00.000Z" } }
POST/servers/{id}/upgrade202 · charges balance

Charges the quoted amount immediately and queues the resize; returns the quote that was applied. The new CPU and RAM take effect through a restart — a running server is rebooted, so expect a short downtime. The virtual disk grows in place; if the guest OS does not extend its partition on boot, extend it yourself.

bodytype
specrequiredobjectthe target size, as in the quote
maxChargerequiredmoney stringthe most you agree to pay now — pass the quote's charge
{ "spec": { "vcpu": 4, "ramGb": 8, "diskGb": 80 }, "maxCharge": "5.48" }

// the price went above maxCharge: nothing charged, the fresh quote is in details
{ "ok": false, "error": { "code": "UPGRADE_PRICE_CHANGED", "message": "…",
    "details": { "serverId": "3b0c…", "from": { … }, "to": { … }, "charge": "5.60", "periodPrice": "22.00", "period": "MONTH", "expiresAt": "…" } } }
400 UPGRADE_PRICE_CHANGED confirm the new quote400 NO_CAPACITY the node has no room, nothing charged402 INSUFFICIENT_FUNDS409 SERVER_BUSY

Firewall

An inbound allow-list on the hypervisor, in front of the guest OS. Off (the default): all inbound traffic is open. On: inbound traffic is dropped except what your rules allow. Already established connections survive the switch. Isolation from other customers and anti-spoofing stay on in every state and are not part of this list. With the proxy add-on on, ports 80/443 from the server's front address (proxySource) are allowed automatically, so the proxy keeps working. Changes are written to the hypervisor right away; if that fails, the previous set is kept and the call answers PROVIDER_ERROR.

Rule syntax

ruleallows
22TCP port 22 from anywhere
80,443several ports (up to 10)
8000:8100/udpa port range; /udp or /tcp (default TCP)
3306 from 1.2.3.4only from one IPv4 address, or a network: from 10.0.0.0/8 (/8…/32)
ping · icmpICMP; also takes from …

Up to 20 rules. Until you change anything, rules shows the defaults that apply when you turn the firewall on: SSH (22, or 3389 RDP on Windows), 80,443 and ping.

GET/servers/{id}/firewall
{ "ok": true, "data": { "enabled": false,
    "rules": [ { "proto": "tcp", "port": "22" }, { "proto": "tcp", "port": "80,443" }, { "proto": "icmp" } ],
    "proxySource": null } }
// a rule limited to a source: { "proto": "tcp", "port": "3306", "source": "1.2.3.4" }
PATCH/servers/{id}/firewall

{ "enabled": true | false } — turn the allow-list on or off; the rules are kept either way. Returns the firewall.

POST/servers/{id}/firewall/rules201

{ "rule": "3306 from 1.2.3.4" } — appends one rule in the syntax above and returns the whole firewall. Adding a rule that is already there changes nothing.

400 VALIDATION_ERROR malformed rule or 20 rules already409 SERVER_BUSY
DELETE/servers/{id}/firewall/rules/{index}

Removes the rule at this 0-based position in rules and returns the firewall. Positions shift after a removal — read the list again before removing another one. 404 NOT_FOUND if there is no such position.