API quickstart
Call the VPSNine API with curl and a bearer token. Check who you are, manage SSH keys, reboot a server, and read the errors it returns.
The API lives at https://my.vpsnine.com/api/v1. It speaks JSON and takes an API token as a bearer token. It covers your account, SSH keys, servers, firewalls, snapshots, reverse DNS and invoices. The full reference lists every endpoint with an example, and the vps9 CLI uses this same API.
All you need is curl, and jq makes the output nicer (apt install -y jq).
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):
export VPS9_TOKEN="$(cat ~/.vps9-token)"
API=https://my.vpsnine.com/api/v1Then ask who you are:
curl -s -H "Authorization: Bearer $VPS9_TOKEN" "$API/me"{"id":"usr_…","email":"[email protected]","emailVerified":true,"createdAt":"2026-10-09T12:00:00.000Z"}If that comes back, your token works.
SSH keys
List them:
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:
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:
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:
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:
curl -s -H "Authorization: Bearer $VPS9_TOKEN" "$API/servers" | jq -r '.data[] | "\(.id) \(.hostname) \(.state)"'Reboot one by its id:
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.
When it says no
Errors are JSON too, with a code you can match on and a message meant for people:
{"error":{"code":"insufficient_scope","message":"This token does not have the write scope."}}| 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. |
| 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.