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.
Servers
KVM VPS: order, power, billing, firewallThe 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, appOrder & manage
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.
Your servers, newest first, deleted ones excluded. At most 50 per page.
{ "ok": true, "data": { "items": [ Server, … ], "total": 2, "offset": 0, "limit": 20 } }Charges the first term, reserves capacity and an IPv4 address, and queues provisioning. Returns the server in PENDING. Counts against the 20 / minute limit.
| body | type | |
|---|---|---|
specrequired | object | { "vcpu", "ramGb", "diskGb" }, integers inside the catalogue's custom limits and steps (disk in 20 GB steps) |
os | string | image code from catalogue.os; required unless app is given |
app | string | pre-installed app from catalogue.apps; forces the app's baseOs (any os is ignored) and needs at least minRamMb of RAM |
periodrequired | MONTH QUARTER HALF_YEAR YEAR | the paid term; renewals use the same term unless you change it |
region | string | one of catalogue.regions; omitted = any region with room |
proxy | boolean | front-proxy add-on: web traffic through a separate front address; its price is added to every term. Only while catalogue.proxy.available |
sshKeyIds | uuid[] · up to 10 | SSH 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" }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.
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.
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": "…" } } }Send only what you change; returns the server.
| body | type | |
|---|---|---|
autoRenew | boolean | at expiry, renew for the current term from the balance; if the balance is short the server is suspended instead. ACTIVE / SUSPENDED servers only |
alias | string ≤ 32 · null | your own name for the server; null or "" clears it |
{ "autoRenew": true, "alias": "api-prod" }Power & reinstall
Queues a power or lifecycle action and answers { "queued": true } right away; watch the result in GET /servers/{id}.
| body | type | |
|---|---|---|
actionrequired | start stop reboot reinstall | |
os | string | reinstall only: switch to another image from the catalogue |
app | string | reinstall 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.
Renewal, term & upgrade
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.
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 } ] }{ "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.
{ "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" } }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.
| body | type | |
|---|---|---|
specrequired | object | the target size, as in the quote |
maxChargerequired | money string | the 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": "…" } } }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
| rule | allows |
|---|---|
22 | TCP port 22 from anywhere |
80,443 | several ports (up to 10) |
8000:8100/udp | a port range; /udp or /tcp (default TCP) |
3306 from 1.2.3.4 | only from one IPv4 address, or a network: from 10.0.0.0/8 (/8…/32) |
ping · icmp | ICMP; 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.
{ "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" }{ "enabled": true | false } — turn the allow-list on or off; the rules are kept either way. Returns the firewall.
{ "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.
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.