> ## Documentation Index
> Fetch the complete documentation index at: https://help.exy.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Use the exy.sh API

> Run your servers and your account from your own code: tokens, scopes, limits, errors and examples.

The exy.sh API lets your own code do most of what the dashboard does. It lives at `https://api.exy.sh/v1`.

* **Servers:** list and read them, start, stop and restart them, reinstall, rename and delete them, set the login password, open a browser console and read their usage.
* **Ordering and billing:** list locations, plans and images, price a server, order it paid from your balance, renew it, switch auto-renew, upgrade it and buy a new IPv4 address. Read your balance, orders, invoices and balance history.
* **Firewall, snapshots and backups:** manage a server's inbound rules, take and restore snapshots and backups.
* **SSH keys and scheduled tasks:** keep your account's SSH keys, schedule restarts, shutdowns, starts and snapshots.
* **Webhooks:** get a signed request when a server is ready, suspended, released or deleted, or when an order is refunded.
* **Support tickets:** open tickets, read them and reply.

The full reference with every endpoint, field and response is at [api.exy.sh/v1/docs](https://api.exy.sh/v1/docs). The machine-readable OpenAPI document is at [api.exy.sh/v1/openapi.json](https://api.exy.sh/v1/openapi.json).

> [card] [SDK and MCP server](https://help.exy.sh/api/sdk-and-mcp.md)
>
> The typed TypeScript client and the MCP server for AI agents.

> [card] [Order and renew servers](https://help.exy.sh/api/order-and-renew-servers.md)
>
> Quote, order from your balance, renew, upgrade and change the IP.

> [card] [Webhooks](https://help.exy.sh/api/webhooks.md)
>
> Events, signature verification and retries.

> [card] [Resellers and WHMCS](https://help.exy.sh/api/resellers-and-whmcs.md)
>
> Reseller status, discount, your own references and the WHMCS module.

## Get a token

**1. Open Settings → API**

In the dashboard, go to **Settings** and open the **API** tab.

**2. Create a token**

Click **Create token**, give it a name you'll recognise (for example the tool or server that uses it), pick only the scopes it needs and choose when it expires: after 30, 90, 180 or 365 days.

**3. Copy it now**

The token starts with `exy_` and is shown exactly once. Store it in your secret manager or environment, for example as `EXY_API_TOKEN`.

Send it with every request as a header:

```
Authorization: Bearer exy_…
```

> [!WARNING]
>
> A token acts as you, within its scopes. Use it from your servers and scripts only, never in code that runs in a browser: the API sends no CORS headers and does not accept dashboard sessions.

* **Expiry:** we email you a week before a token expires. Tokens are never renewed automatically; create a new one and switch over before the old one stops working.
* **Rotation:** create the new token, deploy it, then revoke the old one. Two valid tokens at once are fine.
* **Revoke** a token in **Settings → API**. It stops working with the next request.
* **Password reset:** resetting your password revokes all tokens you created.
* **Teams:** a token created in a team acts on the team's account within your team role. If you leave the team or your role no longer allows a scope, the token loses that access immediately.

## Scopes

Each endpoint in the [reference](https://api.exy.sh/v1/docs) names the scope it needs. A request without it is answered with `403`.

| Scope | Allows |
| - | - |
| `vms.read` | List and read servers and their actions, usage, firewall, snapshots, backups and scheduled tasks; list images, SSH keys and webhooks |
| `vms.operate` | Start, stop and restart servers, open a console, take a snapshot or a backup |
| `vms.manage` | Reinstall, rename and delete servers, set `externalRef`, set the login password, change the firewall, restore and delete snapshots and backups, add and remove SSH keys, schedule tasks, create and delete webhooks |
| `billing.read` | List locations and plans, get quotes, read the balance, orders, invoices and balance history, see the price of an upgrade or a new IP |
| `billing.manage` | Order, renew and upgrade servers, switch auto-renew; together with `vms.manage`, buy a new IP. All of these spend the account owner's balance |
| `tickets.read` | List and read support tickets |
| `tickets.manage` | Open support tickets and reply to them |

A token with `billing.manage` can only be created by a member who has two-factor authentication turned on.

`GET /v1/account` works with any scope and shows the token's name, scopes and expiry.

## Limits

| Limit | Value |
| - | - |
| Requests per account | 60 per minute, every method, across all of the account's tokens |
| Requests per client address | 600 per minute |
| Page size | `limit` at most 50 on list endpoints |
| New server orders | 10 per account in any 24 hours, dashboard orders included |
| Power actions | One at a time per server; another while it runs is `409` |
| Confirmations with `confirmName` | 5 per server in 15 minutes, for reinstall, delete and restore |
| Firewall changes | 30 per server in 10 minutes, at most 20 rules |
| Snapshots | 3 per server |
| Scheduled tasks | 10 per server |
| Ticket messages | 10 per minute per account, new tickets and replies together |
| Webhooks | 10 notification channels per account, the dashboard's Telegram and Discord channels included |
| Tokens per account | 25 |
| Token lifetime | At most 365 days |

A request over a rate limit is answered with `429` and a `Retry-After` header that says how many seconds to wait. Wait at least that long before you try again. The daily order limit answers `DAILY_ORDER_LIMIT_EXCEEDED`, and its `Retry-After` can be up to 24 hours.

All amounts in the API are whole euro cents: `1299` is 12.99 EUR.

## Errors

Every error has the same shape:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "EXPECTED_TOTAL_MISMATCH",
    "message": "expectedTotal 1299 does not match the order's total 1399 (cents); quote again"
  }
}
```

Use `code` in your code; `message` is for people and may change.

| Status | Meaning | Common codes |
| - | - | - |
| `400` | The request is invalid, or the balance is too low | `VALIDATION_ERROR`, `CONFIRMATION_MISMATCH`, `INSUFFICIENT_BALANCE`, `PLAN_OUT_OF_STOCK` |
| `401` | No token, or the token is invalid, expired or revoked | `UNAUTHORIZED` |
| `403` | The token lacks the scope, API access is switched off for the account, the account is suspended, or the server is suspended | `FORBIDDEN`, `ACCOUNT_SUSPENDED`, `VM_SUSPENDED` |
| `404` | The id does not exist or belongs to another account | `NOT_FOUND` |
| `409` | The server's state does not allow the action, or the price is not the one you expected | `VM_STATE_INVALID`, `VM_POWER_ACTION_RUNNING`, `EXPECTED_TOTAL_MISMATCH` |
| `429` | A rate limit was reached; see `Retry-After` | `RATE_LIMIT_EXCEEDED`, `DAILY_ORDER_LIMIT_EXCEEDED` |

## Example with curl

List your servers, restart one and follow the action until it finishes:

```bash theme={null}
export EXY_API_TOKEN="exy_…"

curl "https://api.exy.sh/v1/servers?limit=10" \
  -H "Authorization: Bearer $EXY_API_TOKEN"

curl -X POST https://api.exy.sh/v1/servers/123/power \
  -H "Authorization: Bearer $EXY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "restart"}'
# 202 {"data":{"actionId":456}}

curl https://api.exy.sh/v1/servers/123/actions/456 \
  -H "Authorization: Bearer $EXY_API_TOKEN"
# {"data":{"id":456,"serverId":123,"type":"restart","status":"completed",...}}
```

An action is `pending`, then `running`, and ends as `completed` or `failed`. A failed action may carry a `failureCode`: after `graceful_timeout` the guest did not shut down in time and `force_stop` or `force_restart` works; after `proxmox_busy` or `lease_lost`, try again.

## Example with TypeScript

For a ready-made typed client, install [@exysh/sdk](https://help.exy.sh/api/sdk-and-mcp.md). To generate your own instead:

The OpenAPI document generates a fully typed client with [openapi-typescript](https://openapi-ts.dev) and [openapi-fetch](https://openapi-ts.dev/openapi-fetch/). Generate the types once, and again whenever you want new endpoints:

```bash theme={null}
npx openapi-typescript https://api.exy.sh/v1/openapi.json -o exy-api.d.ts
npm install openapi-fetch
```

```ts theme={null}
import createClient from "openapi-fetch"
import type { paths } from "./exy-api"

const exy = createClient<paths>({
  baseUrl: "https://api.exy.sh",
  headers: { Authorization: `Bearer ${process.env.EXY_API_TOKEN}` },
})

const { data, error, response } = await exy.POST("/v1/servers/{id}/power", {
  params: { path: { id: 123 } },
  body: { action: "restart" },
})

if (response.status === 429) {
  const seconds = Number(response.headers.get("Retry-After"))
  console.log(`Rate limited, try again in ${seconds} s`)
} else if (error) {
  console.error(error.error.code, error.error.message)
} else {
  console.log("Queued action", data.data.actionId)
}
```

Paths, parameters, request bodies and responses are all typed: a wrong action name or a missing field is a compile error.

## Compatibility

Within `/v1` we only add: new endpoints, new optional request fields and new response fields. Ignore response fields you don't know. We announce new values of an existing list, such as a new server status, before they appear. Anything else comes as `/v2`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.