API quickstart

Your first VPSNine API calls in curl, JavaScript, Python, the TypeScript SDK and the vps9 CLI, side by side: list servers, reboot one, read errors.

To call the VPSNine API, send a request to https://my.vpsnine.com/api/v1 with your API token in an Authorization: Bearer header; answers are JSON. Below, the same calls are shown in curl, JavaScript, Python, the TypeScript SDK and the vps9 CLI, so you can start in whichever you use.

The API covers your account, SSH keys, servers, firewalls, snapshots, reverse DNS, invoices, webhooks and more. The full reference lists every endpoint with an example in each language and a Try it button, and the vps9 CLI uses this same API.

An endpoint in the API reference: List servers, with tabs for curl, JavaScript, Python, TypeScript and the vps9 CLI, and the example answer
Every endpoint in the reference has these tabs. Your choice of language sticks as you move between endpoints.

What you need

  • An API token, made under tokens in the panel.
  • One of: curl (with jq to make the output readable: apt install -y jq), Node.js 18 or later, Python 3 with requests, or the vps9 CLI.

Set up

Make a token under tokens in the panel. read only can make every GET request. Anything that changes something, like adding a key or rebooting a server, needs read and write.

Read the token from a file so it stays out of your shell history (Create and use API tokens shows how to store it):

bash
export VPS9_TOKEN="$(cat ~/.vps9-token)"
API=https://my.vpsnine.com/api/v1

Then ask who you are:

bash
curl -s -H "Authorization: Bearer $VPS9_TOKEN" "$API/me"
json
{"id":"usr_…","email":"you@example.com","emailVerified":true,"createdAt":"2026-10-09T12:00:00.000Z"}

If that comes back, your token works.

The same calls in five tools

List your servers. The JavaScript and Python versions read the token from VPS9_TOKEN and stop with the API's error code, message and request id if something is wrong:

bash
curl -s -H "Authorization: Bearer $VPS9_TOKEN" "$API/servers" | jq -r '.data[] | "\(.hostname) \(.state) \(.ipv4)"'
js
// servers.mjs: node servers.mjs (Node 18 or later; fetch is built in)
const API = process.env.VPS9_API_URL ?? "https://my.vpsnine.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.VPS9_TOKEN}` };

const res = await fetch(`${API}/servers`, { headers });
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.requestId})`);
for (const s of body.data) console.log(s.hostname, s.state, s.ipv4);
python
# servers.py: python3 servers.py (pip install requests)
import os
import requests

API = os.environ.get("VPS9_API_URL", "https://my.vpsnine.com/api/v1")
headers = {"Authorization": f"Bearer {os.environ['VPS9_TOKEN']}"}

res = requests.get(f"{API}/servers", headers=headers, timeout=30)
body = res.json()
if not res.ok:
    raise SystemExit(f"{body['error']['code']}: {body['error']['message']} ({body['error']['requestId']})")
for s in body["data"]:
    print(s["hostname"], s["state"], s["ipv4"])
ts
// With the TypeScript SDK (not on npm yet; see the SDK tutorial)
import { Vps9 } from "@vpsnine/sdk";

const vps9 = new Vps9(); // reads VPS9_TOKEN
for (const s of (await vps9.listServers()).data) console.log(s.hostname, s.state, s.ipv4);
bash
vps9 servers

Reboot one. It answers 202 at once with pendingAction set to reboot:

bash
curl -s -X POST -H "Authorization: Bearer $VPS9_TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"reboot"}' "$API/servers/srv_…/actions"
js
const res = await fetch(`${API}/servers/srv_…/actions`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.VPS9_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ action: "reboot" }),
});
console.log(res.status, (await res.json()).pendingAction);
python
res = requests.post(
    f"{API}/servers/srv_…/actions",
    headers={"Authorization": f"Bearer {os.environ['VPS9_TOKEN']}"},
    json={"action": "reboot"},
    timeout=30,
)
print(res.status_code, res.json()["pendingAction"])
ts
await vps9.serverAction("srv_…", { action: "reboot" });
bash
vps9 servers reboot web-1

We ran each of these against a local copy of the API (Node 18 and 22, Python 3.12, curl 8.5 with jq 1.7, and the vps9 CLI built from the same source) before publishing them. requests sets Content-Type: application/json for you when you pass json=; with fetch and curl you set it yourself, or the API answers 415.

SSH keys

List them:

bash
curl -s -H "Authorization: Bearer $VPS9_TOKEN" "$API/ssh-keys"

You get an object with a data list. Each key has an id, name, type, fingerprint, publicKey and createdAt. With jq, just the names and fingerprints:

bash
curl -s -H "Authorization: Bearer $VPS9_TOKEN" "$API/ssh-keys" | jq -r '.data[] | "\(.name)  \(.fingerprint)"'

Add one. Let jq build the JSON so the key text gets quoted properly:

bash
jq -n --arg name laptop --arg key "$(cat ~/.ssh/id_ed25519.pub)" '{name: $name, publicKey: $key}' |
  curl -s -X POST -H "Authorization: Bearer $VPS9_TOKEN" -H "Content-Type: application/json" --data @- "$API/ssh-keys"

A new key comes back with status 201 and its id. The rules match the panel: Ed25519, ECDSA, or RSA of 2048 bits or more, one key per request, 50 at most. name is optional.

Delete one by its id:

bash
curl -s -X DELETE -H "Authorization: Bearer $VPS9_TOKEN" "$API/ssh-keys/key_…"

That returns 204 and no body.

Servers

List them, showing the id, hostname and state:

bash
curl -s -H "Authorization: Bearer $VPS9_TOKEN" "$API/servers" | jq -r '.data[] | "\(.id)  \(.hostname)  \(.state)"'

Reboot one by its id:

bash
curl -s -X POST -H "Authorization: Bearer $VPS9_TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"reboot"}' "$API/servers/srv_…/actions"

It answers 202 straight away, with the server's pendingAction set to reboot. The reboot itself happens in the background, so fetch the server again to see when it's done. start, stop, reinstall and delete work the same way. The API doesn't ask you to confirm a reinstall or delete, so check the id first.

Orders open at launch. When they do, POST /servers places one. Send an Idempotency-Key header with a value you choose, a UUID for example, and keep it until you get an answer. If the request times out, send it again with the same key, and you get the order you already placed instead of a second one.

An order can also name a one-click app in app, a list of extra distribution packages in packages (20 at most) and a first-boot script in setupScript (16 KiB at most). All three are optional:

json
{"plan":"standard","image":"ubuntu-24.04","hostname":"web-1","sshKeyIds":["key_…"],"app":"docker","packages":["htop"],"setupScript":"#!/bin/bash\nset -eu\n…"}

Extra packages and setup scripts shows how to send a script file with jq.

When it says no

Errors are JSON too, with a code you can match on and a message meant for people:

json
{"error":{"code":"insufficient_scope","message":"This token does not have the write scope.","requestId":"req_7d2kq4m5x3n6p2r4s7t2v3w4"}}

requestId is also in the X-Request-Id header of every answer. Put it in a support ticket and we can find that exact request. Developers covers request ids, rate-limit headers and safe retries.

Status Code What it means
401 unauthorized Token missing, wrong, revoked or expired.
400 invalid_json The body isn't valid JSON.
403 insufficient_scope A read-only token tried to change something.
403 email_unverified Ordering needs a confirmed email address. Confirm it first.
404 not_found No such key or server, or no such endpoint.
405 method_not_allowed The path exists, but not with that method. The Allow header lists the ones it takes.
409 duplicate_key That key's already on your account.
409 action_pending, invalid_state The server is busy with another action, or in a state where that action can't run.
413 body_too_large Bodies are 16 KB at most, or 64 KB for POST /servers and server actions, which can carry a setup script.
415 unsupported_media_type A body sent without -H "Content-Type: application/json". Plain curl -d sends a form, which the API refuses.
422 invalid_request The body isn't a JSON object, or has a field the endpoint doesn't know or of the wrong type (a number where text goes, say).
422 invalid_key, invalid_name, too_many_keys The key or name was refused; the message says why.
429 rate_limited Over 60 requests a minute on this token, or 120 across all your account's tokens. Wait the Retry-After seconds.

Add -i to any of these curl commands to see the status line and headers. And don't bother trying a browser cookie: the API ignores cookies entirely. A token is the only way in.

Next steps

Be first when orders open