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.
| status | Meaning |
|---|---|
deploying | Paid, being built. Addresses appear as soon as they are assigned. |
queued | A 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. |
active | Live. Every action is available. |
reinstalling | Being rebuilt (or queued for it). Wait for active. |
suspended | status_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. |
failed | status_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.
?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.
?action=locations
Locations that have Blackbox plans: id, name, city.
?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.
?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.
?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": []
}
}
?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.
?action=server&id={server_id}
One server in full.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-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.
?action=graphs&id={server_id}
CPU, memory and network series for an active server, sampled on the node (no agent in the guest).
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
timeframe | string | 1hr (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.
?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.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
bytes | integer | GET: how much of the tail to return, 1024 to 1048576 (default 65536) |
log * | boolean | POST: 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.
?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}
]
}
}
?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.
?action=create
$ PaidOrder 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.
| Parameter | Type | Description |
|---|---|---|
plan_id * | integer | Plan ID from ?action=plans |
hostname * | string | 3-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 * | string | An id from ?action=os |
days | integer | Term, 7 to 365 (default 31). Priced at retail_price / 31 * days minus your discount. |
password | string | Root (or Administrator) password, 8 to 128 characters. Required for Windows. |
ssh_key | string | One 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. |
snp | boolean | Confidential computing (AMD SEV-SNP). Linux images only. Default false. |
fde | boolean | LUKS full-disk encryption, unlocked over SSH in the initramfs. Debian, Ubuntu, Rocky Linux, AlmaLinux. Needs ssh_key. Default false. |
measured | boolean | Measured boot. Needs snp, fde and ssh_key. Default false. |
preorder | boolean | Accept 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.
?action=renew&id={server_id}
$ PaidExtend 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.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
days * | integer | 7 to 365 |
{"success": true, "data": {"message": "Server renewed.", "new_expiration": 1794678400, "invoice_id": "c01f3b77", "amount_charged": 9.49, "rate_monthly": 9.99, "status": "active"}}
?action=upgrade&id={server_id}
$ PaidMove 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.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
plan_id * | integer | Target plan, dearer than the current one, same location |
preorder | boolean | Accept 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.
?action=power&id={server_id}
Power actions on an active server.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
action * | string | start, 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.
?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.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
os * | string | An id from ?action=os |
password | string | New root (or Administrator) password, 8 to 128 characters |
ssh_key | string | SSH 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. |
snp | boolean | Confidential computing on the rebuilt machine. Default false. |
fde | boolean | Encrypt the rebuilt disk. Default false. |
measured | boolean | Measured boot on the rebuilt machine. Default false. |
{"success": true, "data": {"message": "Reinstall queued. ...", "status": "reinstalling", "os": "almalinux10", "snp": true, "fde": true, "measured": false}}
?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).
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
hostname * | string | 3-32 characters, letters, digits and hyphens |
?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.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
label | string | Up to 64 characters; empty to clear |
?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).
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
ip * | string | One of the server's addresses, as GET ?action=rdns lists them |
hostname | string | Fully 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"}}
?action=ip&id={server_id}
$ PaidGive 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.
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-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.
?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).
| Parameter | Type | Description |
|---|---|---|
id * | string | 8-character server ID |
?action=deposit
Add funds to the account balance from your own tooling. The balance is shared with the Virtual Server API. Two methods:
cardcharges the card saved on the account and credits the balance in the same request. Needs a saved card (has_saved_cardin ?action=account); cards that require a browser challenge are refused here and must be used on the website.cryptoopens an invoice and returns the address and amount to pay. The balance is credited when the network confirms the payment; poll ?action=invoice untilstatusiscompleted, or watchbalancein?action=account. Thepayment_urlis a normal payment page for the same invoice, with QR code, that needs no login.
| Parameter | Type | Description |
|---|---|---|
amount * | number | USD, 5 to 10000, two decimals |
method * | string | card or crypto |
currency | string | Crypto 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_key | string | Optional, 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.
?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).
?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.
| Parameter | Type | Description |
|---|---|---|
type | string | balance_deposit (alias deposits), server_create, server_renew, server_upgrade, ip_change |
status | string | pending, completed, refunded, cancelled |
method | string | crypto, card, account_funds |
server_id | string | Only the invoices of one server |
since, until | int | Unix timestamps, inclusive, applied to created_at |
limit, offset | int | Page 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.
| Status | Meaning |
|---|---|
400 | Bad parameter: the message says which. Nothing was charged. |
401 | Missing, unknown or inactive API key. |
402 | Payment refused: insufficient balance, no saved card, card declined. Nothing was changed. |
403 | The server is held for a terms of service violation. |
404 | No such server, plan or invoice on this account. |
405 | Wrong HTTP method. |
409 | Wrong 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. |
500 | Paid, 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. |
501 | Only available in the panel (the interactive console). |
502 | The 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]);







