~/api $

API reference

Run your servers from your own scripts: orders, power, firewalls, snapshots, reverse DNS and invoices. Every endpoint with examples in curl, JavaScript, Python and the rack4 CLI, and a Try it panel that uses your own token.

Base URL

Every endpoint lives under this URL. Requests and answers are JSON.

shell
https://my.rack4.io/api/v1

Authentication

Send an API token as a bearer token. Make one in the panel at https://my.rack4.io/account/api-tokens (the form asks for your password); it is shown only once, so save it. A read-only token can call every GET endpoint; anything that changes something needs a read-and-write token. A revoked, expired or unknown token gets 401. An account can hold 10 tokens at a time, and making one needs a confirmed email address. A token can be limited to address ranges when it is made; from any other address it gets 403 ip_not_allowed. The examples below assume these two variables.

shell
export RACK4_TOKEN="$(cat ~/.vps9-token)"
export API=https://my.rack4.io/api/v1
curl -s -H "Authorization: Bearer $RACK4_TOKEN" "$API/me"

Errors

An error is JSON with a stable code for scripts and a sentence for people. Any endpoint can answer unauthorized (401), insufficient_scope (403, the token lacks the scope), forbidden_role (403, your role on the account can't do this; see Teams), not_found (404, no such endpoint or object), method_not_allowed (405, the path exists but not with that method; the Allow header lists the ones it takes), rate_limited (429) and server_error (500, our fault). Any endpoint that takes a body can also answer invalid_json (400, the body isn't JSON or couldn't be read), body_too_large (413, bodies are 16 KB at most; 64 KB for POST /servers and POST /servers/:id/actions, which can carry a setup script), unsupported_media_type (415, see Conventions) and invalid_request (422, the body isn't a JSON object, has a field we don't know, or a field of the wrong type). Each endpoint below lists its own codes too. A 4xx means the request needs changing before you send it again; only 429, 500 and 503 are worth retrying as they are. A 503 means nothing changed; firewall_unavailable, snapshots_unavailable and orders_unavailable pass quickly, so try again in a moment.

shell
{ "error": { "code": "not_found", "message": "No such server.", "requestId": "req_7d2kq4m5x3n6p2r4s7t2v3w4" } }

Rate limits

Each token gets 60 requests a minute, and each account 120 a minute across all its tokens, so more tokens don't mean more requests. Past either you get 429 rate_limited with a Retry-After header in seconds. Two things have their own limits as well: 10 reverse DNS changes an hour, and 10 orders an hour (tries that fail count, and orders placed in the panel count too). Every answer to a call with a token says where you stand: X-RateLimit-Limit (60, or 120 once the account's limit is the nearer one), X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the minute starts again). GET /prices sends them too, for its own limit.

Request ids

Every answer has an X-Request-Id header (req_ and 24 letters and digits), and an error repeats it as error.requestId. We log each request under its id with the route, the status and the time it took (never the token or the body), so if something looks wrong, quote the id in a support ticket and we can find exactly that request.

shell
{ "error": { "code": "not_found", "message": "No such server.", "requestId": "req_7d2kq4m5x3n6p2r4s7t2v3w4" } }

Conventions

Ids, states, addresses and networks are plain strings. Sizes and amounts are integers: memoryMiB, diskGiB, totalCents. Times are ISO 8601 in UTC. Work that happens on the server answers 202 and finishes in the background: read the server again to follow it. Types are strict. A request with a body must say Content-Type: application/json (charset utf-8 if any; gzip and deflate compression are fine), or it gets 415 unsupported_media_type: curl -d without that header sends a form, which is refused rather than read as empty. The body must be a JSON object; a field we don't know, or a field of the wrong type (a number where a string goes, null, a list), gets 422 invalid_request and nothing changes. Ids are lowercase, exactly as the API gives them out.

Ordering and retries

POST /servers places an order and answers with an invoice; the server is built once that invoice is paid. So that a retry after a timeout can't order twice, send an Idempotency-Key header (up to 60 letters, digits, dots, colons, dashes or underscores; a UUID works) and keep it until you have an answer. The same key sent again with the same order answers with the order it already placed, even if you are now at the limit of unpaid orders or the plan is no longer on sale. The same key with a different order is refused with 422 idempotency_key_reused. Keys are case-sensitive: Order-A and order-a are two keys. Without the header every POST is a new order. Other endpoints ignore the header. Ordering needs a confirmed email address: until then POST /servers answers 403 email_unverified.

shell
export ORDER_KEY="$(uuidgen)"
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $ORDER_KEY" \
  -d '{"plan":"standard","image":"debian-12","hostname":"web-1","sshKeyIds":["key_m2q7x4abcdefgh2s"]}' \
  "$API/servers"

Webhooks

Instead of polling, register an HTTPS endpoint (up to 5 per account, on a public address) and we POST each event to it as JSON: { "id", "type", "createdAt", "data" }. Every delivery has the headers Rack4-Event, Rack4-Delivery and Rack4-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t, a dot and the raw body, keyed with the endpoint's signing secret (shown once when you make the endpoint or rotate it). Check it, and refuse a t more than 5 minutes from your clock. Answer with any 2xx within 10 seconds; anything else is retried after 1 minute, 5 minutes, 30 minutes, 2, 6 and 12 hours. Redirects are not followed. An endpoint that keeps failing for a day is turned off and you are told. The same event can arrive twice (a retry after a lost answer, a redelivery): use its id to skip repeats. Event types: server.ready, server.suspended, server.unsuspended, server.action_failed, snapshot.completed, snapshot.failed, backup.failed, backup.restored, invoice.issued, invoice.paid, invoice.overdue, payment.received, credit.added, ticket.replied, alert.triggered, alert.resolved, monitor.down, monitor.up, monitor.tls_expiring, and ping for a test event. Code samples: https://rack4.io/help/webhooks

Uptime monitors

Monitors check your own services from outside, from our control plane: http (a URL, its status code, an optional keyword, the TLS certificate's expiry), tcp (a host and port), ping (TCP reachability: ports 443, 80 and 22; we can't send ICMP), dns (a record through public resolvers) and heartbeat (your job calls its pingUrl; missed pings mean down). intervalS is 60, 120, 180, 240, 300 seconds (accounts without a server: 300). A failed check is confirmed by a second one 20 seconds later before the monitor is down; then you get one monitor.down notice (in the panel, by email when email is true, and to webhooks subscribed to it) and one monitor.up when it recovers. Only public addresses can be checked: private, loopback, link-local and metadata addresses are refused when you save and before every check. Checks send the user agent Rack4-Monitor/1.0. Status pages show the monitors you pick at https://rack4.io/s/<slug>. Guide: https://rack4.io/help/uptime-monitoring

Notification channels

Channels send the notifications you pick to Slack, Discord, Telegram, ntfy, Microsoft Teams or another email address (up to 10 per account), formatted for each service. categories is any of servers, billing, support, alerts, developer, monitors, deploy; security notices are never sent to a channel. A channel's URL, token, chat or address is kept encrypted and never returned: target shows a masked form. A Telegram channel starts pending with pairing links (open one to connect a chat, within 30 minutes); an email channel starts pending until the address confirms the link we email it. Failed sends are retried on the webhook schedule, and a channel failing for a day is turned off and you are told. Push devices are browsers with push on, added from the panel's notification settings. Guide: https://rack4.io/help/notification-channels

Teams

An account can have several people, each with a role: owner, admin, billing, technical or read only. A token belongs to the person who made it and acts on the account they had open in the panel when they made it; GET /me says which, and their role there. A token never does more than its maker's role allows now: an endpoint the role can't use answers 403 forbidden_role (a role lowered later applies at once), and a read-only member can only make read-only tokens. If the account requires two-factor authentication and the person has it off, every call answers 403 two_factor_required. Removing someone from the account revokes their tokens for it. Ownership transfer is panel-only: it needs the owner's password and 2FA code.

Catalog files

Without the API: https://rack4.io/plans.json has every plan's specs and live price, the add-ons and what every plan includes; https://rack4.io/locations.json lists where servers run. Both are plain JSON for scripts and comparison sites, cached for five minutes, and readable from any origin. A field is only ever added; schemaVersion changes if one is removed or retyped.

AI tools (MCP)

The API is also an MCP server, for Claude, Cursor, VS Code and other AI tools: https://my.rack4.io/mcp over Streamable HTTP with the same bearer token. Every tool call is an API request with the token's scopes, role, address limit and rate limits. A read-only token sees only the read tools; ordering and add-on changes answer with the price until called with confirm: true, and reinstall, delete and snapshot revert or delete also need the server's hostname typed back. Setup for each tool: https://rack4.io/help/mcp.

Prices

Prices

GETGET /prices

no token · answers 200 · getPrices

What is on sale and what it costs, in integer US cents: each plan a month and 12 months at once (yearlyCents; null while a plan has no yearly price), and each add-on per step (step is in its unit: GB, TB, or a count) with its caps per plan. Needs no token (nor does GET /status). 60 requests a minute per address.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/prices"
answer 200
{
  "currency": "USD",
  "plans": [
    {
      "id": "standard",
      "name": "Standard",
      "vcpu": 2,
      "ramGb": 4,
      "nvmeGb": 80,
      "transferTb": 4,
      "monthlyCents": 1699,
      "yearlyCents": 16900
    }
  ],
  "addons": [
    {
      "key": "ram_gb",
      "name": "Extra RAM",
      "unit": "GB",
      "step": 1,
      "caps": {
        "budget": 2,
        "standard": 4,
        "extreme": 8
      },
      "monthlyCents": 350,
      "yearlyCents": 3500
    }
  ]
}

Status

Service status

GETGET /status

scope read · answers 200 · serviceStatus

What rack4.io/status shows, as JSON: the overall level and each component's (operational, maintenance, degraded, partial_outage or major_outage) with its uptime over 90 days in percent (null before there is data), open incidents and maintenance in progress or scheduled, each with its updates newest first. Needs no token; 60 requests a minute per address; cached for 30 seconds. The same document is at https://rack4.io/status.json.

Errors

status_unavailable

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/status"
answer 200
{
  "status": {
    "level": "degraded",
    "description": "Some systems are degraded"
  },
  "updatedAt": "2026-10-21T14:05:00.000Z",
  "page": "https://rack4.io/status",
  "components": [
    {
      "id": "panel",
      "name": "Control panel & API",
      "status": "operational",
      "uptime90d": 99.98
    },
    {
      "id": "billing",
      "name": "Billing and payments",
      "status": "operational",
      "uptime90d": 100
    },
    {
      "id": "nyc",
      "name": "New York",
      "status": "degraded",
      "uptime90d": 99.95
    },
    {
      "id": "network",
      "name": "Network",
      "status": "operational",
      "uptime90d": 100
    }
  ],
  "incidents": [
    {
      "id": "inc_t4q2m7abcdefgh2v",
      "kind": "incident",
      "title": "Slow disk on some New York servers",
      "impact": "minor",
      "status": "monitoring",
      "components": [
        "nyc"
      ],
      "createdAt": "2026-10-21T13:10:00.000Z",
      "updatedAt": "2026-10-21T14:00:00.000Z",
      "startsAt": null,
      "endsAt": null,
      "resolvedAt": null,
      "url": "https://rack4.io/status/incidents/inc_t4q2m7abcdefgh2v",
      "updates": [
        {
          "at": "2026-10-21T14:00:00.000Z",
          "status": "monitoring",
          "body": "A failed drive was replaced; the array is rebuilding and speeds are back to normal."
        },
        {
          "at": "2026-10-21T13:10:00.000Z",
          "status": "investigating",
          "body": "Some servers in New York see slow disk writes. We are looking into it."
        }
      ]
    }
  ],
  "maintenance": []
}

Account

Who am I

GETGET /me

scope read · answers 200 · getMe

The person the token belongs to, the account it acts on with their role there (see Teams), and the token itself: its id, name, scopes and expiry (null: never).

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/me"
answer 200
{
  "id": "usr_a2b3c4d5e6f7g2h3",
  "email": "you@example.com",
  "emailVerified": true,
  "createdAt": "2026-10-01T09:00:00.000Z",
  "account": {
    "id": "usr_a2b3c4d5e6f7g2h3",
    "name": "you@example.com",
    "role": "owner"
  },
  "token": {
    "id": "tok_q4m7x2abcdefgh2r",
    "name": "laptop CLI",
    "scopes": [
      "read",
      "write"
    ],
    "expiresAt": "2027-01-19T09:00:00.000Z"
  }
}

SSH keys

List SSH keys

GETGET /ssh-keys

scope read · answers 200 · listSshKeys

The SSH keys on your account, oldest first.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/ssh-keys"
answer 200
{
  "data": [
    {
      "id": "key_m2q7x4abcdefgh2s",
      "name": "laptop",
      "type": "ssh-ed25519",
      "fingerprint": "SHA256:Zx9eQ1nO4sV7kq2Lr8wYc3TmA5uBf6Hd0jPgXyN2Ke4",
      "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… you@laptop",
      "createdAt": "2026-10-01T09:05:00.000Z"
    }
  ]
}

Add an SSH key

POSTPOST /ssh-keys

scope write · answers 201 · addSshKey

Adds a public key (ed25519, ECDSA or RSA of 2048 bits or more). You pick keys when you order a server, and a reinstall puts all of the account's keys on it.

Parameters

namebody · string
A label, up to 200 characters. Optional.
publicKeybody · string · required
The one line of your .pub file.

Errors

invalid_requestinvalid_nameinvalid_keytoo_many_keysduplicate_key

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"laptop","publicKey":"your public key here: the one line in ~/.ssh/id_ed25519.pub"}' \
  "$API/ssh-keys"
answer 201
{
  "id": "key_m2q7x4abcdefgh2s",
  "name": "laptop",
  "type": "ssh-ed25519",
  "fingerprint": "SHA256:Zx9eQ1nO4sV7kq2Lr8wYc3TmA5uBf6Hd0jPgXyN2Ke4",
  "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… you@laptop",
  "createdAt": "2026-10-09T12:00:00.000Z"
}

Delete an SSH key

DELETEDELETE /ssh-keys/:id

scope write · answers 204 · deleteSshKey

Removes the key from your account. Servers keep the keys they already have.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/ssh-keys/key_m2q7x4abcdefgh2s"

204: no body.

Servers

List images

GETGET /images

scope read · answers 200 · listImages

The operating systems you can order or reinstall, in the order form's order. image in POST /servers and in a reinstall takes an id, or a key (the newest image of that system, e.g. ubuntu-24.04). family (debian, rhel or arch, or null) decides which one-click apps an image can take.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/images"
answer 200
{
  "data": [
    {
      "id": "ubuntu-24.04-20261009",
      "key": "ubuntu-24.04",
      "name": "Ubuntu 24.04 LTS",
      "os": "ubuntu",
      "version": "24.04",
      "family": "debian"
    }
  ]
}

List one-click apps

GETGET /apps

scope read · answers 200 · listApps

The one-click apps a new server or a reinstall can install on first boot: app in POST /servers and in a reinstall takes an id. families lists the image families (GET /images) each runs on, minPlan the smallest plan it runs well on, ports what it listens on publicly, and url its page with the steps it leaves to you.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/apps"
answer 200
{
  "data": [
    {
      "id": "docker",
      "name": "Docker",
      "category": "containers",
      "summary": "Docker Engine and the Compose plugin from Docker's own repository.",
      "families": [
        "debian",
        "rhel"
      ],
      "minPlan": "budget",
      "ports": [],
      "url": "https://rack4.io/apps/docker"
    }
  ]
}

List servers

GETGET /servers

scope read · answers 200 · listServers

Your servers, newest first. Deleted ones are left out. label is the name you gave it in the panel (null: none; the hostname is its name) and tags its tags.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers"
answer 200
{
  "data": [
    {
      "id": "srv_k3j5m2abcdefgh2q",
      "hostname": "web-1",
      "plan": "standard",
      "image": "debian-12",
      "state": "running",
      "pendingAction": null,
      "ipv4": "203.0.113.10",
      "ipv6": "2001:db8:10::2",
      "vcpus": 2,
      "memoryMiB": 4096,
      "diskGiB": 80,
      "createdAt": "2026-10-09T12:00:00.000Z",
      "stateSince": "2026-10-09T12:01:00.000Z",
      "app": "docker",
      "deletionProtected": false,
      "purgeAt": null,
      "label": "Shop front",
      "tags": [
        "prod",
        "web"
      ]
    }
  ]
}

Show a server

GETGET /servers/:id

scope read · answers 200 · getServer

One server, its name, tags and private notes, and its recent events.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q"
answer 200
{
  "id": "srv_k3j5m2abcdefgh2q",
  "hostname": "web-1",
  "plan": "standard",
  "image": "debian-12",
  "state": "running",
  "pendingAction": null,
  "ipv4": "203.0.113.10",
  "ipv6": "2001:db8:10::2",
  "vcpus": 2,
  "memoryMiB": 4096,
  "diskGiB": 80,
  "createdAt": "2026-10-09T12:00:00.000Z",
  "stateSince": "2026-10-09T12:01:00.000Z",
  "app": "docker",
  "deletionProtected": false,
  "purgeAt": null,
  "label": "Shop front",
  "tags": [
    "prod",
    "web"
  ],
  "notes": "Runs the shop. Deploys from main.",
  "events": [
    {
      "at": "2026-10-09T12:01:00.000Z",
      "actor": "system",
      "action": "create.done",
      "detail": null
    }
  ]
}

Name, tag or annotate a server

PUTPUT /servers/:id/details

scope write · answers 200 · nameTagOrAnnotateServer

Sets the server's display name (label: up to 63 characters, or null for none), its tags (up to 10, each 1 to 24 lowercase letters, digits, dots, dashes, underscores or colons; duplicates are dropped) and private notes (plain text, up to 2,000 characters). Each field you send replaces the stored one; the others are kept. It doesn't change the hostname inside the server: that is set on the first boot (change it there with hostnamectl).

Parameters

idpath · string · required
labelbody · string
tagsbody · list of string

Errors

invalid_requestinvalid_labelinvalid_tagsinvalid_notes

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"label":"Shop front","tags":["prod","web"]}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/details"
answer 200
{
  "label": "Shop front",
  "tags": [
    "prod",
    "web"
  ],
  "notes": "Runs the shop. Deploys from main."
}

Order a server

POSTPOST /servers

scope write · answers 202 · orderServer

Places an order and answers with the invoice to pay at payUrl; the server is built once it is paid. plan is budget, standard or extreme. You can have 3 unpaid orders at a time and try 10 orders an hour. Send an Idempotency-Key header to make retries safe. Your account's email must be confirmed (403 email_unverified until it is). If orders are paused, this answers 403 staff_only and orders nothing. Optional, installed on first boot: app (a one-click app id such as docker, wordpress or wireguard; it must suit the image's family and the plan), packages (up to 20 distribution package names) and setupScript (up to 16 KiB, run once as root after them; its output goes to /var/log/vps9-setup.log and its exit code to /var/lib/vps9-setup.exit). The script is stored encrypted and never shown back; GET answers only app. term is "month" (the default) or "year": 12 months at once at the yearly price. addons adds resources on top of the plan, in their units: ram_gb, disk_gb (steps of 20), transfer_tb, ipv4 and vcpu, each up to the plan's cap (GET /prices lists them); they are billed for the same term. backups: 1 orders it with off-site backups (priced per plan). restoreFrom (optional): one of your backups (bkp_…), restored into the new server once it is built; the plan's disk (with disk_gb) must be at least the backup's. rootPassword (optional): "generate" makes a random root password and answers with it in rootPassword, this once (it is never stored, so a retry with the same Idempotency-Key can't show it again); or send your own (12 to 128 printable ASCII characters, not a common password). Only its SHA-512 crypt hash reaches the server. With a root password, sshKeyIds may be empty; SSH keys stay the safer way in, and with a key root's password works on the serial console only. promoCode (optional): a promo code; its discount is a line on the invoice. A code that can't be used for this order (unknown, expired, used up, for other plans or yearly billing only, for new customers only) is 422 invalid_promo_code with the reason, and nothing is ordered; 10 wrong codes an hour, with the order form. quantity (optional, 1 to 10, default 1): that many identical servers on one invoice, each with its own order, built once the invoice is paid. Each is named from hostname, numbered: web gives web-1, web-2...; a name ending in a number counts on from it (node09 gives node09, node10); a domain is kept (web-1.example.com). Or, with a quantity over 1, send hostnames, one per server (exactly quantity of them, all different; hostname may then be left out); one server is named by hostname, which it needs. A name that isn't a valid hostname is 422 invalid_hostname naming it. orders in the answer lists every server's order id and hostname (order is the first). A promo code's discount is on the whole invoice; a code that takes a fixed amount off each of the first months is for one server at a time. restoreFrom needs quantity 1. Each order counts once against the 3 unpaid orders, however many servers it has. A retry with the same Idempotency-Key must have the same quantity and names.

Parameters

Idempotency-Keyheader · string
Makes a retry safe: the same key with the same order answers with the order it already placed; with a different order, 422 idempotency_key_reused. Up to 60 letters, digits, dots, colons, dashes or underscores. Case-sensitive.
planbody · string · required
budget, standard or extreme.
imagebody · string · required
An image id or an OS key like ubuntu-24.04 (GET /images).
hostnamebody · string
The server's hostname (needed for one server); with a quantity over 1, the base each server is numbered from (web gives web-1, web-2...), which may be left out when hostnames is sent.
quantitybody · integer
How many identical servers to order on one invoice, 1 to 10. Default 1.
hostnamesbody · list of string
With a quantity over 1 only: one hostname per server (exactly quantity, all different), instead of numbering hostname.
sshKeyIdsbody · list of string · required
At least one key id from GET /ssh-keys.
appbody · string
A one-click app id, like docker.
packagesbody · list of string
setupScriptbody · string
Run once as root on first boot. Up to 16 KiB; stored encrypted, never shown back.
rootPasswordbody · string
"generate" (the answer carries it, once) or your own, 12 or more characters. Left out: no password, SSH keys only. Only its hash reaches the server.
termbody · month | year
Default month.
addonsbody · object
Add-on totals by key, in their units (GB, TB or a count).
promoCodebody · string or null
A promo code, like SPRING25; its discount is a line on the invoice. null or left out: none.

Errors

staff_onlyemail_unverifiedinvalid_requestinvalid_idempotency_keyinvalid_quantityinvalid_planinvalid_terminvalid_addoninvalid_imageinvalid_hostnamessh_key_requiredinvalid_ssh_keyinvalid_root_passwordinvalid_appapp_unsupported_osapp_needs_bigger_planinvalid_packagesinvalid_setup_scriptidempotency_key_reusedtoo_many_open_ordersrate_limitedorders_unavailableinvalid_promo_codeinvalid_restorebackup_not_foundbackup_too_large

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"plan":"standard","image":"ubuntu-24.04","hostname":"web-1","sshKeyIds":["key_m2q7x4abcdefgh2s"],"app":"docker","packages":["htop"],"setupScript":"docker run -d --restart unless-stopped -p 80:80 nginx","term":"year","addons":{"ram_gb":2,"disk_gb":40,"transfer_tb":0,"ipv4":1,"vcpu":0},"promoCode":"SPRING25"}' \
  "$API/servers"
answer 202
{
  "order": {
    "id": "ord_x2m4q7abcdefgh2u"
  },
  "orders": [
    {
      "id": "ord_x2m4q7abcdefgh2u",
      "hostname": "web-1"
    }
  ],
  "invoice": {
    "id": "inv_q4m7x2abcdefgh2t",
    "number": "R4-000042",
    "kind": "order",
    "status": "open",
    "currency": "USD",
    "subtotalCents": 1200,
    "creditAppliedCents": 0,
    "totalCents": 1200,
    "issuedAt": "2026-10-09T12:00:00.000Z",
    "dueAt": "2026-10-09T12:00:00.000Z",
    "paidAt": null,
    "voidedAt": null,
    "serverId": null,
    "term": "month"
  },
  "payUrl": "https://my.rack4.io/billing/invoices/inv_q4m7x2abcdefgh2t"
}

Start, stop, reboot, reinstall or delete

POSTPOST /servers/:id/actions

scope write · answers 202 · serverAction

Queues one action: start, stop, reboot, reinstall or delete. A server runs one action at a time. Reinstall wipes the disk and puts your account's SSH keys on it; delete is final. The API doesn't ask you to confirm, so check the id before you send either. A reinstall installs the same image, app, packages and setup script as last time, unless you send image (another image id or key), app (another app, or null for none), packages (replaces them) or setupScript (replaces it; null or "" removes it). A reinstall also takes rootPassword: "generate" (answered once in rootPassword) or your own, as for an order; without it root has no password. Other actions take only action.

Parameters

idpath · string · required
actionbody · start | stop | reboot | reinstall | delete · required
imagebody · string
reinstall only: another image id or key.
appbody · string or null
reinstall only: another app, or null for none.
packagesbody · list of string
reinstall only: replaces the packages.
setupScriptbody · string or null
reinstall only: replaces the script; null or "" removes it.

Errors

invalid_actionaction_pendinginvalid_stateprotectedssh_key_requiredinvalid_root_passwordimage_unavailableinvalid_imageinvalid_appapp_unsupported_osapp_needs_bigger_planinvalid_packagesinvalid_setup_script

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"reboot"}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/actions"
answer 202
{
  "id": "srv_k3j5m2abcdefgh2q",
  "hostname": "web-1",
  "plan": "standard",
  "image": "debian-12",
  "state": "running",
  "pendingAction": "reboot",
  "ipv4": "203.0.113.10",
  "ipv6": "2001:db8:10::2",
  "vcpus": 2,
  "memoryMiB": 4096,
  "diskGiB": 80,
  "createdAt": "2026-10-09T12:00:00.000Z",
  "stateSince": "2026-10-09T12:01:00.000Z",
  "app": "docker",
  "deletionProtected": false,
  "purgeAt": null
}

Turn deletion protection on

POSTPOST /servers/:id/lock

scope write · answers 200 · turnDeletionProtectionOn

Locks the server against deletion: while it is on, delete, reinstall and going back to a snapshot answer 409 protected (in the panel, the API and the CLI). Power actions, snapshots and everything else work as before. Billing and our staff can still act on the server. No body.

Parameters

idpath · string · required

Errors

invalid_state

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/lock"
answer 200
{
  "id": "srv_k3j5m2abcdefgh2q",
  "hostname": "web-1",
  "plan": "standard",
  "image": "debian-12",
  "state": "running",
  "pendingAction": null,
  "ipv4": "203.0.113.10",
  "ipv6": "2001:db8:10::2",
  "vcpus": 2,
  "memoryMiB": 4096,
  "diskGiB": 80,
  "createdAt": "2026-10-09T12:00:00.000Z",
  "stateSince": "2026-10-09T12:01:00.000Z",
  "app": "docker",
  "deletionProtected": true,
  "purgeAt": null
}

Turn deletion protection off

POSTPOST /servers/:id/unlock

scope write · answers 200 · turnDeletionProtectionOff

Unlocks the server. Send your account password, and code (from your authenticator app, or a recovery code) when the account has one. Wrong answers count toward the same limits as the panel's forms. An account that uses passkeys without an authenticator app gets 403 unlock_in_panel: unlock it in the panel, where the passkey can confirm it's you.

Parameters

idpath · string · required
passwordbody · string
codebody · string

Errors

invalid_requestwrong_passwordcode_requiredwrong_codeunlock_in_panelrate_limitedinvalid_state

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"password":"your account password","code":"123456"}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/unlock"
answer 200
{
  "id": "srv_k3j5m2abcdefgh2q",
  "hostname": "web-1",
  "plan": "standard",
  "image": "debian-12",
  "state": "running",
  "pendingAction": null,
  "ipv4": "203.0.113.10",
  "ipv6": "2001:db8:10::2",
  "vcpus": 2,
  "memoryMiB": 4096,
  "diskGiB": 80,
  "createdAt": "2026-10-09T12:00:00.000Z",
  "stateSince": "2026-10-09T12:01:00.000Z",
  "app": "docker",
  "deletionProtected": false,
  "purgeAt": null
}

Show the power schedule

GETGET /servers/:id/power-schedule

scope read · answers 200 · showPowerSchedule

The server's power schedule, or null when it has none: a reboot (daily or weekly) and off hours (stopped at stopHour on each of days, started again at startHour, the next morning when it is earlier), in timezone. next has when each runs next (within 5 minutes after the hour); last is the latest run: ok, failed, skipped (with why) or running. heldOff is true while the schedule has the server stopped.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/power-schedule"
answer 200
{
  "schedule": {
    "timezone": "Europe/Berlin",
    "reboot": {
      "frequency": "weekly",
      "hour": 4,
      "weekday": "sun"
    },
    "offHours": {
      "stopHour": 20,
      "startHour": 8,
      "days": [
        "mon",
        "tue",
        "wed",
        "thu",
        "fri"
      ]
    },
    "next": {
      "reboot": "2026-10-25T03:02:41.000Z",
      "stop": "2026-10-21T18:02:41.000Z",
      "start": "2026-10-22T06:02:41.000Z"
    },
    "last": {
      "action": "start",
      "at": "2026-10-21T06:02:41.000Z",
      "status": "ok",
      "detail": null
    },
    "heldOff": false
  }
}

Set the power schedule

PUTPUT /servers/:id/power-schedule

scope write · answers 200 · setPowerSchedule

Sets the schedule, replacing any. timezone: an IANA name; reboot: null or { frequency: daily or weekly, hour: 0 to 23, weekday: sun to sat (weekly) }; offHours: null or { stopHour, startHour: 0 to 23, different, days: the weekdays the off hours start on }. At least one of reboot and offHours. A reboot runs only on a running server; the stop is a clean shutdown (forced after a minute); the start at the end of the off hours runs only if the schedule stopped it. A suspended server, one in rescue or one busy for over 30 minutes is skipped until the next time.

Parameters

idpath · string · required
timezonebody · string
rebootbody · object
offHoursbody · object

Errors

invalid_requestinvalid_schedule

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"timezone":"Europe/Berlin","reboot":{"frequency":"weekly","hour":4,"weekday":"sun"},"offHours":{"stopHour":20,"startHour":8,"days":["mon","tue","wed","thu","fri"]}}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/power-schedule"
answer 200
{
  "schedule": {
    "timezone": "Europe/Berlin",
    "reboot": {
      "frequency": "weekly",
      "hour": 4,
      "weekday": "sun"
    },
    "offHours": {
      "stopHour": 20,
      "startHour": 8,
      "days": [
        "mon",
        "tue",
        "wed",
        "thu",
        "fri"
      ]
    },
    "next": {
      "reboot": "2026-10-25T03:02:41.000Z",
      "stop": "2026-10-21T18:02:41.000Z",
      "start": "2026-10-22T06:02:41.000Z"
    },
    "last": {
      "action": "start",
      "at": "2026-10-21T06:02:41.000Z",
      "status": "ok",
      "detail": null
    },
    "heldOff": false
  }
}

Turn the power schedule off

DELETEDELETE /servers/:id/power-schedule

scope write · answers 200 · turnPowerScheduleOff

Removes the schedule. A server the schedule had stopped stays stopped: start it yourself. No body.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/power-schedule"
answer 200
{
  "schedule": null
}

Add-ons

Show add-ons

GETGET /servers/:id/addons

scope read · answers 200 · getAddons

The server's add-ons now, and what the next renewal bills (lower where you lowered one), its term, its extra IPv4 addresses (configure them on the server: a reinstall does it for you) and rebootPending: new vCPU or RAM is set and waits for a reboot from the panel or the API (a reboot from inside the server keeps the old size). restartAt: when a smaller size a renewal set is applied by a restart we make (72 hours after the renewal is paid), unless you reboot sooner; null when none is planned.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/addons"
answer 200
{
  "billed": true,
  "term": "month",
  "nextTerm": "month",
  "paidUntil": "2026-11-09T12:00:00.000Z",
  "addons": {
    "ram_gb": 2,
    "disk_gb": 40,
    "transfer_tb": 0,
    "ipv4": 1,
    "vcpu": 0
  },
  "nextRenewal": {
    "ram_gb": 1,
    "disk_gb": 40,
    "transfer_tb": 0,
    "ipv4": 1,
    "vcpu": 0
  },
  "extraIpv4": [
    {
      "address": "203.0.113.20",
      "prefix": 24,
      "gateway": "203.0.113.1",
      "state": "active"
    }
  ],
  "rebootPending": false,
  "restartAt": null
}

Change add-ons

PUTPUT /servers/:id/addons

scope write · answers 202 · changeAddons

Sets add-on totals; keys you leave out stay as they are. More is invoiced now for the days left in the paid period (and the next period too when its renewal invoice is already out), at least $0.50, and applied once paid: answer invoice and payUrl. Less takes effect from the next renewal invoice, with no refund (scheduled); once that invoice is issued at the lower size, taking the decrease back waits until it is paid (renewal_issued). Less vCPU or RAM is applied at your next reboot from the panel or the API, or by a restart we make 72 hours after the renewal is paid. A disk never shrinks (disk_shrink). apply is "now" (the default: new vCPU or RAM restarts the server once paid) or "reboot" (it waits for your next reboot). One unpaid add-on invoice per server at a time.

Parameters

idpath · string · required
addonsbody · AddonQuantities · required
applybody · now | reboot
Default now.

Errors

invalid_requestinvalid_addondisk_shrinknothing_changedno_billingcancellingperiod_endedaddon_invoice_openaddons_unavailableno_roomnode_vcpu_limitnode_ipv4_limitno_ipv4renewal_issued

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"addons":{"ram_gb":4},"apply":"reboot"}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/addons"
answer 202
{
  "invoice": {
    "id": "inv_q4m7x2abcdefgh2t",
    "number": "R4-000042",
    "kind": "addon",
    "status": "open",
    "currency": "USD",
    "subtotalCents": 474,
    "creditAppliedCents": 0,
    "totalCents": 474,
    "issuedAt": "2026-11-02T12:00:00.000Z",
    "dueAt": "2026-11-09T12:00:00.000Z",
    "paidAt": null,
    "voidedAt": null,
    "serverId": "srv_k3j5m2abcdefgh2q",
    "term": "month"
  },
  "payUrl": "https://my.rack4.io/billing/invoices/inv_q4m7x2abcdefgh2t",
  "scheduled": []
}

Firewall

Show the firewall

GETGET /servers/:id/firewall

scope read · answers 200 · getFirewall

The inbound firewall: the default policy, the rules in order (the first match wins) and whether the latest change is applied. A failed or degraded (partly applied) status comes with a message saying what of the rules shown is in effect. The ETag header (like "r3") changes with every save, not when the status changes; send it back as If-Match on a PUT so you never overwrite a change you haven't seen. This endpoint ignores If-None-Match and always sends the firewall, so you never miss a status change.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/firewall"
answer 200
{
  "policy": "deny",
  "rules": [
    {
      "action": "allow",
      "protocol": "tcp",
      "ports": "22",
      "source": "198.51.100.0/24"
    },
    {
      "action": "allow",
      "protocol": "tcp",
      "ports": "80-443",
      "source": null
    }
  ],
  "status": "applied",
  "errorCode": null,
  "updatedAt": "2026-10-09T13:05:00.000Z"
}

Replace the firewall

PUTPUT /servers/:id/firewall

scope write · answers 202 · setFirewall

Sets the default policy and the whole rule list, up to 50 rules. action is allow or deny; protocol is tcp, udp, icmp or any; ports (tcp and udp only) is one port or a range like 80-443, or null; source is an address or network, or null for anywhere. Replies to connections the server opened always come back in. With If-Match (the ETag from a GET; a weak W/"r3" from a proxy works too), the save happens only if nobody saved since; otherwise it is refused with 412 firewall_changed and nothing changes. If-Match: * saves over any version. Any other If-Match value is refused with 400 invalid_if_match. The answer carries the new ETag.

Parameters

idpath · string · required
If-Matchheader · string
The ETag from GET (like "r3"): save only if nobody saved since, or 412 firewall_changed. * saves over any version.
policybody · allow | deny · required
rulesbody · list of FirewallRule · required

Errors

invalid_firewallinvalid_if_matchinvalid_statefirewall_changedfirewall_unavailable

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"policy":"deny","rules":[{"action":"allow","protocol":"tcp","ports":"22","source":"198.51.100.0/24"},{"action":"allow","protocol":"tcp","ports":"80-443","source":null}]}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/firewall"
answer 202
{
  "policy": "deny",
  "rules": [
    {
      "action": "allow",
      "protocol": "tcp",
      "ports": "22",
      "source": "198.51.100.0/24"
    },
    {
      "action": "allow",
      "protocol": "tcp",
      "ports": "80-443",
      "source": null
    }
  ],
  "status": "applying",
  "errorCode": null,
  "updatedAt": "2026-10-09T13:05:00.000Z"
}

Snapshots

List snapshots

GETGET /servers/:id/snapshots

scope read · answers 200 · listSnapshots

The server's snapshots, oldest first. A server keeps at most 3.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/snapshots"
answer 200
{
  "data": [
    {
      "id": "snap_7xq2m4abcdefgh2r",
      "label": "before-upgrade",
      "state": "ready",
      "createdAt": "2026-10-09T13:00:00.000Z",
      "automatic": false
    }
  ]
}

Take a snapshot

POSTPOST /servers/:id/snapshots

scope write · answers 202 · createSnapshot

Takes a disk snapshot. name labels it: up to 40 letters, digits, spaces, dots, dashes or underscores. A running server shuts down for it and starts again, usually within a minute.

Parameters

idpath · string · required
namebody · string

Errors

invalid_requestinvalid_labellimit_reachedaction_pendinginvalid_statesnapshots_unavailable

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"before-upgrade"}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/snapshots"
answer 202
{
  "snapshot": {
    "id": "snap_7xq2m4abcdefgh2r",
    "label": "before-upgrade",
    "state": "creating",
    "createdAt": "2026-10-09T13:00:00.000Z",
    "automatic": false
  },
  "server": {
    "id": "srv_k3j5m2abcdefgh2q",
    "hostname": "web-1",
    "plan": "standard",
    "image": "debian-12",
    "state": "running",
    "pendingAction": "snapshot",
    "ipv4": "203.0.113.10",
    "ipv6": "2001:db8:10::2",
    "vcpus": 2,
    "memoryMiB": 4096,
    "diskGiB": 80,
    "createdAt": "2026-10-09T12:00:00.000Z",
    "stateSince": "2026-10-09T12:01:00.000Z",
    "app": "docker",
    "deletionProtected": false,
    "purgeAt": null
  }
}

Revert to a snapshot

POSTPOST /servers/:id/snapshots/:snapshotId/revert

scope write · answers 202 · revertSnapshot

Puts the disk back the way it was in the snapshot, then starts the server. Everything written since the snapshot is lost. No body.

Parameters

idpath · string · required
snapshotIdpath · string · required

Errors

snapshot_not_foundnot_readyaction_pendinginvalid_statesnapshots_unavailable

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/snapshots/snap_7xq2m4abcdefgh2r/revert"
answer 202
{
  "snapshot": {
    "id": "snap_7xq2m4abcdefgh2r",
    "label": "before-upgrade",
    "state": "ready",
    "createdAt": "2026-10-09T13:00:00.000Z",
    "automatic": false
  },
  "server": {
    "id": "srv_k3j5m2abcdefgh2q",
    "hostname": "web-1",
    "plan": "standard",
    "image": "debian-12",
    "state": "running",
    "pendingAction": "revert",
    "ipv4": "203.0.113.10",
    "ipv6": "2001:db8:10::2",
    "vcpus": 2,
    "memoryMiB": 4096,
    "diskGiB": 80,
    "createdAt": "2026-10-09T12:00:00.000Z",
    "stateSince": "2026-10-09T12:01:00.000Z",
    "app": "docker",
    "deletionProtected": false,
    "purgeAt": null
  }
}

Delete a snapshot

DELETEDELETE /servers/:id/snapshots/:snapshotId

scope write · answers 202 · deleteSnapshot

Deletes the snapshot. A running server shuts down briefly while it is removed.

Parameters

idpath · string · required
snapshotIdpath · string · required

Errors

snapshot_not_foundnot_readyaction_pendinginvalid_statesnapshots_unavailable

curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/snapshots/snap_7xq2m4abcdefgh2r"
answer 202
{
  "snapshot": {
    "id": "snap_7xq2m4abcdefgh2r",
    "label": "before-upgrade",
    "state": "deleting",
    "createdAt": "2026-10-09T13:00:00.000Z",
    "automatic": false
  },
  "server": {
    "id": "srv_k3j5m2abcdefgh2q",
    "hostname": "web-1",
    "plan": "standard",
    "image": "debian-12",
    "state": "running",
    "pendingAction": "snapshot_delete",
    "ipv4": "203.0.113.10",
    "ipv6": "2001:db8:10::2",
    "vcpus": 2,
    "memoryMiB": 4096,
    "diskGiB": 80,
    "createdAt": "2026-10-09T12:00:00.000Z",
    "stateSince": "2026-10-09T12:01:00.000Z",
    "app": "docker",
    "deletionProtected": false,
    "purgeAt": null
  }
}

Show the automatic snapshot schedule

GETGET /servers/:id/snapshot-schedule

scope read · answers 200 · showAutomaticSnapshotSchedule

The server's automatic snapshot schedule, or null when automatic snapshots are off. nextRunAt is when the next one starts (within 30 minutes after the hour); lastStatus is running, ok, failed or skipped, with the reason in lastError.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/snapshot-schedule"
answer 200
{
  "schedule": {
    "frequency": "daily",
    "hour": 3,
    "weekday": null,
    "timezone": "Europe/Berlin",
    "keep": 3,
    "nextRunAt": "2026-10-22T01:17:00.000Z",
    "lastRunAt": "2026-10-21T01:17:00.000Z",
    "lastStatus": "ok",
    "lastError": null
  }
}

Set the automatic snapshot schedule

PUTPUT /servers/:id/snapshot-schedule

scope write · answers 200 · setAutomaticSnapshotSchedule

Turns automatic snapshots on, or changes the schedule. frequency: daily or weekly; hour: 0 to 23 in timezone (an IANA name, default UTC); weekday (weekly only): 0 to 6 from Sunday, or sun to sat; keep: how many automatic snapshots to keep, 1 to 4 (default 3). Each new one replaces the oldest automatic one. They don't count toward the 3 you take yourself. Like any snapshot, each pauses the server for about a minute. A suspended server, or one in rescue, is skipped until the next run.

Parameters

idpath · string · required
frequencybody · string
hourbody · integer
timezonebody · string
keepbody · integer

Errors

invalid_requestinvalid_schedule

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"frequency":"daily","hour":3,"timezone":"Europe/Berlin","keep":3}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/snapshot-schedule"
answer 200
{
  "schedule": {
    "frequency": "daily",
    "hour": 3,
    "weekday": null,
    "timezone": "Europe/Berlin",
    "keep": 3,
    "nextRunAt": "2026-10-22T01:17:00.000Z",
    "lastRunAt": "2026-10-21T01:17:00.000Z",
    "lastStatus": "ok",
    "lastError": null
  }
}

Turn automatic snapshots off

DELETEDELETE /servers/:id/snapshot-schedule

scope write · answers 200 · turnAutomaticSnapshotsOff

Turns automatic snapshots off. The automatic snapshots already taken are kept until you delete them. No body.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/snapshot-schedule"
answer 200
{
  "schedule": null
}

Backups

Show backups

GETGET /servers/:id/backups

scope read · answers 200 · showBackups

The server's off-site backups: whether the add-on is on (and when it ends, if you turned it off), its price for this plan (free: true on a server staff made, which is never billed: on and off at once), the daily hour and time zone, the retention (keepDaily, keepWeekly), the next and last run (lastStatus ok, failed or skipped, with the reason in lastError), the stored size after deduplication and compression, what is queued or running, and the backups, oldest first (sizeBytes: the disk's size; addedBytes: the new data that backup stored). runs: the 10 latest backups, restores and deletes.

Parameters

idpath · string · required

Errors

backups_unavailable

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/backups"
answer 200
{
  "status": {
    "enabled": true,
    "endsAt": null,
    "price": {
      "monthlyCents": 260,
      "yearlyCents": 2600
    },
    "free": false,
    "hour": 3,
    "timezone": "Europe/Berlin",
    "keepDaily": 7,
    "keepWeekly": 4,
    "nextRunAt": "2026-10-22T01:14:00.000Z",
    "lastRunAt": "2026-10-21T01:14:00.000Z",
    "lastStatus": "ok",
    "lastError": null,
    "lastSuccessAt": "2026-10-21T01:14:00.000Z",
    "activeRun": null,
    "storedBytes": 6210000000,
    "purgeAt": null,
    "manualLeft": 3
  },
  "backups": [
    {
      "id": "bkp_q4m2x7abcdefgh3k",
      "state": "ready",
      "trigger": "scheduled",
      "takenAt": "2026-10-21T01:16:31.000Z",
      "sizeBytes": 85899345920,
      "addedBytes": 412000000
    }
  ],
  "runs": [
    {
      "id": "brun_x7q2m4abcdefgh3m",
      "kind": "backup",
      "state": "done",
      "serverId": "srv_k3j5m2abcdefgh2q",
      "sourceServerId": "srv_k3j5m2abcdefgh2q",
      "backupId": "bkp_q4m2x7abcdefgh3k",
      "error": null,
      "errorText": null,
      "createdAt": "2026-10-21T01:14:00.000Z",
      "startedAt": "2026-10-21T01:14:02.000Z",
      "finishedAt": "2026-10-21T01:19:40.000Z"
    }
  ]
}

Turn backups on

POSTPOST /servers/:id/backups/enable

scope write · answers 200 · turnBackupsOn

Adds the backup add-on (by default 20% of the plan's price): an invoice for the rest of the current period, paid from your credit first. enabled is true once it is paid; otherwise pay it at payUrl. Turning it on again before a scheduled turn-off takes effect cancels the turn-off at no charge. No body.

Parameters

idpath · string · required

Errors

backups_unavailablebackups_not_on_saleaddon_change_refused

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/backups/enable"
answer 200
{
  "enabled": false,
  "invoiceId": "inv_m2q7x4abcdefgh2s",
  "payUrl": "https://my.rack4.io/billing/invoices/inv_m2q7x4abcdefgh2s"
}

Turn backups off

POSTPOST /servers/:id/backups/disable

scope write · answers 200 · turnBackupsOff

Turns the add-on off from the next renewal (endsAt). Backups keep running until then; after it they are kept 14 days, then deleted. No body.

Parameters

idpath · string · required

Errors

backups_unavailablebackups_offaddon_change_refused

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/backups/disable"
answer 200
{
  "endsAt": "2026-11-09T12:00:00.000Z"
}

Set the backup schedule

PUTPUT /servers/:id/backups/settings

scope write · answers 200 · setBackupSchedule

The daily hour (0 to 23) in timezone (an IANA name), and how many backups to keep: keepDaily (1 to 14, the newest of each day) and keepWeekly (0 to 8, the newest of each week after those). Fields left out keep their value. A run starts within 30 minutes after the hour.

Parameters

idpath · string · required
hourbody · integer
timezonebody · string
keepDailybody · integer
keepWeeklybody · integer

Errors

backups_unavailableinvalid_requestinvalid_hourinvalid_timezoneinvalid_keep_dailyinvalid_keep_weekly

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"hour":2,"timezone":"Europe/Berlin","keepDaily":14,"keepWeekly":4}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/backups/settings"
answer 200
{
  "status": {
    "enabled": true,
    "endsAt": null,
    "price": {
      "monthlyCents": 260,
      "yearlyCents": 2600
    },
    "free": false,
    "hour": 2,
    "timezone": "Europe/Berlin",
    "keepDaily": 14,
    "keepWeekly": 4,
    "nextRunAt": "2026-10-22T01:14:00.000Z",
    "lastRunAt": "2026-10-21T01:14:00.000Z",
    "lastStatus": "ok",
    "lastError": null,
    "lastSuccessAt": "2026-10-21T01:14:00.000Z",
    "activeRun": null,
    "storedBytes": 6210000000,
    "purgeAt": null,
    "manualLeft": 3
  },
  "backups": [
    {
      "id": "bkp_q4m2x7abcdefgh3k",
      "state": "ready",
      "trigger": "scheduled",
      "takenAt": "2026-10-21T01:16:31.000Z",
      "sizeBytes": 85899345920,
      "addedBytes": 412000000
    }
  ],
  "runs": [
    {
      "id": "brun_x7q2m4abcdefgh3m",
      "kind": "backup",
      "state": "done",
      "serverId": "srv_k3j5m2abcdefgh2q",
      "sourceServerId": "srv_k3j5m2abcdefgh2q",
      "backupId": "bkp_q4m2x7abcdefgh3k",
      "error": null,
      "errorText": null,
      "createdAt": "2026-10-21T01:14:00.000Z",
      "startedAt": "2026-10-21T01:14:02.000Z",
      "finishedAt": "2026-10-21T01:19:40.000Z"
    }
  ]
}

Back up now

POSTPOST /servers/:id/backups

scope write · answers 202 · backUpNow

Queues a backup now (3 a day by hand; the schedule's own don't count). It runs when the server's host is free: one backup at a time per host. While it runs the server keeps running and its other actions wait. No body.

Parameters

idpath · string · required

Errors

backups_unavailablebackups_offbackup_busyinvalid_staterate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/backups"
answer 202
{
  "run": {
    "id": "brun_x7q2m4abcdefgh3m",
    "kind": "backup",
    "state": "queued",
    "serverId": "srv_k3j5m2abcdefgh2q",
    "sourceServerId": "srv_k3j5m2abcdefgh2q",
    "backupId": "bkp_t2m7q4abcdefgh3n",
    "error": null,
    "errorText": null,
    "createdAt": "2026-10-21T01:14:00.000Z",
    "startedAt": null,
    "finishedAt": null
  }
}

Restore a backup

POSTPOST /servers/:id/backups/:backupId/restore

scope write · answers 202 · restoreBackup

Restores the backup over this server: it is shut down, its disk replaced by the backup and started again. Everything written since the backup is lost. To restore into a new server instead, order one with restoreFrom (POST /servers). No body.

Parameters

idpath · string · required
backupIdpath · string · required

Errors

backups_unavailablebackup_not_foundbackup_busyinvalid_statetoo_largeheld

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/backups/bkp_q4m2x7abcdefgh3k/restore"
answer 202
{
  "run": {
    "id": "brun_x7q2m4abcdefgh3m",
    "kind": "restore",
    "state": "queued",
    "serverId": "srv_k3j5m2abcdefgh2q",
    "sourceServerId": "srv_k3j5m2abcdefgh2q",
    "backupId": "bkp_q4m2x7abcdefgh3k",
    "error": null,
    "errorText": null,
    "createdAt": "2026-10-21T01:14:00.000Z",
    "startedAt": null,
    "finishedAt": null
  }
}

Delete a backup

DELETEDELETE /servers/:id/backups/:backupId

scope write · answers 202 · deleteBackup

Deletes one backup from off-site storage for good. The others are kept.

Parameters

idpath · string · required
backupIdpath · string · required

Errors

backups_unavailablebackup_not_foundbackup_busyheld

curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/backups/bkp_q4m2x7abcdefgh3k"
answer 202
{
  "run": {
    "id": "brun_x7q2m4abcdefgh3m",
    "kind": "delete",
    "state": "queued",
    "serverId": "srv_k3j5m2abcdefgh2q",
    "sourceServerId": "srv_k3j5m2abcdefgh2q",
    "backupId": "bkp_q4m2x7abcdefgh3k",
    "error": null,
    "errorText": null,
    "createdAt": "2026-10-21T01:14:00.000Z",
    "startedAt": null,
    "finishedAt": null
  }
}

Rescue

Show rescue mode

GETGET /servers/:id/rescue

scope read · answers 200 · showRescueMode

The server's rescue: state is off, entering, on, leaving or stuck (switching failed part way: leave rescue to boot the server's own disk). startedAt and expiresAt while it isn't off; available is false while the server is suspended or rescue isn't set up.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/rescue"
answer 200
{
  "rescue": {
    "state": "on",
    "startedAt": "2026-10-21T09:12:00.000Z",
    "expiresAt": "2026-10-22T09:12:00.000Z",
    "available": true
  }
}

Boot into rescue

POSTPOST /servers/:id/rescue

scope write · answers 202 · bootIntoRescue

Shuts the server down and boots a rescue Linux system, with the server's disk attached as /dev/vdb (not mounted), its IP addresses and its firewall. rootPassword is a one-time root password for the console and SSH: it is in this answer only, so keep it. Your account's SSH keys work too. Rescue ends by itself after 24 hours. A suspended server can't use rescue. 10 an hour. No body.

Parameters

idpath · string · required

Errors

action_pendinginvalid_staterescue_unavailablerate_limitedrescue_not_queued

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/rescue"
answer 202
{
  "rescue": {
    "state": "entering",
    "startedAt": "2026-10-21T09:12:00.000Z",
    "expiresAt": "2026-10-22T09:12:00.000Z",
    "available": true
  },
  "rootPassword": "Kp7mQ-x3RtN-9wZcA-hG2vE",
  "server": {
    "id": "srv_k3j5m2abcdefgh2q",
    "hostname": "web-1",
    "plan": "standard",
    "image": "debian-12",
    "state": "running",
    "pendingAction": "rescue",
    "ipv4": "203.0.113.10",
    "ipv6": "2001:db8:10::2",
    "vcpus": 2,
    "memoryMiB": 4096,
    "diskGiB": 80,
    "createdAt": "2026-10-09T12:00:00.000Z",
    "stateSince": "2026-10-09T12:01:00.000Z",
    "app": "docker",
    "deletionProtected": false,
    "purgeAt": null
  }
}

Leave rescue

DELETEDELETE /servers/:id/rescue

scope write · answers 202 · leaveRescue

Boots the server from its own disk again. The rescue system and anything changed on it (not on your disk) is thrown away. No body.

Parameters

idpath · string · required

Errors

action_pendingnot_in_rescueinvalid_staterescue_not_queued

curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/rescue"
answer 202
{
  "rescue": {
    "state": "leaving",
    "startedAt": "2026-10-21T09:12:00.000Z",
    "expiresAt": "2026-10-22T09:12:00.000Z",
    "available": true
  },
  "server": {
    "id": "srv_k3j5m2abcdefgh2q",
    "hostname": "web-1",
    "plan": "standard",
    "image": "debian-12",
    "state": "rescue",
    "pendingAction": "rescue_exit",
    "ipv4": "203.0.113.10",
    "ipv6": "2001:db8:10::2",
    "vcpus": 2,
    "memoryMiB": 4096,
    "diskGiB": 80,
    "createdAt": "2026-10-09T12:00:00.000Z",
    "stateSince": "2026-10-09T12:01:00.000Z",
    "app": "docker",
    "deletionProtected": false,
    "purgeAt": null
  }
}

Reverse DNS

List reverse DNS

GETGET /servers/:id/rdns

scope read · answers 200 · listReverseDns

One entry per address of the server, with its PTR name and state (pending, active, failed, removing, or null when none is set).

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/rdns"
answer 200
{
  "data": [
    {
      "address": "203.0.113.10",
      "family": 4,
      "hostname": "mail.example.com",
      "state": "active",
      "errorCode": null
    },
    {
      "address": "2001:db8:10::2",
      "family": 6,
      "hostname": null,
      "state": null,
      "errorCode": null
    }
  ]
}

Set reverse DNS

PUTPUT /servers/:id/rdns/:address

scope write · answers 202 · setReverseDns

Asks for a PTR name on one of the server's addresses. The name must already resolve to that address (an A record for IPv4, AAAA for IPv6); the lookup gives up after 3 seconds. The request is pending until it is applied. Ten changes an hour.

Parameters

idpath · string · required
addresspath · string · required
hostnamebody · string

Errors

invalid_requestaddress_not_foundinvalid_hostnameno_recordforward_mismatchdns_timeoutdns_errorrate_limitedinvalid_state

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"hostname":"mail.example.com"}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/rdns/203.0.113.10"
answer 202
{
  "address": "203.0.113.10",
  "family": 4,
  "hostname": "mail.example.com",
  "state": "pending",
  "errorCode": null
}

Clear reverse DNS

DELETEDELETE /servers/:id/rdns/:address

scope write · answers 202 · clearReverseDns

Takes the PTR name off the address. A name that was never applied just goes; an applied one shows removing until it is reset. Counts toward the ten changes an hour.

Parameters

idpath · string · required
addresspath · string · required

Errors

address_not_foundrate_limitedinvalid_state

curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/rdns/203.0.113.10"
answer 202
{
  "address": "203.0.113.10",
  "family": 4,
  "hostname": "mail.example.com",
  "state": "removing",
  "errorCode": null
}

Invoices

List invoices

GETGET /invoices

scope read · answers 200 · listInvoices

Your invoices, newest first, a page at a time. Amounts are integer cents. limit sets the page size: 50 unless you say, 200 at most. next is null on the last page; otherwise ask for the following page with ?before= and that value, like /invoices?limit=50&before=R4-000042.

Parameters

limitquery · integer
Page size: 1 to 200, 50 by default.
beforequery · string
The next value of the previous page, an invoice number like R4-000042.

Errors

invalid_request

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/invoices"
answer 200
{
  "data": [
    {
      "id": "inv_q4m7x2abcdefgh2t",
      "number": "R4-000042",
      "kind": "renewal",
      "status": "open",
      "currency": "USD",
      "subtotalCents": 1200,
      "creditAppliedCents": 0,
      "totalCents": 1200,
      "issuedAt": "2026-11-02T12:00:00.000Z",
      "dueAt": "2026-11-09T12:00:00.000Z",
      "paidAt": null,
      "voidedAt": null,
      "serverId": "srv_k3j5m2abcdefgh2q",
      "term": "month"
    }
  ],
  "next": null
}

Show an invoice

GETGET /invoices/:id

scope read · answers 200 · getInvoice

One invoice with its lines.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/invoices/inv_q4m7x2abcdefgh2t"
answer 200
{
  "id": "inv_q4m7x2abcdefgh2t",
  "number": "R4-000042",
  "kind": "renewal",
  "status": "open",
  "currency": "USD",
  "subtotalCents": 1200,
  "creditAppliedCents": 0,
  "totalCents": 1200,
  "issuedAt": "2026-11-02T12:00:00.000Z",
  "dueAt": "2026-11-09T12:00:00.000Z",
  "paidAt": null,
  "voidedAt": null,
  "serverId": "srv_k3j5m2abcdefgh2q",
  "term": "month",
  "lines": [
    {
      "description": "Standard server web-1, 2026-11-09 to 2026-12-09",
      "periodStart": "2026-11-09T12:00:00.000Z",
      "periodEnd": "2026-12-09T12:00:00.000Z",
      "amountCents": 1200,
      "serverId": "srv_k3j5m2abcdefgh2q",
      "addon": null,
      "quantity": null
    },
    {
      "description": "Extra RAM +2 GB, web-1, 2026-11-09 to 2026-12-09",
      "periodStart": "2026-11-09T12:00:00.000Z",
      "periodEnd": "2026-12-09T12:00:00.000Z",
      "amountCents": 700,
      "serverId": "srv_k3j5m2abcdefgh2q",
      "addon": "ram",
      "quantity": 2
    }
  ]
}

Billing

Balance and upcoming charges

GETGET /billing

scope read · answers 200 · balanceAndUpcomingCharges

Your account credit, what is due in the next 30 days (open invoices plus renewals not invoiced yet) and how much of it credit covers, each billed server's next charge, your open invoices, and what switching a monthly server to yearly would save. upcoming[].state is estimated (no renewal invoice yet: today's prices for the plan and each add-on), invoiced (its renewal is open), paid, or cancelling (deleted at dueAt; nothing more is charged). Credit pays each new invoice first.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/billing"
answer 200
{
  "currency": "USD",
  "creditCents": 1500,
  "next30Days": {
    "totalCents": 1899,
    "coveredByCreditCents": 1500,
    "leftToPayCents": 399
  },
  "upcoming": [
    {
      "serverId": "srv_k3j5m2abcdefgh2q",
      "hostname": "web-1",
      "plan": "Standard",
      "term": "month",
      "nextTerm": "month",
      "dueAt": "2026-11-09T12:00:00.000Z",
      "state": "estimated",
      "amountCents": 1899,
      "lines": [
        {
          "description": "Standard server web-1, 2026-11-09 to 2026-12-09",
          "amountCents": 1299
        },
        {
          "description": "Extra RAM +2 GB, web-1, 2026-11-09 to 2026-12-09",
          "amountCents": 700
        }
      ],
      "invoiceId": null
    }
  ],
  "openInvoices": [],
  "yearlySavings": [
    {
      "serverId": "srv_k3j5m2abcdefgh2q",
      "hostname": "web-1",
      "monthlyCents": 1999,
      "twelveMonthsCents": 23988,
      "yearlyCents": 20900,
      "saveCents": 3088
    }
  ],
  "topup": {
    "available": true,
    "minCents": 500,
    "maxCents": 100000
  }
}

Credit history

GETGET /billing/credit

scope read · answers 200 · creditHistory

Every change to your account credit, newest first, with the balance after it. reason is topup, overpayment (paid over an invoice, or after it closed), refund (to credit), manual (from Rack4 staff), applied (negative: paid an invoice; positive: came back from a voided one) or reversal (a top-up taken back after its payment was reversed). limit: 100 unless you say, 500 at most; more is true when older movements were left out.

Errors

invalid_request

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/billing/credit"
answer 200
{
  "creditCents": 1500,
  "data": [
    {
      "at": "2026-11-02T12:00:00.000Z",
      "reason": "applied",
      "description": "Paid invoice R4-000042",
      "amountCents": -1000,
      "balanceCents": 1500,
      "invoiceId": "inv_q4m7x2abcdefgh2t"
    },
    {
      "at": "2026-10-20T09:30:00.000Z",
      "reason": "topup",
      "description": "Top-up",
      "amountCents": 2500,
      "balanceCents": 2500,
      "invoiceId": "inv_t2m4q7abcdefgh2v"
    }
  ],
  "more": false
}

Add funds (a credit top-up)

POSTPOST /billing/topups

scope write · answers 201 · addFundsCreditTopUp

Buys account credit: makes a top-up invoice for amountCents (from $5.00 to $1,000.00 unless the limits in GET /billing say otherwise) and a checkout for it. method is btcpay, usdt (USDT on Tron, TRC-20 only) or paypal; left out, the first available. Open checkoutUrl to pay (null when the provider did not answer: pay from payUrl instead). Once paid, the amount is added to your credit, which pays open invoices and renewals first. At most 3 unpaid top-ups at a time (they lapse after 7 days), 10 tries an hour. Needs a confirmed email.

Parameters

methodbody · string
amountCentsbody · integer

Errors

invalid_amountinvalid_requestemail_unverifiedtopups_closedprovider_unavailabletoo_many_open_topupsrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"method":"btcpay","amountCents":2500}' \
  "$API/billing/topups"
answer 201
{
  "invoice": {
    "id": "inv_q4m7x2abcdefgh2t",
    "number": "R4-000042",
    "kind": "topup",
    "status": "open",
    "currency": "USD",
    "subtotalCents": 2500,
    "creditAppliedCents": 0,
    "totalCents": 2500,
    "issuedAt": "2026-10-20T09:30:00.000Z",
    "dueAt": "2026-10-20T09:30:00.000Z",
    "paidAt": null,
    "voidedAt": null,
    "serverId": null,
    "term": "month"
  },
  "payUrl": "https://my.rack4.io/billing/invoices/inv_q4m7x2abcdefgh2t",
  "checkoutUrl": "https://btcpay.node4.io/i/AbC123xYz",
  "provider": "btcpay"
}

Show billing details

GETGET /billing/profile

scope read · answers 200 · showBillingDetails

Who your invoices are made out to. Every field is a string, "" when not given; country is an ISO 3166 code like GB. updatedAt is null before you first save.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/billing/profile"
answer 200
{
  "name": "Ada Lovelace",
  "company": "Analytical Engines Ltd",
  "address1": "12 Byron Street",
  "address2": "",
  "city": "London",
  "region": "",
  "postalCode": "W1 2AB",
  "country": "GB",
  "taxId": "GB123456789",
  "updatedAt": "2026-10-20T09:30:00.000Z"
}

Replace billing details

PUTPUT /billing/profile

scope write · answers 200 · replaceBillingDetails

Sets the whole profile: a field left out is cleared. Fields: name, company, address1, address2, city, region, postalCode (one line of text each), country (an ISO 3166 code or "") and taxId (a VAT number or tax ID; its format is checked for your country and it is stored normalized, like GB123456789; it is not verified with any registry). Invoices issued from now on carry these details; invoices already issued keep the ones they were issued with.

Parameters

namebody · string
companybody · string
address1body · string
citybody · string
postalCodebody · string
countrybody · string
taxIdbody · string

Errors

invalid_requestinvalid_profileinvalid_countryinvalid_tax_idrate_limited

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Ada Lovelace","company":"Analytical Engines Ltd","address1":"12 Byron Street","city":"London","postalCode":"W1 2AB","country":"GB","taxId":"GB 123 456 789"}' \
  "$API/billing/profile"
answer 200
{
  "name": "Ada Lovelace",
  "company": "Analytical Engines Ltd",
  "address1": "12 Byron Street",
  "address2": "",
  "city": "London",
  "region": "",
  "postalCode": "W1 2AB",
  "country": "GB",
  "taxId": "GB123456789",
  "updatedAt": "2026-10-20T09:30:00.000Z"
}

List gift cards

GETGET /gift-cards

scope read · answers 200 · listGiftCards

The gift cards your account bought, newest first (100 at most). state is pending (unpaid), active (paid, not redeemed yet), redeemed, void or lapsed (never paid). The code itself is never in the API: it is on the card's page in the panel and in the recipient's email; hint is its last four characters.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/gift-cards"
answer 200
{
  "data": [
    {
      "id": "gft_r4m7x2abcdefgh2w",
      "amountCents": 2500,
      "state": "active",
      "hint": "R3TB",
      "recipientEmail": "sam@example.com",
      "recipientName": "Sam",
      "fromName": "Alex",
      "message": "Happy birthday! Build something fun.",
      "invoiceId": "inv_q4m7x2abcdefgh2t",
      "createdAt": "2026-10-21T09:30:00.000Z",
      "paidAt": "2026-10-21T09:41:00.000Z",
      "sentAt": "2026-10-21T09:41:00.000Z",
      "redeemedAt": null
    }
  ]
}

Buy a gift card

POSTPOST /gift-cards

scope write · answers 201 · buyGiftCard

Makes a gift card and the invoice for it, with a checkout. amountCents is one of 1000, 2500, 5000, 10000 or 20000; recipientEmail is where the card is emailed once the invoice is paid; recipientName, fromName (up to 80 characters each) and message (up to 500) are optional and appear on the card. method is btcpay, usdt (USDT on Tron, TRC-20 only) or paypal; left out, the first available. Account credit can't pay a gift card. At most 3 unpaid gift cards at a time (they lapse after 7 days), 10 tries an hour. Needs a confirmed email.

Parameters

amountCentsbody · integer
recipientEmailbody · string
recipientNamebody · string
fromNamebody · string
messagebody · string
methodbody · string

Errors

invalid_amountinvalid_emailinvalid_requestemail_unverifiedgift_cards_closedtoo_many_open_gift_cardsrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"amountCents":2500,"recipientEmail":"sam@example.com","recipientName":"Sam","fromName":"Alex","message":"Happy birthday! Build something fun.","method":"btcpay"}' \
  "$API/gift-cards"
answer 201
{
  "giftCard": {
    "id": "gft_r4m7x2abcdefgh2w",
    "amountCents": 2500,
    "state": "pending",
    "hint": "R3TB",
    "recipientEmail": "sam@example.com",
    "recipientName": "Sam",
    "fromName": "Alex",
    "message": "Happy birthday! Build something fun.",
    "invoiceId": "inv_q4m7x2abcdefgh2t",
    "createdAt": "2026-10-21T09:30:00.000Z",
    "paidAt": null,
    "sentAt": null,
    "redeemedAt": null
  },
  "invoice": {
    "id": "inv_q4m7x2abcdefgh2t",
    "number": "R4-000042",
    "kind": "gift",
    "status": "open",
    "currency": "USD",
    "subtotalCents": 2500,
    "creditAppliedCents": 0,
    "totalCents": 2500,
    "issuedAt": "2026-10-21T09:30:00.000Z",
    "dueAt": "2026-10-21T09:30:00.000Z",
    "paidAt": null,
    "voidedAt": null,
    "serverId": null,
    "term": "month"
  },
  "payUrl": "https://my.rack4.io/billing/invoices/inv_q4m7x2abcdefgh2t",
  "checkoutUrl": "https://btcpay.node4.io/i/AbC123xYz",
  "provider": "btcpay"
}

Redeem a gift card

POSTPOST /gift-cards/redeem

scope write · answers 200 · redeemGiftCard

Adds a gift card's whole amount to your account credit, which then pays your open invoices first. code is as printed (GIFT-XXXX-XXXX-XXXX-XXXX), in any case, with or without dashes. A code works once. 10 wrong codes an hour per account (and per address); a right one doesn't count. Needs a confirmed email.

Parameters

codebody · string

Errors

invalid_requestinvalid_gift_codegift_card_not_foundgift_card_not_paidgift_card_redeemedgift_card_voidemail_unverifiedrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"code":"GIFT-7K2M-Q9XD-4HNP-R3TB"}' \
  "$API/gift-cards/redeem"
answer 200
{
  "redeemedCents": 2500,
  "creditCents": 2500
}

Notifications

List notifications

GETGET /notifications

scope read · answers 200 · listNotifications

Your notifications, newest first, 50 a page (page=2 for the next; ten pages at most), the unread count, and whether there are more. unread=true lists only unread ones. Kept 90 days, 500 at most.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/notifications"
answer 200
{
  "data": [
    {
      "id": "ntf_r2t7k4abcdefgh3a",
      "type": "alert.triggered",
      "category": "alerts",
      "title": "web-1: CPU usage above 90% for 10 minutes",
      "body": "Alert triggered: CPU usage above 90% for 10 minutes. Reading: 97%.",
      "link": "/servers/srv_k3j5m2abcdefgh2q/alerts",
      "createdAt": "2026-10-21T09:14:00.000Z",
      "read": false
    }
  ],
  "unread": 1,
  "page": 1,
  "more": false
}

Mark one read

POSTPOST /notifications/:id/read

scope write · answers 200 · markOneRead

Marks the notification read and answers with it.

Parameters

idpath · string · required
curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/notifications/ntf_r2t7k4abcdefgh3a/read"
answer 200
{
  "id": "ntf_r2t7k4abcdefgh3a",
  "type": "alert.triggered",
  "category": "alerts",
  "title": "web-1: CPU usage above 90% for 10 minutes",
  "body": "Alert triggered: CPU usage above 90% for 10 minutes. Reading: 97%.",
  "link": "/servers/srv_k3j5m2abcdefgh2q/alerts",
  "createdAt": "2026-10-21T09:14:00.000Z",
  "read": true
}

Mark all read

POSTPOST /notifications/read-all

scope write · answers 200 · markAllRead

Marks every notification read; marked is how many were unread.

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/notifications/read-all"
answer 200
{
  "marked": 3
}

Alert rules

List a server's alert rules

GETGET /servers/:id/alerts

scope read · answers 200 · listServerSAlertRules

The server's resource alert rules, oldest first, with their state (ok or firing) and when they last fired.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/alerts"
answer 200
{
  "data": [
    {
      "id": "alr_m4q2x7abcdefgh3b",
      "metric": "cpu",
      "threshold": 90,
      "durationMin": 10,
      "email": true,
      "enabled": true,
      "state": "ok",
      "text": "CPU usage above 90% for 10 minutes",
      "firedAt": null,
      "resolvedAt": null,
      "lastValue": null,
      "serverId": "srv_k3j5m2abcdefgh2q",
      "createdAt": "2026-10-21T09:00:00.000Z"
    }
  ]
}

Add an alert rule

POSTPOST /servers/:id/alerts

scope write · answers 201 · addAlertRule

metric: cpu, memory, transfer (this month's traffic against the allowance) or down (the host hasn't reported the server, or it used no CPU, for durationMin). threshold: a whole percentage from 1 to 100 (not for down). durationMin: 5, 10, 15, 30 or 60 (not for transfer). email: also email you (default true). Checked every minute; one notice when it fires and one when it clears, then quiet for 30 minutes. 10 rules per server.

Parameters

idpath · string · required
metricbody · string
thresholdbody · integer
durationMinbody · integer
emailbody · boolean

Errors

invalid_requestinvalid_metricinvalid_thresholdinvalid_durationtoo_many_alert_rules

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"metric":"cpu","threshold":90,"durationMin":10,"email":true}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/alerts"
answer 201
{
  "id": "alr_m4q2x7abcdefgh3b",
  "metric": "cpu",
  "threshold": 90,
  "durationMin": 10,
  "email": true,
  "enabled": true,
  "state": "ok",
  "text": "CPU usage above 90% for 10 minutes",
  "firedAt": null,
  "resolvedAt": null,
  "lastValue": null,
  "serverId": "srv_k3j5m2abcdefgh2q",
  "createdAt": "2026-10-21T09:00:00.000Z"
}

Change an alert rule

PUTPUT /servers/:id/alerts/:ruleId

scope write · answers 200 · changeAlertRule

Send any of enabled, email, threshold and durationMin. A change ends an incident in progress quietly; the rule is judged afresh from the next minute. The metric can't change.

Parameters

idpath · string · required
ruleIdpath · string · required
enabledbody · boolean
emailbody · boolean

Errors

invalid_requestinvalid_thresholdinvalid_duration

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"enabled":false,"email":true}' \
  "$API/servers/srv_k3j5m2abcdefgh2q/alerts/alr_m4q2x7abcdefgh3b"
answer 200
{
  "id": "alr_m4q2x7abcdefgh3b",
  "metric": "cpu",
  "threshold": 90,
  "durationMin": 10,
  "email": true,
  "enabled": false,
  "state": "ok",
  "text": "CPU usage above 90% for 10 minutes",
  "firedAt": null,
  "resolvedAt": null,
  "lastValue": null,
  "serverId": "srv_k3j5m2abcdefgh2q",
  "createdAt": "2026-10-21T09:00:00.000Z"
}

Delete an alert rule

DELETEDELETE /servers/:id/alerts/:ruleId

scope write · answers 204 · deleteAlertRule

Deletes the rule. Nothing more is sent for it.

Parameters

idpath · string · required
ruleIdpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/servers/srv_k3j5m2abcdefgh2q/alerts/alr_m4q2x7abcdefgh3b"

204: no body.

Webhooks

List webhook endpoints

GETGET /webhooks

scope read · answers 200 · listWebhookEndpoints

Your endpoints, oldest first. A secret is never listed.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/webhooks"
answer 200
{
  "data": [
    {
      "id": "whk_x7m2q4abcdefgh3c",
      "url": "https://hooks.example.com/vpsnine",
      "description": "deploy bot",
      "events": [
        "server.ready",
        "alert.triggered",
        "alert.resolved"
      ],
      "enabled": true,
      "disabledReason": null,
      "failureStreak": 0,
      "lastSuccessAt": "2026-10-21T09:14:01.000Z",
      "lastFailureAt": null,
      "createdAt": "2026-10-20T08:00:00.000Z",
      "rotatedUntil": null
    }
  ]
}

Add a webhook endpoint

POSTPOST /webhooks

scope write · answers 201 · addWebhookEndpoint

url: https only, on a public address (private, loopback, link-local and metadata addresses are refused, and checked again before every delivery). events: the types to send. The answer carries the signing secret: the only time it is shown.

Parameters

urlbody · string
descriptionbody · string
eventsbody · list of string

Errors

invalid_requestinvalid_urlinvalid_descriptioninvalid_eventstoo_many_webhooks

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/vpsnine","description":"deploy bot","events":["server.ready","alert.triggered","alert.resolved"]}' \
  "$API/webhooks"
answer 201
{
  "id": "whk_x7m2q4abcdefgh3c",
  "url": "https://hooks.example.com/vpsnine",
  "description": "deploy bot",
  "events": [
    "server.ready",
    "alert.triggered",
    "alert.resolved"
  ],
  "enabled": true,
  "disabledReason": null,
  "failureStreak": 0,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "createdAt": "2026-10-20T08:00:00.000Z",
  "rotatedUntil": null,
  "secret": "whsec_shown-once-keep-it-somewhere-safe"
}

Show a webhook endpoint

GETGET /webhooks/:id

scope read · answers 200 · showWebhookEndpoint

One endpoint, without its secret.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/webhooks/whk_x7m2q4abcdefgh3c"
answer 200
{
  "id": "whk_x7m2q4abcdefgh3c",
  "url": "https://hooks.example.com/vpsnine",
  "description": "deploy bot",
  "events": [
    "server.ready",
    "alert.triggered",
    "alert.resolved"
  ],
  "enabled": true,
  "disabledReason": null,
  "failureStreak": 0,
  "lastSuccessAt": "2026-10-21T09:14:01.000Z",
  "lastFailureAt": null,
  "createdAt": "2026-10-20T08:00:00.000Z",
  "rotatedUntil": null
}

Change a webhook endpoint

PUTPUT /webhooks/:id

scope write · answers 200 · changeWebhookEndpoint

Send any of description, events, url and enabled. Turning an endpoint off fails what is still waiting; turning it on clears its failures.

Parameters

idpath · string · required
descriptionbody · string
eventsbody · list of string
enabledbody · boolean

Errors

invalid_requestinvalid_urlinvalid_descriptioninvalid_events

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"description":"deploy bot (staging)","events":["server.ready"],"enabled":true}' \
  "$API/webhooks/whk_x7m2q4abcdefgh3c"
answer 200
{
  "id": "whk_x7m2q4abcdefgh3c",
  "url": "https://hooks.example.com/vpsnine",
  "description": "deploy bot (staging)",
  "events": [
    "server.ready"
  ],
  "enabled": true,
  "disabledReason": null,
  "failureStreak": 0,
  "lastSuccessAt": "2026-10-21T09:14:01.000Z",
  "lastFailureAt": null,
  "createdAt": "2026-10-20T08:00:00.000Z",
  "rotatedUntil": null
}

Delete a webhook endpoint

DELETEDELETE /webhooks/:id

scope write · answers 204 · deleteWebhookEndpoint

Deletes the endpoint and its delivery log. Nothing more is sent to it.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/webhooks/whk_x7m2q4abcdefgh3c"

204: no body.

Rotate the signing secret

POSTPOST /webhooks/:id/rotate-secret

scope write · answers 200 · rotateSigningSecret

A new secret, in the answer only. For 24 hours each delivery carries a second v1 signature made with the old one, so your receiver can switch over without dropping events.

Parameters

idpath · string · required
curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/webhooks/whk_x7m2q4abcdefgh3c/rotate-secret"
answer 200
{
  "id": "whk_x7m2q4abcdefgh3c",
  "url": "https://hooks.example.com/vpsnine",
  "description": "deploy bot",
  "events": [
    "server.ready",
    "alert.triggered",
    "alert.resolved"
  ],
  "enabled": true,
  "disabledReason": null,
  "failureStreak": 0,
  "lastSuccessAt": "2026-10-21T09:14:01.000Z",
  "lastFailureAt": null,
  "createdAt": "2026-10-20T08:00:00.000Z",
  "rotatedUntil": "2026-10-22T09:20:00.000Z",
  "secret": "whsec_shown-once-keep-it-somewhere-safe"
}

Send a test event

POSTPOST /webhooks/:id/test

scope write · answers 202 · sendTestEvent

Queues a ping event to the endpoint now (one attempt, whatever it subscribes to); read the delivery log for the answer. 5 a minute.

Parameters

idpath · string · required

Errors

endpoint_disabledrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/webhooks/whk_x7m2q4abcdefgh3c/test"
answer 202
{
  "id": "whd_q2x7m4abcdefgh3d",
  "eventId": "evt_t4k2r7abcdefgh3e",
  "eventType": "ping",
  "state": "pending",
  "attempts": 0,
  "status": null,
  "error": null,
  "latencyMs": null,
  "response": null,
  "payload": "{\"id\":\"evt_t4k2r7abcdefgh3e\",\"type\":\"ping\",\"createdAt\":\"2026-10-21T09:20:00.000Z\",\"data\":{\"message\":\"A test event from Rack4. Your endpoint is set up.\",\"endpoint\":{\"id\":\"whk_x7m2q4abcdefgh3c\"}}}",
  "createdAt": "2026-10-21T09:20:00.000Z",
  "nextAttemptAt": "2026-10-21T09:20:00.000Z"
}

List deliveries

GETGET /webhooks/:id/deliveries

scope read · answers 200 · listDeliveries

The endpoint's last 50 deliveries, newest first: state, attempts, the last answer's status, latency and first 1 KiB, and the exact body sent.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/webhooks/whk_x7m2q4abcdefgh3c/deliveries"
answer 200
{
  "data": [
    {
      "id": "whd_q2x7m4abcdefgh3d",
      "eventId": "evt_t4k2r7abcdefgh3e",
      "eventType": "alert.triggered",
      "state": "succeeded",
      "attempts": 1,
      "status": 200,
      "error": null,
      "latencyMs": 182,
      "response": "ok",
      "payload": "{\"id\":\"evt_t4k2r7abcdefgh3e\",\"type\":\"alert.triggered\",\"createdAt\":\"2026-10-21T09:14:00.000Z\",\"data\":{\"rule\":{\"id\":\"alr_m4q2x7abcdefgh3b\",\"metric\":\"cpu\",\"threshold\":90,\"durationMin\":10},\"server\":{\"id\":\"srv_k3j5m2abcdefgh2q\",\"hostname\":\"web-1\"},\"value\":97,\"state\":\"firing\"}}",
      "createdAt": "2026-10-21T09:14:00.000Z",
      "nextAttemptAt": null
    }
  ]
}

Redeliver

POSTPOST /webhooks/:id/deliveries/:deliveryId/redeliver

scope write · answers 202 · redeliver

Sends the same event (same id and body) again as a new delivery. 5 a minute.

Parameters

idpath · string · required
deliveryIdpath · string · required

Errors

endpoint_disabledrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/webhooks/whk_x7m2q4abcdefgh3c/deliveries/whd_q2x7m4abcdefgh3d/redeliver"
answer 202
{
  "id": "whd_m7q2x4abcdefgh3f",
  "eventId": "evt_t4k2r7abcdefgh3e",
  "eventType": "alert.triggered",
  "state": "pending",
  "attempts": 0,
  "status": null,
  "error": null,
  "latencyMs": null,
  "response": null,
  "payload": "{\"id\":\"evt_t4k2r7abcdefgh3e\",\"type\":\"alert.triggered\",\"createdAt\":\"2026-10-21T09:14:00.000Z\",\"data\":{\"rule\":{\"id\":\"alr_m4q2x7abcdefgh3b\",\"metric\":\"cpu\",\"threshold\":90,\"durationMin\":10},\"server\":{\"id\":\"srv_k3j5m2abcdefgh2q\",\"hostname\":\"web-1\"},\"value\":97,\"state\":\"firing\"}}",
  "createdAt": "2026-10-21T09:14:00.000Z",
  "nextAttemptAt": "2026-10-21T09:25:00.000Z"
}

Monitors

List monitors

GETGET /monitors

scope read · answers 200 · listMonitors

Your monitors, oldest first, each with uptime over 24 hours and 30 days, the average response time over 24 hours and hourly averages (spark). limits: how many you may have, and the shortest interval.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/monitors"
answer 200
{
  "data": [
    {
      "id": "mon_k4q2m7abcdefgh4a",
      "name": "Shop",
      "kind": "http",
      "targetText": "https://shop.example.com/",
      "target": "https://shop.example.com/",
      "port": null,
      "intervalS": 60,
      "timeoutS": 10,
      "email": true,
      "state": "up",
      "stateSince": "2026-10-20T08:00:12.000Z",
      "lastCheckAt": "2026-10-21T09:14:03.000Z",
      "lastLatencyMs": 182,
      "lastStatus": 200,
      "lastError": null,
      "tlsExpiresAt": "2026-12-30T23:59:59.000Z",
      "pingUrl": null,
      "lastPingAt": null,
      "settings": {
        "method": "GET",
        "statusMin": 200,
        "statusMax": 399,
        "keyword": "Add to cart",
        "keywordAbsent": false,
        "followRedirects": true,
        "hasBody": false,
        "tlsWarnDays": 14,
        "dnsType": "A",
        "dnsExpect": null,
        "graceS": 300
      },
      "createdAt": "2026-10-20T08:00:00.000Z",
      "uptime24h": 100,
      "uptime30d": 99.95,
      "avgLatency24h": 176,
      "spark": [
        171,
        168,
        180,
        175,
        179,
        182
      ]
    }
  ],
  "limits": {
    "monitors": 15,
    "used": 1,
    "minIntervalS": 60,
    "statusPages": 3
  }
}

Add a monitor

POSTPOST /monitors

scope write · answers 201 · addMonitor

kind: http, tcp, ping, dns or heartbeat; name. http: url, and optionally method (GET, HEAD, POST, PUT), statusMin and statusMax (default 200-399), keyword with keywordAbsent, followRedirects (default true), body (POST and PUT, at most 1 KiB), tlsWarnDays (0, 7, 14, 21 or 30). tcp: host and port. ping: host. dns: host, dnsType (A, AAAA, CNAME, MX, TXT, NS) and optionally dnsExpect. heartbeat: periodS (60, 300, 600, 900, 1800, 3600, 7200, 21600, 43200, 86400, 604800) and graceS (60, 300, 600, 1800, 3600, 7200); the answer carries its pingUrl. Others: intervalS, timeoutS (1 to 30), email (default true). The first check runs within seconds.

Parameters

kindbody · string
namebody · string
urlbody · string
keywordbody · string
intervalSbody · integer

Errors

invalid_requestinvalid_kindinvalid_nameinvalid_urlinvalid_hostinvalid_portinvalid_interval_sinvalid_period_stoo_many_monitorsrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"kind":"http","name":"Shop","url":"https://shop.example.com/","keyword":"Add to cart","intervalS":60}' \
  "$API/monitors"
answer 201
{
  "id": "mon_k4q2m7abcdefgh4a",
  "name": "Shop",
  "kind": "http",
  "targetText": "https://shop.example.com/",
  "target": "https://shop.example.com/",
  "port": null,
  "intervalS": 60,
  "timeoutS": 10,
  "email": true,
  "state": "pending",
  "stateSince": "2026-10-20T08:00:12.000Z",
  "lastCheckAt": null,
  "lastLatencyMs": null,
  "lastStatus": null,
  "lastError": null,
  "tlsExpiresAt": null,
  "pingUrl": null,
  "lastPingAt": null,
  "settings": {
    "method": "GET",
    "statusMin": 200,
    "statusMax": 399,
    "keyword": "Add to cart",
    "keywordAbsent": false,
    "followRedirects": true,
    "hasBody": false,
    "tlsWarnDays": 14,
    "dnsType": "A",
    "dnsExpect": null,
    "graceS": 300
  },
  "createdAt": "2026-10-20T08:00:00.000Z"
}

Show a monitor

GETGET /monitors/:id

scope read · answers 200 · showMonitor

One monitor with its uptime over 24 hours, 7, 30 and 90 days, its response time (range=24h, 7d, 30d or 90d; average, slowest and the series, oldest first) and its last 90 days.

Parameters

idpath · string · required
rangequery · string
24h (15-minute buckets), 7d (hourly), 30d (6-hourly) or 90d (daily). Default 24h.
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/monitors/mon_k4q2m7abcdefgh4a"
answer 200
{
  "id": "mon_k4q2m7abcdefgh4a",
  "name": "Shop",
  "kind": "http",
  "targetText": "https://shop.example.com/",
  "target": "https://shop.example.com/",
  "port": null,
  "intervalS": 60,
  "timeoutS": 10,
  "email": true,
  "state": "up",
  "stateSince": "2026-10-20T08:00:12.000Z",
  "lastCheckAt": "2026-10-21T09:14:03.000Z",
  "lastLatencyMs": 182,
  "lastStatus": 200,
  "lastError": null,
  "tlsExpiresAt": "2026-12-30T23:59:59.000Z",
  "pingUrl": null,
  "lastPingAt": null,
  "settings": {
    "method": "GET",
    "statusMin": 200,
    "statusMax": 399,
    "keyword": "Add to cart",
    "keywordAbsent": false,
    "followRedirects": true,
    "hasBody": false,
    "tlsWarnDays": 14,
    "dnsType": "A",
    "dnsExpect": null,
    "graceS": 300
  },
  "createdAt": "2026-10-20T08:00:00.000Z",
  "uptime": {
    "24h": 100,
    "7d": 99.98,
    "30d": 99.95,
    "90d": 99.97
  },
  "responseTime": {
    "range": "24h",
    "averageMs": 176,
    "maxMs": 412,
    "series": [
      171,
      168,
      174,
      182
    ]
  },
  "days": [
    {
      "date": "2026-10-21",
      "uptime": 100,
      "level": "operational",
      "incidents": [],
      "maintenance": 0
    }
  ]
}

Change a monitor

PUTPUT /monitors/:id

scope write · answers 200 · changeMonitor

Send any of the fields you can add a monitor with, except kind. A change to what is checked starts the monitor afresh (pending), and ends an outage in progress without a notice.

Parameters

idpath · string · required
namebody · string
intervalSbody · integer

Errors

invalid_requestinvalid_kindinvalid_nameinvalid_urlinvalid_interval_srate_limited

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Shop (EU)","intervalS":120}' \
  "$API/monitors/mon_k4q2m7abcdefgh4a"
answer 200
{
  "id": "mon_k4q2m7abcdefgh4a",
  "name": "Shop (EU)",
  "kind": "http",
  "targetText": "https://shop.example.com/",
  "target": "https://shop.example.com/",
  "port": null,
  "intervalS": 120,
  "timeoutS": 10,
  "email": true,
  "state": "pending",
  "stateSince": "2026-10-20T08:00:12.000Z",
  "lastCheckAt": "2026-10-21T09:14:03.000Z",
  "lastLatencyMs": 182,
  "lastStatus": 200,
  "lastError": null,
  "tlsExpiresAt": "2026-12-30T23:59:59.000Z",
  "pingUrl": null,
  "lastPingAt": null,
  "settings": {
    "method": "GET",
    "statusMin": 200,
    "statusMax": 399,
    "keyword": "Add to cart",
    "keywordAbsent": false,
    "followRedirects": true,
    "hasBody": false,
    "tlsWarnDays": 14,
    "dnsType": "A",
    "dnsExpect": null,
    "graceS": 300
  },
  "createdAt": "2026-10-20T08:00:00.000Z"
}

Delete a monitor

DELETEDELETE /monitors/:id

scope write · answers 204 · deleteMonitor

Deletes the monitor with its history, and takes it off your status pages.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/monitors/mon_k4q2m7abcdefgh4a"

204: no body.

Pause a monitor

POSTPOST /monitors/:id/pause

scope write · answers 200 · pauseMonitor

No checks and no notices until it is resumed; an outage in progress ends without a notice. Status pages show it as maintenance.

Parameters

idpath · string · required
curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/monitors/mon_k4q2m7abcdefgh4a/pause"
answer 200
{
  "id": "mon_k4q2m7abcdefgh4a",
  "name": "Shop",
  "kind": "http",
  "targetText": "https://shop.example.com/",
  "target": "https://shop.example.com/",
  "port": null,
  "intervalS": 60,
  "timeoutS": 10,
  "email": true,
  "state": "paused",
  "stateSince": "2026-10-20T08:00:12.000Z",
  "lastCheckAt": "2026-10-21T09:14:03.000Z",
  "lastLatencyMs": 182,
  "lastStatus": 200,
  "lastError": null,
  "tlsExpiresAt": "2026-12-30T23:59:59.000Z",
  "pingUrl": null,
  "lastPingAt": null,
  "settings": {
    "method": "GET",
    "statusMin": 200,
    "statusMax": 399,
    "keyword": "Add to cart",
    "keywordAbsent": false,
    "followRedirects": true,
    "hasBody": false,
    "tlsWarnDays": 14,
    "dnsType": "A",
    "dnsExpect": null,
    "graceS": 300
  },
  "createdAt": "2026-10-20T08:00:00.000Z"
}

Resume a monitor

POSTPOST /monitors/:id/resume

scope write · answers 200 · resumeMonitor

Checks again from now, starting afresh.

Parameters

idpath · string · required
curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/monitors/mon_k4q2m7abcdefgh4a/resume"
answer 200
{
  "id": "mon_k4q2m7abcdefgh4a",
  "name": "Shop",
  "kind": "http",
  "targetText": "https://shop.example.com/",
  "target": "https://shop.example.com/",
  "port": null,
  "intervalS": 60,
  "timeoutS": 10,
  "email": true,
  "state": "pending",
  "stateSince": "2026-10-20T08:00:12.000Z",
  "lastCheckAt": "2026-10-21T09:14:03.000Z",
  "lastLatencyMs": 182,
  "lastStatus": 200,
  "lastError": null,
  "tlsExpiresAt": "2026-12-30T23:59:59.000Z",
  "pingUrl": null,
  "lastPingAt": null,
  "settings": {
    "method": "GET",
    "statusMin": 200,
    "statusMax": 399,
    "keyword": "Add to cart",
    "keywordAbsent": false,
    "followRedirects": true,
    "hasBody": false,
    "tlsWarnDays": 14,
    "dnsType": "A",
    "dnsExpect": null,
    "graceS": 300
  },
  "createdAt": "2026-10-20T08:00:00.000Z"
}

List recent checks

GETGET /monitors/:id/checks

scope read · answers 200 · listRecentChecks

The monitor's checks of the last 48 hours, newest first (500 at most): passed or not, the response time, the status code and why it failed. For a heartbeat, its pings.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/monitors/mon_k4q2m7abcdefgh4a/checks"
answer 200
{
  "data": [
    {
      "at": "2026-10-21T09:14:03.000Z",
      "ok": true,
      "latencyMs": 182,
      "status": 200,
      "error": null
    }
  ]
}

List outages

GETGET /monitors/:id/incidents

scope read · answers 200 · listOutages

Confirmed outages of the last 90 days, newest first. quiet: no notice was sent (the monitor went down more than three times that hour).

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/monitors/mon_k4q2m7abcdefgh4a/incidents"
answer 200
{
  "data": [
    {
      "id": "mni_q7m2x4abcdefgh4c",
      "startedAt": "2026-10-18T22:41:05.000Z",
      "resolvedAt": "2026-10-18T22:52:40.000Z",
      "cause": "HTTP 502 (expected 200-399)",
      "quiet": false
    }
  ]
}

Status pages

List status pages

GETGET /status-pages

scope read · answers 200 · listStatusPages

Your public status pages, oldest first, with their addresses.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/status-pages"
answer 200
{
  "data": [
    {
      "id": "spg_m2k7q4abcdefgh4d",
      "slug": "northwind",
      "title": "Northwind Labs status",
      "logoText": "Northwind",
      "about": "Our shop and API.",
      "monitorIds": [
        "mon_k4q2m7abcdefgh4a"
      ],
      "indexable": false,
      "url": "https://rack4.io/s/northwind",
      "createdAt": "2026-10-20T08:10:00.000Z",
      "updatedAt": "2026-10-20T08:10:00.000Z"
    }
  ]
}

Publish a status page

POSTPOST /status-pages

scope write · answers 201 · publishStatusPage

slug (3 to 40 lowercase letters, digits and dashes: the page is at https://rack4.io/s/<slug>), title, and optionally logoText, about, monitorIds (up to 20, in display order) and indexable (default false: the page asks search engines not to list it). The page shows names, states and uptime, never what a monitor checks.

Parameters

slugbody · string
titlebody · string
logoTextbody · string
aboutbody · string
monitorIdsbody · list of string

Errors

invalid_requestinvalid_sluginvalid_titleinvalid_logo_textinvalid_aboutinvalid_monitor_idsinvalid_indexableslug_takentoo_many_status_pages

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"slug":"northwind","title":"Northwind Labs status","logoText":"Northwind","about":"Our shop and API.","monitorIds":["mon_k4q2m7abcdefgh4a"]}' \
  "$API/status-pages"
answer 201
{
  "id": "spg_m2k7q4abcdefgh4d",
  "slug": "northwind",
  "title": "Northwind Labs status",
  "logoText": "Northwind",
  "about": "Our shop and API.",
  "monitorIds": [
    "mon_k4q2m7abcdefgh4a"
  ],
  "indexable": false,
  "url": "https://rack4.io/s/northwind",
  "createdAt": "2026-10-20T08:10:00.000Z",
  "updatedAt": "2026-10-20T08:10:00.000Z",
  "notes": []
}

Show a status page

GETGET /status-pages/:id

scope read · answers 200 · showStatusPage

One page with its notes, newest first.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/status-pages/spg_m2k7q4abcdefgh4d"
answer 200
{
  "id": "spg_m2k7q4abcdefgh4d",
  "slug": "northwind",
  "title": "Northwind Labs status",
  "logoText": "Northwind",
  "about": "Our shop and API.",
  "monitorIds": [
    "mon_k4q2m7abcdefgh4a"
  ],
  "indexable": false,
  "url": "https://rack4.io/s/northwind",
  "createdAt": "2026-10-20T08:10:00.000Z",
  "updatedAt": "2026-10-20T08:10:00.000Z",
  "notes": [
    {
      "id": "spn_x4q7m2abcdefgh4e",
      "kind": "maintenance",
      "title": "Database upgrade",
      "body": "Sunday 02:00 to 02:30 UTC. Checkout may be slow.",
      "createdAt": "2026-10-21T09:00:00.000Z",
      "resolvedAt": null
    }
  ]
}

Change a status page

PUTPUT /status-pages/:id

scope write · answers 200 · changeStatusPage

Send any of the fields you can publish a page with. A new slug moves the page; the old address stops working.

Parameters

idpath · string · required
titlebody · string
indexablebody · boolean

Errors

invalid_requestinvalid_sluginvalid_titleinvalid_monitor_idsslug_taken

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"title":"Northwind Labs: service status","indexable":true}' \
  "$API/status-pages/spg_m2k7q4abcdefgh4d"
answer 200
{
  "id": "spg_m2k7q4abcdefgh4d",
  "slug": "northwind",
  "title": "Northwind Labs: service status",
  "logoText": "Northwind",
  "about": "Our shop and API.",
  "monitorIds": [
    "mon_k4q2m7abcdefgh4a"
  ],
  "indexable": true,
  "url": "https://rack4.io/s/northwind",
  "createdAt": "2026-10-20T08:10:00.000Z",
  "updatedAt": "2026-10-20T08:10:00.000Z",
  "notes": [
    {
      "id": "spn_x4q7m2abcdefgh4e",
      "kind": "maintenance",
      "title": "Database upgrade",
      "body": "Sunday 02:00 to 02:30 UTC. Checkout may be slow.",
      "createdAt": "2026-10-21T09:00:00.000Z",
      "resolvedAt": null
    }
  ]
}

Delete a status page

DELETEDELETE /status-pages/:id

scope write · answers 204 · deleteStatusPage

Deletes the page and its notes. Its address stops working at once.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/status-pages/spg_m2k7q4abcdefgh4d"

204: no body.

Post a note

POSTPOST /status-pages/:id/notes

scope write · answers 201 · postNote

kind: incident, maintenance or notice; title (120 characters) and body (2000). Open notes show at the top of the page; an open incident marks the page as partly down.

Parameters

idpath · string · required
kindbody · string
titlebody · string
bodybody · string

Errors

invalid_requestinvalid_kindinvalid_titleinvalid_body

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"kind":"maintenance","title":"Database upgrade","body":"Sunday 02:00 to 02:30 UTC. Checkout may be slow."}' \
  "$API/status-pages/spg_m2k7q4abcdefgh4d/notes"
answer 201
{
  "id": "spn_x4q7m2abcdefgh4e",
  "kind": "maintenance",
  "title": "Database upgrade",
  "body": "Sunday 02:00 to 02:30 UTC. Checkout may be slow.",
  "createdAt": "2026-10-21T09:00:00.000Z",
  "resolvedAt": null
}

Resolve a note

POSTPOST /status-pages/:id/notes/:noteId/resolve

scope write · answers 200 · resolveNote

Marks the note resolved: it moves to the page's past 14 days.

Parameters

idpath · string · required
noteIdpath · string · required
curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/status-pages/spg_m2k7q4abcdefgh4d/notes/spn_x4q7m2abcdefgh4e/resolve"
answer 200
{
  "id": "spn_x4q7m2abcdefgh4e",
  "kind": "maintenance",
  "title": "Database upgrade",
  "body": "Sunday 02:00 to 02:30 UTC. Checkout may be slow.",
  "createdAt": "2026-10-21T09:00:00.000Z",
  "resolvedAt": "2026-10-26T02:31:00.000Z"
}

Delete a note

DELETEDELETE /status-pages/:id/notes/:noteId

scope write · answers 204 · deleteNote

Removes the note from the page.

Parameters

idpath · string · required
noteIdpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/status-pages/spg_m2k7q4abcdefgh4d/notes/spn_x4q7m2abcdefgh4e"

204: no body.

Notification channels

List channels

GETGET /channels

scope read · answers 200 · listChannels

Your notification channels, oldest first (push devices are under /push-devices).

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/channels"
answer 200
{
  "data": [
    {
      "id": "chn_s4k7q2abcdefgh3f",
      "kind": "slack",
      "name": "#ops",
      "target": "hooks.slack.com/services/…k3Xz",
      "categories": [
        "servers",
        "alerts"
      ],
      "state": "active",
      "enabled": true,
      "disabledReason": null,
      "failureStreak": 0,
      "lastSuccessAt": "2026-10-21T09:14:01.000Z",
      "lastFailureAt": null,
      "createdAt": "2026-10-20T08:00:00.000Z",
      "pairing": null
    }
  ]
}

Add a channel

POSTPOST /channels

scope write · answers 201 · addChannel

kind: slack, discord, teams or ntfy (send url: an incoming webhook, a channel webhook, a Teams Workflows URL, or an ntfy topic URL on any public server, with an optional token for a protected topic), telegram (no url; the answer has the pairing links), or email (send email; it gets a confirmation link first). name is optional. Slack, Discord and Teams URLs must be on their service's own hosts; every URL must be https on a public address.

Parameters

kindbody · string
namebody · string
urlbody · string
categoriesbody · list of string

Errors

invalid_requestinvalid_kindinvalid_nameinvalid_urlinvalid_tokeninvalid_emailinvalid_categoriestoo_many_channelschannel_kind_unavailable

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"kind":"slack","name":"#ops","url":"https://hooks.slack.com/services/T0000/B0000/XXXXXXXXXXXX","categories":["servers","alerts"]}' \
  "$API/channels"
answer 201
{
  "id": "chn_s4k7q2abcdefgh3f",
  "kind": "slack",
  "name": "#ops",
  "target": "hooks.slack.com/services/…k3Xz",
  "categories": [
    "servers",
    "alerts"
  ],
  "state": "active",
  "enabled": true,
  "disabledReason": null,
  "failureStreak": 0,
  "lastSuccessAt": "2026-10-21T09:14:01.000Z",
  "lastFailureAt": null,
  "createdAt": "2026-10-20T08:00:00.000Z",
  "pairing": null
}

Show a channel

GETGET /channels/:id

scope read · answers 200 · showChannel

One channel. A pending Telegram channel carries its pairing links until they expire.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/channels/chn_t7m2x4abcdefgh3g"
answer 200
{
  "id": "chn_t7m2x4abcdefgh3g",
  "kind": "telegram",
  "name": "on-call phone",
  "target": "not paired yet",
  "categories": [
    "servers",
    "alerts"
  ],
  "state": "pending",
  "enabled": true,
  "disabledReason": null,
  "failureStreak": 0,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "createdAt": "2026-10-20T08:00:00.000Z",
  "pairing": {
    "link": "https://t.me/VpsNineBot?start=q2x7m4k3j5abcdefgh2r7t4k",
    "groupLink": "https://t.me/VpsNineBot?startgroup=q2x7m4k3j5abcdefgh2r7t4k",
    "expiresAt": "2026-10-21T09:44:00.000Z"
  }
}

Change a channel

PUTPUT /channels/:id

scope write · answers 200 · changeChannel

Send any of name, categories, enabled, and for Slack, Discord, Teams and ntfy a new url (and token). Turning a channel on clears its failure count.

Parameters

idpath · string · required
categoriesbody · list of string
enabledbody · boolean

Errors

invalid_requestinvalid_nameinvalid_urlinvalid_tokeninvalid_categories

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"categories":["servers","alerts","billing"],"enabled":true}' \
  "$API/channels/chn_s4k7q2abcdefgh3f"
answer 200
{
  "id": "chn_s4k7q2abcdefgh3f",
  "kind": "slack",
  "name": "#ops",
  "target": "hooks.slack.com/services/…k3Xz",
  "categories": [
    "servers",
    "alerts",
    "billing"
  ],
  "state": "active",
  "enabled": true,
  "disabledReason": null,
  "failureStreak": 0,
  "lastSuccessAt": "2026-10-21T09:14:01.000Z",
  "lastFailureAt": null,
  "createdAt": "2026-10-20T08:00:00.000Z",
  "pairing": null
}

Delete a channel

DELETEDELETE /channels/:id

scope write · answers 204 · deleteChannel

Deletes the channel and its delivery log. Nothing more is sent to it.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/channels/chn_s4k7q2abcdefgh3f"

204: no body.

Send a test message

POSTPOST /channels/:id/test

scope write · answers 202 · sendTestMessage

Queues a test message to the channel now (one attempt, whatever its categories); 5 a minute per account. The channel must be on and confirmed or paired.

Parameters

idpath · string · required

Errors

rate_limitedchannel_not_ready

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/channels/chn_s4k7q2abcdefgh3f/test"
answer 202
{
  "id": "chd_d4t2k7abcdefgh3j",
  "eventType": "test",
  "title": "Test message from Rack4",
  "state": "pending",
  "attempts": 1,
  "status": null,
  "error": null,
  "latencyMs": null,
  "createdAt": "2026-10-21T09:14:00.000Z",
  "nextAttemptAt": null
}

List channel deliveries

GETGET /channels/:id/deliveries

scope read · answers 200 · listChannelDeliveries

The channel's last 50 messages, newest first: state, attempts, the service's HTTP status and the error, if any.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/channels/chn_s4k7q2abcdefgh3f/deliveries"
answer 200
{
  "data": [
    {
      "id": "chd_d4t2k7abcdefgh3j",
      "eventType": "alert.triggered",
      "title": "web-1: CPU usage above 90% for 10 minutes",
      "state": "succeeded",
      "attempts": 1,
      "status": 200,
      "error": null,
      "latencyMs": 214,
      "createdAt": "2026-10-21T09:14:00.000Z",
      "nextAttemptAt": null
    }
  ]
}

List push devices

GETGET /push-devices

scope read · answers 200 · listPushDevices

Browsers and installed panels with push notifications on (up to 10), oldest first. A push carries a title and a panel link only.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/push-devices"
answer 200
{
  "data": [
    {
      "id": "chn_p2q7m4abcdefgh3h",
      "name": "Chrome on Windows",
      "categories": [
        "servers",
        "alerts",
        "billing"
      ],
      "createdAt": "2026-10-20T18:00:00.000Z",
      "lastSuccessAt": "2026-10-21T09:14:02.000Z",
      "failureStreak": 0
    }
  ]
}

Change a push device

PUTPUT /push-devices/:id

scope write · answers 200 · changePushDevice

Send name and/or categories.

Parameters

idpath · string · required
categoriesbody · list of string

Errors

invalid_requestinvalid_nameinvalid_categories

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"categories":["alerts"]}' \
  "$API/push-devices/chn_p2q7m4abcdefgh3h"
answer 200
{
  "id": "chn_p2q7m4abcdefgh3h",
  "name": "Chrome on Windows",
  "categories": [
    "alerts"
  ],
  "createdAt": "2026-10-20T18:00:00.000Z",
  "lastSuccessAt": "2026-10-21T09:14:02.000Z",
  "failureStreak": 0
}

Remove a push device

DELETEDELETE /push-devices/:id

scope write · answers 204 · removePushDevice

The device gets no more push notifications. The browser keeps its permission; turning push on again from the panel adds it back.

Parameters

idpath · string · required
curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/push-devices/chn_p2q7m4abcdefgh3h"

204: no body.

Recycle bin

List the recycle bin

GETGET /recycle-bin

scope read · answers 200 · listRecycleBin

Servers you deleted that are still kept: state deleted, purgeAt when the disk is wiped for good. Their addresses stay reserved for them until then. restore says whether it can be restored now, and the open invoice of a restore already ordered.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/recycle-bin"
answer 200
{
  "data": [
    {
      "id": "srv_k3j5m2abcdefgh2q",
      "hostname": "web-1",
      "plan": "standard",
      "image": "debian-12",
      "state": "deleted",
      "pendingAction": null,
      "ipv4": "203.0.113.10",
      "ipv6": "2001:db8:10::2",
      "vcpus": 2,
      "memoryMiB": 4096,
      "diskGiB": 80,
      "createdAt": "2026-10-09T12:00:00.000Z",
      "stateSince": "2026-10-21T09:00:00.000Z",
      "app": "docker",
      "deletionProtected": false,
      "purgeAt": "2026-10-28T09:00:00.000Z",
      "deletedAt": "2026-10-21T09:00:00.000Z",
      "restore": {
        "available": true,
        "reason": null,
        "invoiceId": null
      }
    }
  ]
}

Restore a deleted server

POSTPOST /recycle-bin/:id/restore

scope write · answers 202 · restoreDeletedServer

Orders the server back, with its disk and its addresses: an invoice for a new billing period (term month or year, at today's prices, with the add-ons it had), paid like any order; account credit applies. Once it is paid the server boots again, billed from that day. Asking again while that invoice is unpaid answers the same invoice.

Parameters

idpath · string · required
termbody · string

Errors

invalid_requestinvalid_termnot_restorabletoo_many_open_ordersstaff_onlyemail_unverifiedrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"term":"month"}' \
  "$API/recycle-bin/srv_k3j5m2abcdefgh2q/restore"
answer 202
{
  "order": {
    "id": "ord_x2m4q7abcdefgh2u"
  },
  "invoice": {
    "id": "inv_q4m7x2abcdefgh2t",
    "number": "R4-000043",
    "kind": "order",
    "status": "open",
    "currency": "USD",
    "subtotalCents": 1299,
    "creditAppliedCents": 0,
    "totalCents": 1299,
    "issuedAt": "2026-10-21T09:30:00.000Z",
    "dueAt": "2026-10-21T09:30:00.000Z",
    "paidAt": null,
    "voidedAt": null,
    "serverId": null,
    "term": "month"
  },
  "payUrl": "https://my.rack4.io/billing/invoices/inv_q4m7x2abcdefgh2t"
}

Delete a server forever

DELETEDELETE /recycle-bin/:id

scope write · answers 202 · deleteServerForever

Wipes the kept disk now instead of at purgeAt, and releases the addresses. It can't be undone. No body.

Parameters

idpath · string · required

Errors

action_pendinginvalid_state

curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/recycle-bin/srv_k3j5m2abcdefgh2q"
answer 202
{
  "id": "srv_k3j5m2abcdefgh2q",
  "hostname": "web-1",
  "plan": "standard",
  "image": "debian-12",
  "state": "deleted",
  "pendingAction": "purge",
  "ipv4": "203.0.113.10",
  "ipv6": "2001:db8:10::2",
  "vcpus": 2,
  "memoryMiB": 4096,
  "diskGiB": 80,
  "createdAt": "2026-10-09T12:00:00.000Z",
  "stateSince": "2026-10-21T09:00:00.000Z",
  "app": "docker",
  "deletionProtected": false,
  "purgeAt": "2026-10-28T09:00:00.000Z"
}

AI assistant

Show the AI assistant setting

GETGET /assistant

scope read · answers 200 · showAiAssistantSetting

Your own account's AI assistant setting. mode: on (answers questions in the panel and adds a short explanation to alert and failure notifications), on_demand (only when someone asks) or off. available: whether the assistant runs right now; questionsLeftToday and dailyLimit: your account's questions (UTC day).

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/assistant"
answer 200
{
  "mode": "on",
  "available": true,
  "questionsLeftToday": 28,
  "dailyLimit": 30
}

Change the AI assistant setting

PUTPUT /assistant

scope write · answers 200 · changeAiAssistantSetting

Sets mode (on, on_demand or off) for your own account. off also stops questions from team members acting on it. Answers with the setting.

Parameters

modebody · string

Errors

invalid_modeassistant_unavailable

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"mode":"on_demand"}' \
  "$API/assistant"
answer 200
{
  "mode": "on_demand",
  "available": true,
  "questionsLeftToday": 28,
  "dailyLimit": 30
}

Support

List tickets

GETGET /tickets

scope read · answers 200 · listTickets

Your support tickets, latest activity first (100 at most). status is open (waiting on us), pending (we answered) or closed.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/tickets"
answer 200
{
  "data": [
    {
      "id": "tkt_m4q2x7abcdefgh2u",
      "subject": "web-1 doesn't answer on port 443",
      "category": "technical",
      "priority": "high",
      "status": "open",
      "serverId": "srv_k3j5m2abcdefgh2q",
      "createdAt": "2026-10-12T09:00:00.000Z",
      "updatedAt": "2026-10-12T09:00:00.000Z",
      "closedAt": null,
      "rating": null
    }
  ]
}

Open a ticket

POSTPOST /tickets

scope write · answers 201 · openTicket

Opens a ticket and emails you a receipt. category is technical, billing, sales, abuse, account or other (other when left out); priority is low, normal (the default), high or urgent; serverId (optional) links one of your servers. Plain text; put logs between lines of ``` to keep their layout. 5 new tickets an hour and 20 a day per account.

Parameters

subjectbody · string
categorybody · string
prioritybody · string
serverIdbody · string
bodybody · string

Errors

invalid_requestinvalid_subjectinvalid_categoryinvalid_priorityinvalid_serverinvalid_bodyrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"subject":"web-1 doesn't answer on port 443","category":"technical","priority":"high","serverId":"srv_k3j5m2abcdefgh2q","body":"Nginx is running and ufw allows 443, but curl from outside times out."}' \
  "$API/tickets"
answer 201
{
  "id": "tkt_m4q2x7abcdefgh2u",
  "subject": "web-1 doesn't answer on port 443",
  "category": "technical",
  "priority": "high",
  "status": "open",
  "serverId": "srv_k3j5m2abcdefgh2q",
  "createdAt": "2026-10-12T09:00:00.000Z",
  "updatedAt": "2026-10-12T09:00:00.000Z",
  "closedAt": null,
  "rating": null,
  "messages": [
    {
      "id": 41,
      "at": "2026-10-12T09:00:00.000Z",
      "from": "you",
      "body": "Nginx is running and ufw allows 443, but curl from outside times out."
    }
  ]
}

Show a ticket

GETGET /tickets/:id

scope read · answers 200 · showTicket

One ticket and its messages, oldest first. from is you or support.

Parameters

idpath · string · required
curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/tickets/tkt_m4q2x7abcdefgh2u"
answer 200
{
  "id": "tkt_m4q2x7abcdefgh2u",
  "subject": "web-1 doesn't answer on port 443",
  "category": "technical",
  "priority": "high",
  "status": "pending",
  "serverId": "srv_k3j5m2abcdefgh2q",
  "createdAt": "2026-10-12T09:00:00.000Z",
  "updatedAt": "2026-10-12T09:00:00.000Z",
  "closedAt": null,
  "rating": null,
  "messages": [
    {
      "id": 41,
      "at": "2026-10-12T09:00:00.000Z",
      "from": "you",
      "body": "Nginx is running and ufw allows 443, but curl from outside times out."
    },
    {
      "id": 44,
      "at": "2026-10-12T09:20:00.000Z",
      "from": "support",
      "body": "Your server's own firewall has 443 open, but the panel firewall doesn't: add a rule for tcp 443."
    }
  ]
}

Reply to a ticket

POSTPOST /tickets/:id/replies

scope write · answers 201 · replyToTicket

Adds your message and reopens the ticket for us. 30 replies an hour per account. A closed ticket takes no replies: open a new one.

Parameters

idpath · string · required
bodybody · string

Errors

invalid_requestinvalid_bodyticket_closedrate_limited

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"body":"That was it, thanks. Works now."}' \
  "$API/tickets/tkt_m4q2x7abcdefgh2u/replies"
answer 201
{
  "id": "tkt_m4q2x7abcdefgh2u",
  "subject": "web-1 doesn't answer on port 443",
  "category": "technical",
  "priority": "high",
  "status": "open",
  "serverId": "srv_k3j5m2abcdefgh2q",
  "createdAt": "2026-10-12T09:00:00.000Z",
  "updatedAt": "2026-10-12T09:00:00.000Z",
  "closedAt": null,
  "rating": null,
  "messages": [
    {
      "id": 41,
      "at": "2026-10-12T09:00:00.000Z",
      "from": "you",
      "body": "Nginx is running and ufw allows 443, but curl from outside times out."
    },
    {
      "id": 45,
      "at": "2026-10-12T09:31:00.000Z",
      "from": "you",
      "body": "That was it, thanks. Works now."
    }
  ]
}

Close a ticket

POSTPOST /tickets/:id/close

scope write · answers 200 · closeTicket

Closes the ticket. You can rate a closed ticket in the panel.

Parameters

idpath · string · required

Errors

ticket_closed

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/tickets/tkt_m4q2x7abcdefgh2u/close"
answer 200
{
  "id": "tkt_m4q2x7abcdefgh2u",
  "subject": "web-1 doesn't answer on port 443",
  "category": "technical",
  "priority": "high",
  "status": "closed",
  "serverId": "srv_k3j5m2abcdefgh2q",
  "createdAt": "2026-10-12T09:00:00.000Z",
  "updatedAt": "2026-10-12T09:00:00.000Z",
  "closedAt": "2026-10-12T09:32:00.000Z",
  "rating": null,
  "messages": [
    {
      "id": 41,
      "at": "2026-10-12T09:00:00.000Z",
      "from": "you",
      "body": "Nginx is running and ufw allows 443, but curl from outside times out."
    }
  ]
}

Teams

List the team

GETGET /team

scope read · answers 200 · listTeam

The people on the account (the owner first) with their roles, and the invites still waiting. Every role can look.

curl
curl -s -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/team"
answer 200
{
  "account": {
    "id": "usr_a2b3c4d5e6f7g2h3",
    "name": "Acme ops",
    "role": "admin",
    "require2fa": true
  },
  "members": [
    {
      "id": "usr_a2b3c4d5e6f7g2h3",
      "email": "owner@example.com",
      "role": "owner",
      "since": "2026-10-01T09:00:00.000Z",
      "twoFactor": true
    },
    {
      "id": "usr_t4m7x2abcdefgh2u",
      "email": "dev@example.com",
      "role": "technical",
      "since": "2026-10-12T15:30:00.000Z",
      "twoFactor": true
    }
  ],
  "invites": [
    {
      "id": "tinv_r5n8x2abcdefgh2v",
      "email": "books@example.com",
      "role": "billing",
      "invitedBy": "owner@example.com",
      "createdAt": "2026-10-20T10:00:00.000Z",
      "expiresAt": "2026-10-27T10:00:00.000Z"
    }
  ]
}

Invite someone

POSTPOST /team/invites

scope write · answers 201 · inviteSomeone

Emails an invite link that works once, for 7 days, for exactly this address. role: admin, billing, technical or readonly. Owners and admins only, with a confirmed email (403 email_unverified until then); 20 invites an hour per account.

Parameters

emailbody · string
rolebody · string

Errors

invalid_emailinvalid_rolealready_memberinvite_pendingtoo_many_invitestoo_many_membersforbidden_roleemail_unverifiedrate_limitedinvalid_request

curl
curl -s -X POST -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"email":"books@example.com","role":"billing"}' \
  "$API/team/invites"
answer 201
{
  "id": "tinv_r5n8x2abcdefgh2v",
  "email": "books@example.com",
  "role": "billing",
  "invitedBy": "owner@example.com",
  "createdAt": "2026-10-20T10:00:00.000Z",
  "expiresAt": "2026-10-27T10:00:00.000Z"
}

Revoke an invite

DELETEDELETE /team/invites/:id

scope write · answers 204 · revokeInvite

Its link stops working. Owners and admins only.

Parameters

idpath · string · required

Errors

forbidden_role

curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/team/invites/tinv_r5n8x2abcdefgh2v"

204: no body.

Change a role

PUTPUT /team/members/:userId

scope write · answers 200 · changeRole

Sets a member's role (not the owner's, and not your own). Their tokens follow the new role at once. Owners and admins only.

Parameters

userIdpath · string · required
rolebody · string

Errors

invalid_roleown_roleowner_roleforbidden_roleinvalid_request

curl
curl -s -X PUT -H "Authorization: Bearer $RACK4_TOKEN" -H "Content-Type: application/json" \
  -d '{"role":"readonly"}' \
  "$API/team/members/usr_t4m7x2abcdefgh2u"
answer 200
{
  "id": "usr_t4m7x2abcdefgh2u",
  "email": "dev@example.com",
  "role": "readonly",
  "since": "2026-10-12T15:30:00.000Z",
  "twoFactor": true
}

Remove a member

DELETEDELETE /team/members/:userId

scope write · answers 204 · removeMember

Removes them from the account, signs out their sessions on it and revokes their tokens for it. Not the owner, not yourself. Owners and admins only.

Parameters

userIdpath · string · required

Errors

owner_roleremove_selfforbidden_role

curl
curl -s -X DELETE -H "Authorization: Bearer $RACK4_TOKEN" \
  "$API/team/members/usr_t4m7x2abcdefgh2u"

204: no body.