# API rate limits and request ids

> How many requests the VPSNine API takes a minute, the X-RateLimit headers, handling 429 with Retry-After, and using X-Request-Id when you ask us about a call.

Source: https://vpsnine.com/help/api-rate-limits-and-request-ids · Updated: 2026-10-21

The VPSNine API takes 60 requests a minute per token and 120 a minute per account across all its tokens. Past either limit it answers `429 rate_limited` with a `Retry-After` header in seconds; every answer also carries an `X-Request-Id` that lets us find that exact request if you ask us about it.

## What you need

- An [API token](https://vpsnine.com/help/api-tokens) and `curl`. The examples read the token from `VPS9_TOKEN` and the base address from `API`:

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

## The limits

| What | Limit | Counted per |
| --- | --- | --- |
| Any request with a token | 60 a minute | token |
| Any request with a token | 120 a minute | account, all tokens together |
| `GET /prices` and `GET /status` (no token) | 60 a minute | address |
| Orders (`POST /servers`) | 10 an hour, failed tries included, panel orders too | account |
| Reverse DNS changes | 10 an hour | account |
| Starting rescue mode | 10 an hour | account |
| Webhook test events, and redeliveries | 5 a minute each | account |

A minute is a fixed window that starts with your first request in it, not a rolling one. More tokens don't buy more requests: the account limit is shared.

## Read where you stand

Every answer to a request with a token has three headers:

```bash
curl -sS -o /dev/null -D - -H "Authorization: Bearer $VPS9_TOKEN" "$API/me" | grep -i '^x-'
```

```text
X-Request-Id: req_a3o27yn6jdcya34oexlef5bg
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 56
X-RateLimit-Reset: 60
```

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | 60, or 120 once the account's limit is the nearer one |
| `X-RateLimit-Remaining` | Requests left in this window |
| `X-RateLimit-Reset` | Seconds until the window starts again |

When `Remaining` gets low, slow down rather than waiting for the 429. A script that polls a server every 5 seconds uses 12 requests a minute, which leaves room for everything else.

## When you get a 429

The answer looks like this, with `Retry-After` set to the seconds until the window resets:

```text
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60
Retry-After: 60

{"error":{"code":"rate_limited","message":"Too many requests: 60 a minute per token.","requestId":"req_yk6yypmf4w2uh5dmylbyqssl"}}
```

A 429 means nothing happened, so the same request is always safe to send again after the wait. `500`, `502`, `503` and `504` are worth retrying too, with care for a `POST`: see [Idempotent orders](https://vpsnine.com/help/idempotent-orders-with-idempotency-key).

In a shell script, this function waits for `Retry-After` and tries again, up to five times. It prints the body and fails on any other error:

```bash
# call PATH [curl options]: one API request, waiting and trying again on 429, 500, 502, 503 and 504 (5 tries).
call() {
  local path=$1; shift
  local headers body status wait
  headers="$(mktemp)"
  for attempt in 1 2 3 4 5; do
    body="$(curl -sS -D "$headers" -w '\n%{http_code}' -H "Authorization: Bearer $VPS9_TOKEN" "$@" "$API$path")"
    status="${body##*$'\n'}"; body="${body%$'\n'*}"
    case "$status" in
      429|500|502|503|504)
        wait="$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' "$headers")"
        wait="${wait:-$((2 ** attempt))}"
        echo "got $status, waiting ${wait}s (try $attempt)" >&2
        sleep "$wait" ;;
      *) rm -f "$headers"; printf '%s\n' "$body"; [ "$status" -lt 400 ]; return ;;
    esac
  done
  rm -f "$headers"; printf '%s\n' "$body"; return 1
}

call /me | jq -r .email
```

The same in Python with `requests`:

```python
import os, time
import requests

API = os.environ.get("VPS9_API_URL", "https://my.vpsnine.com/api/v1")
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['VPS9_TOKEN']}"
RETRY = {429, 500, 502, 503, 504}


def call(method: str, path: str, **kwargs) -> requests.Response:
    """One API request; waits and tries again on 429 and 5xx, up to 5 tries. Don't use it for POST /servers without an Idempotency-Key."""
    for attempt in range(1, 6):
        res = session.request(method, API + path, timeout=30, **kwargs)
        if res.status_code not in RETRY or attempt == 5:
            return res
        wait = int(res.headers.get("Retry-After", 2 ** attempt))
        print(f"got {res.status_code} (request {res.headers.get('X-Request-Id')}), waiting {wait}s")
        time.sleep(wait)
    return res


res = call("GET", "/me")
print(res.status_code, res.headers["X-RateLimit-Remaining"], "left this minute")
```

We ran both against a local copy of the API until it answered 429: each waited the 60 seconds and then got its answer. The [vps9 CLI](https://vpsnine.com/help/install-the-vps9-cli) doesn't retry a 429 on its own; it stops with the message and the request id. The [TypeScript SDK](https://vpsnine.com/tutorials/script-vpsnine-with-the-typescript-sdk) retries for you.

## Request ids

Every answer from `/api/v1` has an `X-Request-Id` header: `req_` and 24 letters and digits. Errors repeat it in the body as `error.requestId`. We log each request under its id with the route, the status and how long it took, never your token or what you sent, so the id is all we need to find a call.

Log it next to anything that goes wrong in your own code, and quote it in a [support ticket](https://my.vpsnine.com/support/new). The CLI prints it under every API error:

```text
vps9: Another action is still running on this server. Wait for it to finish: wait for it to finish, then try again
vps9: request id req_zl6db5dcag24ifpqaqyvd67k (quote it if you contact support)
```

The **Try it** panel in the [API reference](https://my.vpsnine.com/api) shows it with a copy button:

[Screenshot: The Try it panel of List servers after sending a request with a revoked token: 401 Unauthorized, the request id with a copy id button, and the JSON error that repeats it] Try it shows the status, the request id and the answer. Copy the id into a ticket if the answer surprises you.

An activity row for anything a token changed notes its request id as well, under **Account > activity** in the panel.

## Troubleshooting

**429 when one script is well under 60 a minute.** Another token on the same account is busy: the account limit of 120 is shared. Look at **last used** on the [token list](https://vpsnine.com/help/api-tokens) to find it.

**`429 rate_limited` on `POST /servers` after a few tries.** That's the 10 orders an hour, which counts failed tries and orders placed in the panel. Fix the order's error before you send it again, and wait for the hour to pass.

**A loop that polls for an action to finish runs out of requests.** Poll every 5 seconds, not every second; most actions take under a minute. Or use [webhooks](https://vpsnine.com/help/webhooks) for `server.ready` and `snapshot.completed` instead of polling.

## Next steps

- [Idempotent orders with Idempotency-Key](https://vpsnine.com/help/idempotent-orders-with-idempotency-key)
- [Use curl and jq with the VPSNine API](https://vpsnine.com/tutorials/use-curl-and-jq-with-the-vpsnine-api)
- [The developer hub](https://vpsnine.com/help/developers) and the [plans](https://vpsnine.com/plans) your scripts will run against.
