# 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.

Source: https://vpsnine.com/help/api-quickstart · Updated: 2026-10-10

The API lives at `https://my.vpsnine.com/api/v1`. It speaks JSON and takes an [API token](https://vpsnine.com/help/api-tokens) as a bearer token. It covers your account, SSH keys, servers, firewalls, snapshots, reverse DNS and invoices. The [full reference](https://my.vpsnine.com/api) lists every endpoint with an example, and the [vps9 CLI](https://vpsnine.com/help/install-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](https://vpsnine.com/help/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.

## 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.

## 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."}}
```

| 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](https://vpsnine.com/help/verify-your-email) 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.
