Home / Docs / API

Automate
everything.

Resell our infrastructure under your brand. Plug-and-play billing platform integrations, plus complete REST API references: the Blackbox line on our own hardware, and the Virtual Server line.

Authentication

All requests carry your reseller key in the X-Reseller-Key header. It is the same key as the Virtual Server API, from your reseller dashboard.

X-Reseller-Key: your_api_key_here

Keep your API key secret. Never expose it in client-side code. Every paid call is billed to the account the key belongs to: orders, renewals and upgrades at your partner discount, address changes at the flat price.

Base URL

https://servury.com/api/reseller.php

This API drives the Blackbox line only: our own hardware in Montreal, AMD SEV-SNP, LUKS you hold the key to, measured boot, one IPv4 and a /64 of IPv6 per server, reverse DNS you manage. The resold VPS line lives behind the Virtual Server API.

Response Format

All endpoints return JSON. Successful responses:

{"success": true, "data": {...}}

Error responses, with an HTTP status that means what it says (reference):

{"success": false, "error": "message"}

Parameters of POST calls may be sent as a JSON body, as form fields, or in the query string. If the same parameter appears in more than one place, the JSON body wins, then the form field, then the query string. action, id and the filters of GET calls are always read from the query string. Booleans accept true/false, 1/0, yes/no. Timestamps are Unix seconds, UTC. Prices are USD.

Lifecycle and Statuses

Provisioning is asynchronous. An order returns at once with a server_id; the machine is built in the background, usually within five minutes. Poll ?action=server until status is active and the addresses are filled in. Reinstalls work the same way.

statusMeaning
deployingPaid, being built. Addresses appear as soon as they are assigned.
queuedA pre-order: paid, waiting for capacity. queue.position says where it stands (the queue holds pre-orders and pre-ordered upgrades alike, by request time). The oldest request that fits the freed room deploys; the term starts that day. Pre-orders are final.
activeLive. Every action is available.
reinstallingBeing rebuilt (or queued for it). Wait for active.
suspendedstatus_reason unpaid: the term ended; renew within 24 hours to restore it, after deleted_after it is gone. status_reason violation: held by our abuse desk; read-only until support lifts it.
failedstatus_reason deploy: the build could not complete; the charge was refunded to the balance; terminate the row (it then answers 404) and order again. status_reason reinstall: the last reinstall failed and the machine is down; nothing was refunded; reinstall it again, or terminate it.

Sync jobs can rely on: expired false = live, expired true = renew or drop, HTTP 404 = gone. powered_on is the VM's power state as the node last reported it (refreshed every minute and on every power action): true on, false off, null not observed yet.

GET

?action=plans

Every Blackbox plan with your partner pricing. in_stock false means no node has room today: the plan can still be ordered as a pre-order (preorder: true on ?action=create) and queue_length is how many orders are waiting for it. One IPv4 and a /64 of IPv6 are included in every price.

{
    "success": true,
    "data": {
        "plans": [
            {
                "id": 108,
                "plan_name": "Blackbox Core",
                "location_id": 2,
                "location": "Canada",
                "cpu": "2x EPYC 7B13",
                "ram": "4 GB DDR4",
                "storage": "50 GB NVMe",
                "cores": 2,
                "ram_mb": 4096,
                "disk_gb": 50,
                "port_speed": "10 Gbps",
                "bandwidth": "4 TB",
                "transfer_tb": 4,
                "retail_price": 9.99,
                "reseller_price_monthly": 9.49,
                "ipv4_surcharge_monthly": 0,
                "in_stock": true,
                "preorder": false,
                "queue_length": 0
            }
        ],
        "queue_length": 0
    }
}

A term of days costs retail_price / 31 * days rounded to the cent, minus your discount, rounded again. Minimum and maximum terms are in ?action=account.

GET

?action=locations

Locations that have Blackbox plans: id, name, city.

GET

?action=os

Images you can order or reinstall, with what each supports. The three flags are the same gates the order form uses: a request that asks for snp, fde or measured on an image that does not support it is refused, never silently downgraded.

{
    "success": true,
    "data": {
        "operating_systems": [
            {"id": "debian13", "name": "Debian 13", "username": "root",
             "supports_confidential": true, "supports_fde": true, "supports_measured_boot": true},
            {"id": "rocky9", "name": "Rocky Linux 9", "username": "root",
             "supports_confidential": true, "supports_fde": true, "supports_measured_boot": true},
            {"id": "freebsd15", "name": "FreeBSD 15", "username": "root",
             "supports_confidential": false, "supports_fde": false, "supports_measured_boot": false}
        ]
    }
}

confidential (AMD SEV-SNP) encrypts the machine's memory under a key the host never holds; Linux images only. fde encrypts the disk in place with LUKS on first boot and is unlocked over SSH in the initramfs, so it needs an SSH key. measured boot folds the kernel, initramfs and command line into the launch measurement the CPU signs; it needs confidential, fde and an SSH key. The measured boot guide explains what each protects.

GET

?action=account

Balance, discount, how paid calls are charged, and the limits this API applies.

{
    "success": true,
    "data": {
        "balance": 42.50,
        "discount_percent": 5,
        "payment_method": "account_balance",
        "has_saved_card": false,
        "limits": {
            "min_days": 7,
            "max_days": 365,
            "ip_change_price": 4.99,
            "deposit_min_usd": 5,
            "deposit_max_usd": 10000
        }
    }
}

payment_method is set in the reseller dashboard: account_balance deducts from the balance, saved_card charges the card on file per call. Pre-ordered upgrades need the balance method. When a card-paid action fails after the charge, the amount is credited to your balance and a refunded invoice carrying the charge id is written.

GET

?action=status

Node status, scheduled maintenance and open incidents, the same data the public status page renders, so you can surface platform health inside your own dashboard. overall is one of operational, maintenance, degraded, partial_outage, major_outage; per-node status is up, degraded or down. Poll no more than once a minute; the data refreshes on a schedule.

{
    "success": true,
    "data": {
        "overall": "operational",
        "generated_at": 1787347200,
        "summary": {"nodes_total": 9, "nodes_down": 0, "nodes_degraded": 0, "open_incidents": 0, "maintenance_scheduled": 0},
        "nodes": [{"id": "...", "name": "Blackbox MTL", "category": "Blackbox", "status": "up", "uptime_90d": 99.98, "last_cause": null, "changed_at": 1786435261}],
        "maintenance": [],
        "incidents": []
    }
}
GET

?action=servers

Every Blackbox server on the account, newest expiry first, in the brief form (the full form is ?action=server), with total. Optional status filter with any status word, e.g. ?action=servers&status=queued.

GET

?action=server&id={server_id}

One server in full.

ParameterTypeDescription
id *string8-character server ID

Example Response

{
    "success": true,
    "data": {
        "server": {
            "id": "abc12345",
            "hostname": "myserver",
            "label": "customer 4411",
            "plan_id": 108,
            "plan": "Blackbox Core",
            "location": "Canada",
            "os": "debian13",
            "username": "root",
            "ipv4_address": "23.155.44.120",
            "ipv6_address": "2602:f41c:0:2a::1",
            "ipv6_subnet": "2602:f41c:0:2a::/64",
            "status": "active",
            "status_reason": null,
            "powered_on": true,
            "snp_enabled": true,
            "fde_requested": true,
            "fde_enabled": true,
            "measured_boot": false,
            "measured_release": null,
            "expiration_date": 1792000000,
            "expired": false,
            "deleted_after": 1792086400,
            "specs": {"cores": 2, "ram_mb": 4096, "disk_gb": 50},
            "rate_monthly": 9.99,
            "auto_renew": false,
            "bandwidth": {"quota_tb": 4, "used_bytes": 183000000000, "window_start": 1789400000, "capped": false},
            "pending_upgrade": null
        }
    }
}

fde_requested is what was ordered; fde_enabled turns true once the node sees a LUKS header on the disk, which on a non-measured build happens after the customer's first boot and on a measured build after they type a passphrase in the initramfs. bandwidth is the transfer allowance for the current 31-day window; past it the port is shaped to 10 Mbps until the window rolls over (capped). pending_upgrade is set while a pre-ordered upgrade waits: plan_id, plan_name, requested_at, position. A queued server carries a queue block instead: position, days, queued_at.

GET

?action=graphs&id={server_id}

CPU, memory and network series for an active server, sampled on the node (no agent in the guest).

ParameterTypeDescription
id *string8-character server ID
timeframestring1hr (default) or 24hr
{
    "success": true,
    "data": {
        "timeframe": "1hr",
        "cpu":     [{"time": 1789480000, "cpu": 3.2}],
        "ram":     [{"time": 1789480000, "ram": 41.5}],
        "network": [{"time": 1789480000, "in": 12.4, "out": 88.1}]
    }
}

cpu and ram are percent, network is KB/s. Memory is the host-side footprint of the VM clamped to the plan, an upper bound: a confidential guest's memory cannot be read from outside, by design.

GETPOST

?action=serial&id={server_id}

The serial console transcript as plain text: boot messages, kernel output, the LUKS prompt of an encrypted machine. The transcript exists only while the server's console log is on; it is off on every machine by default, because a record of what a guest prints is exactly what our privacy policy says we do not keep unless asked. GET reads the transcript and reports the switch; POST flips it. Not available on Windows guests, which emit no serial console.

ParameterTypeDescription
id *string8-character server ID
bytesintegerGET: how much of the tail to return, 1024 to 1048576 (default 65536)
log *booleanPOST: true starts recording at the machine's next start; false empties the transcript on the host at once and stops recording at the next start (until then the running machine keeps appending)
POST ?action=serial&id=abc12345   {"log": true}
{"success": true, "data": {"message": "Console log on. It starts recording at the next start of the machine.", "logging": true}}

GET ?action=serial&id=abc12345&bytes=4000
{"success": true, "data": {"logging": true, "recording_now": true, "pending_restart": false, "bytes": 3912, "output": "[    0.000000] Linux version 6.12 ...\n"}}

With the log on, this is the quickest way to see why a machine is not answering: turn it on, restart with ?action=power, read. The transcript is wired into the machine at start, so recording_now says what the running machine does and pending_restart is true while the switch and the running machine disagree. bytes is 0 when nothing has been recorded.

GET

?action=rdns&id={server_id}

The PTR record of each of the server's addresses (IPv4 and the ::1 of its /64). status is active, pending (applied within a minute), failed, or null when there is no record.

{
    "success": true,
    "data": {
        "records": [
            {"ip": "23.155.44.120", "hostname": "mail.example.com", "status": "active", "updated_at": 1789480000},
            {"ip": "2602:f41c:0:2a::1", "hostname": null, "status": null, "updated_at": null}
        ]
    }
}
GET

?action=console&id={server_id}

Answers 501 with the panel URL. The interactive console (serial, or VNC on Windows) is a short-lived ticket bound to the browser session that opens it and cannot be handed out as a URL. For console output over the API use ?action=serial.

POST

?action=create

Order a server. Either password or ssh_key is required. The call returns as soon as the order is paid and recorded; the machine is built in the background and is usually up within five minutes. Poll ?action=server until status is active. A deploy that fails is refunded to the account balance automatically.

ParameterTypeDescription
plan_id *integerPlan ID from ?action=plans
hostname *string3-32 characters: letters, digits and hyphens, not starting or ending with a hyphen. Our name for the VM; the hostname inside the guest is the customer's to set.
os *stringAn id from ?action=os
daysintegerTerm, 7 to 365 (default 31). Priced at retail_price / 31 * days minus your discount.
passwordstringRoot (or Administrator) password, 8 to 128 characters. Required for Windows.
ssh_keystringOne or more SSH public keys, one per line (or ssh_keys as an array). Each is decoded and checked: a truncated key or a pasted password is refused. Required for fde and measured.
snpbooleanConfidential computing (AMD SEV-SNP). Linux images only. Default false.
fdebooleanLUKS full-disk encryption, unlocked over SSH in the initramfs. Debian, Ubuntu, Rocky Linux, AlmaLinux. Needs ssh_key. Default false.
measuredbooleanMeasured boot. Needs snp, fde and ssh_key. Default false.
preorderbooleanAccept a pre-order if the plan is sold out (see below). Default false: a sold-out plan answers 409 and nothing is charged.

Example

curl -X POST "https://servury.com/api/reseller.php?action=create" \
  -H "X-Reseller-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"plan_id": 108, "hostname": "cust-4411", "os": "debian13", "days": 31,
       "ssh_key": "ssh-ed25519 AAAA... ops@yourbrand", "snp": true, "fde": true}'
{
    "success": true,
    "data": {
        "message": "Order accepted. The machine is built in the background ...",
        "server_id": "abc12345",
        "hostname": "cust-4411",
        "status": "deploying",
        "expiration_date": 1792000000,
        "invoice_id": "7d2e9a10",
        "amount_charged": 9.49
    }
}

Pre-orders. When no node has room, the plan is sold out and the order is refused unless preorder is true. A pre-order is paid now, answers with status queued and a queue_position, and deploys by itself when a slot frees, oldest order first; the term starts the day it deploys. Pre-orders are final: they cannot be cancelled or refunded. Only send preorder: true when your customer has agreed to wait.

An encrypted machine (fde) boots once unencrypted, encrypts its own disk and reboots to an SSH prompt in the initramfs on port 22 where the customer sets the passphrase; ?action=serial shows where it stands. With measured the machine boots straight into the measured initramfs and waits for the customer; see the guide for the attestation flow your customer runs.

POST

?action=renew&id={server_id}

Extend the term. Days are added to the current expiry (a server that expired yesterday and is renewed for 31 days gets 30). A server suspended for non-payment is started again once its new expiry is in the future. A server with a pre-ordered upgrade renews at the new plan's rate (rate_monthly in the response). Your billing drives renewals: there is no auto-renewal on API-ordered servers.

ParameterTypeDescription
id *string8-character server ID
days *integer7 to 365
{"success": true, "data": {"message": "Server renewed.", "new_expiration": 1794678400, "invoice_id": "c01f3b77", "amount_charged": 9.49, "rate_monthly": 9.99, "status": "active"}}
POST

?action=upgrade&id={server_id}

Move an active server to a dearer plan in the same location. You pay the difference between the two monthly rates, prorated to the days left on the term, minus your discount; the expiry date does not change and renewals bill at the new rate. The disk grows at once and the machine restarts once with the new cores and memory. There is no downgrade path. The call holds until the resize and the restart are done (up to a few minutes); a retry that overlaps it answers 409 and charges nothing.

ParameterTypeDescription
id *string8-character server ID
plan_id *integerTarget plan, dearer than the current one, same location
preorderbooleanAccept a pre-ordered upgrade if the server's host has no room today (see below). Default false.
{"success": true, "data": {"message": "Upgrade applied. ...", "status": "applied", "new_plan": "Blackbox Plus", "new_plan_id": 113, "invoice_id": "9a4b0c21", "amount_charged": 7.63}}

Pre-ordered upgrades. An upgrade resizes the VM in place, so its own host must have the extra memory. When it does not, the call answers 409 with the price; send preorder: true to pay it now from the balance (balance method only) and queue the upgrade. It applies by itself when the room frees, the machine restarts once, and the response carries status queued with a queue_position. Pre-ordered upgrades are final. The server's pending_upgrade shows the wait.

On an encrypted machine the LUKS volume and the filesystem inside it do not grow by themselves: run cryptsetup resize and then resize2fs (or xfs_growfs) in the guest once it is back.

POST

?action=power&id={server_id}

Power actions on an active server.

ParameterTypeDescription
id *string8-character server ID
action *stringstart, stop (ACPI shutdown), restart, forcestop (cut the power)

stop and restart are ACPI events the guest has to act on. A machine parked at its LUKS prompt ignores them; the node cuts the power after 90 seconds, or use forcestop.

POST

?action=reinstall&id={server_id}

Rebuild the machine from a fresh image. This wipes the disk. Operating system, confidential mode, encryption and measured boot are all build-time properties, so this one call sets all four: what you do not send is off. Credentials are yours to supply; nothing is generated or returned. The rebuild runs in the background: status reads reinstalling until it is active again, usually within a few minutes. Addresses, VMID and MAC survive a reinstall.

ParameterTypeDescription
id *string8-character server ID
os *stringAn id from ?action=os
passwordstringNew root (or Administrator) password, 8 to 128 characters
ssh_keystringSSH public key(s), one per line, or ssh_keys as an array. One of password / ssh_key is required; fde and measured need a key.
snpbooleanConfidential computing on the rebuilt machine. Default false.
fdebooleanEncrypt the rebuilt disk. Default false.
measuredbooleanMeasured boot on the rebuilt machine. Default false.
{"success": true, "data": {"message": "Reinstall queued. ...", "status": "reinstalling", "os": "almalinux10", "snp": true, "fde": true, "measured": false}}
POST

?action=hostname&id={server_id}

Rename the VM. The node rewrites the machine's cloud-init seed, so Linux and BSD guests take the new name at their next boot, when cloud-init runs again; that run also regenerates the guest's SSH host keys, so expect a host-key warning afterwards. There is no agent, so nothing changes until that boot (restart_required: true in the response).

ParameterTypeDescription
id *string8-character server ID
hostname *string3-32 characters, letters, digits and hyphens
POST

?action=label&id={server_id}

A free-text note on the server, up to 64 characters, shown as label: your customer's reference, a ticket number, anything. Never sent to the node. Works in every status, pre-orders included. An empty label clears it.

ParameterTypeDescription
id *string8-character server ID
labelstringUp to 64 characters; empty to clear
POST

?action=rdns&id={server_id}

Set or delete the PTR record of one of the server's addresses. Mail servers reject senders without one. The record is written to our nameserver within a minute (status pending until then).

ParameterTypeDescription
id *string8-character server ID
ip *stringOne of the server's addresses, as GET ?action=rdns lists them
hostnamestringFully qualified domain name; empty deletes the record
{"success": true, "data": {"message": "PTR for 23.155.44.120 will resolve to mail.example.com within a minute.", "ip": "23.155.44.120", "hostname": "mail.example.com", "status": "pending"}}
POST

?action=ip&id={server_id}

Give an active Linux or BSD server a fresh IPv4 from the same pool. Charged at the flat ip_change_price from ?action=account, not discounted. The node rewrites the address and the anti-spoof rule, the machine restarts, and the Linux images come back on the new address by themselves (the cloud-init re-run also regenerates the SSH host keys); the old address stops passing traffic at once. Reverse DNS on the old address is dropped. The response carries new_ip, old_ip, netmask, gateway, restarted, invoice_id, amount_charged and a message.

ParameterTypeDescription
id *string8-character server ID

A machine that does not come back on the new address can be fixed from the panel console: the Overview tab shows the commands to run inside the guest.

POST

?action=terminate&id={server_id}

Destroy the server and release its addresses. No refund for the days left. A pre-order cannot be terminated: it is final and deploys when its slot frees. A failed row can be terminated: a failed deploy is simply forgotten, a failed reinstall's machine is torn down. The server answers 404 from then on (within a minute at most).

ParameterTypeDescription
id *string8-character server ID
POST

?action=deposit

Add funds to the account balance from your own tooling. The balance is shared with the Virtual Server API. Two methods:

  • card charges the card saved on the account and credits the balance in the same request. Needs a saved card (has_saved_card in ?action=account); cards that require a browser challenge are refused here and must be used on the website.
  • crypto opens an invoice and returns the address and amount to pay. The balance is credited when the network confirms the payment; poll ?action=invoice until status is completed, or watch balance in ?action=account. The payment_url is a normal payment page for the same invoice, with QR code, that needs no login.
ParameterTypeDescription
amount *numberUSD, 5 to 10000, two decimals
method *stringcard or crypto
currencystringCrypto only. One of BTC, XMR, LTC, ETH, BNB, TRX, USDT_ERC20, USDT_TRC20, USDT_BEP20, USDC_ERC20, USDC_BEP20. The error message for an unknown code lists what is currently accepted.
idempotency_keystringOptional, up to 64 characters, unique per account. Send one on every card deposit: a retry with the same key returns the original invoice (replayed: true) instead of charging again. Works for crypto too.
curl -X POST "https://servury.com/api/reseller.php?action=deposit" \
  -H "X-Reseller-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"amount": 100, "method": "crypto", "currency": "XMR"}'
{
    "success": true,
    "data": {
        "replayed": false,
        "invoice": {
            "id": "b71e04d9",
            "type": "balance_deposit",
            "method": "crypto",
            "status": "pending",
            "amount_usd": "100.00",
            "description": "Balance deposit via crypto: $100",
            "server_id": null,
            "created_at": 1789480000,
            "updated_at": 1789480000,
            "payment_url": "https://servury.com/cn/pay/9c1d.../",
            "crypto": {
                "currency": "XMR",
                "address": "4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge",
                "amount_crypto": "0.6210",
                "expires_at": 1789483600,
                "processor_status": "check"
            }
        },
        "balance": 42.50
    }
}

Errors: 402 when there is no saved card or the charge is declined (nothing is credited), 502 when the crypto invoice could not be opened.

GET

?action=invoice&id={invoice_id}

One of this account's invoices, by id: deposits, orders, renewals, upgrades, address changes. For a pending crypto deposit the response includes the same crypto block as ?action=deposit. server_id links a purchase to its server.

Statuses: pending (awaiting payment), completed (paid), refunded (a failed deploy or a refused change), cancelled (expired unpaid, or a declined card).

GET

?action=invoices

Invoice history, newest first, for the whole account (both lines). Use it to reconcile top-ups or rebuild a ledger: deposits credit the balance, invoices with method account_funds debit it. All filters are optional and combine.

ParameterTypeDescription
typestringbalance_deposit (alias deposits), server_create, server_renew, server_upgrade, ip_change
statusstringpending, completed, refunded, cancelled
methodstringcrypto, card, account_funds
server_idstringOnly the invoices of one server
since, untilintUnix timestamps, inclusive, applied to created_at
limit, offsetintPage size 1 to 200 (default 50) and offset; total in the response is the unpaged count
GET ?action=invoices&server_id=abc12345

{
    "success": true,
    "data": {
        "invoices": [
            {"id": "7d2e9a10", "type": "server_create", "method": "account_funds", "status": "completed",
             "amount_usd": "9.49", "description": "[API] Server deployment: Blackbox Core for 31 days",
             "server_id": "abc12345", "created_at": 1789564800, "updated_at": 1789564800}
        ],
        "total": 1,
        "limit": 50,
        "offset": 0,
        "balance": 33.01
    }
}

Error Responses

All errors return JSON with success: false and a message written to be shown to a human. The HTTP status tells your code what kind of error it is.

StatusMeaning
400Bad parameter: the message says which. Nothing was charged.
401Missing, unknown or inactive API key.
402Payment refused: insufficient balance, no saved card, card declined. Nothing was changed.
403The server is held for a terms of service violation.
404No such server, plan or invoice on this account.
405Wrong HTTP method.
409Wrong state for this action: still deploying, suspended, a pre-order, sold out without preorder, no room without preorder, or another request is changing the same server. Nothing was charged.
500Paid, but we could not record it. The money was returned to the balance (a card charge is credited to the balance and its id is in the message and on a refunded invoice). Contact support.
501Only available in the panel (the interactive console).
502The node or the nameserver did not do it. The charge, if any, was returned to the balance the same way.
{"success": false, "error": "Plan is sold out right now (3 order(s) waiting). Send \"preorder\": true to pay now and queue the order; ..."}

PHP Example

<?php
$apiKey = "your_api_key_here";
$baseUrl = "https://servury.com/api/reseller.php";

function apiRequest($endpoint, $apiKey, $method = 'GET', $data = null) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $endpoint);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "X-Reseller-Key: $apiKey",
        "Content-Type: application/json"
    ]);
    if ($method === 'POST') {
        curl_setopt($ch, CURLOPT_POST, true);
        if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    }
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true);
}

// Order a confidential, encrypted Debian 13 for 31 days
$order = apiRequest("$baseUrl?action=create", $apiKey, 'POST', [
    'plan_id'  => 108,
    'hostname' => 'cust-4411',
    'os'       => 'debian13',
    'days'     => 31,
    'ssh_key'  => 'ssh-ed25519 AAAA... ops@yourbrand',
    'snp'      => true,
    'fde'      => true,
]);
$serverId = $order['data']['server_id'];

// Wait for it to come up
do {
    sleep(15);
    $s = apiRequest("$baseUrl?action=server&id=$serverId", $apiKey)['data']['server'];
} while ($s['status'] === 'deploying');
echo "{$s['hostname']} is {$s['status']} at {$s['ipv4_address']} / {$s['ipv6_address']}\n";

// Reverse DNS for the customer's mail server
apiRequest("$baseUrl?action=rdns&id=$serverId", $apiKey, 'POST', [
    'ip' => $s['ipv4_address'], 'hostname' => 'mail.customer.example',
]);

// Renew for another month when their invoice is paid
apiRequest("$baseUrl?action=renew&id=$serverId", $apiKey, 'POST', ['days' => 31]);