---
title: REST API
description: Call the customer API of your NexCMP installation: authentication, the interactive reference, OpenAPI and Postman downloads, errors, rate limits, idempotency and curl examples.
url: https://nexcmp.com/docs/developers/api/
applies_to: NexCMP 1.6.1
updated: 2026-11-02
---

# REST API

> **Where in the portal:** API reference at `https://<your-domain>/apidoc` (public) and API keys at `/api-keys`. **Where in the admin:** Customers › API keys (`/customers/apikeys`) lists the keys customers created.

## What it does

Every action a customer can do in the portal — list and control VMs, read invoices, manage SSH keys, Kubernetes, storage, support tickets — is available through a versioned REST API under `https://<your-domain>/v1/`. Customers call it from scripts and CI with a scoped **API key**. The same API feeds the [`cmp` command-line tool](https://nexcmp.com/docs/developers/cmp-cli/) and the [MCP server](https://nexcmp.com/docs/developers/mcp/), and the reference is generated from the running installation, so it always lists exactly the features you have switched on.

## Requirements

| Item | Requirement |
|---|---|
| Public HTTPS address | The API is served on your main domain (and on each reseller's own domain). |
| An API key | Created by the customer in the portal; see [API keys](https://nexcmp.com/docs/developers/api-keys/). |
| Dependencies | None. Endpoints of a module you did not enable (for example Kubernetes or Object Storage) are hidden from the reference and refused. |

## Prepare the infrastructure

Nothing to install. If a reverse proxy sits in front of NexCMP, forward `/v1/`, `/mcp`, `/cli/` and `/apidoc` to the panel (the installer's nginx configuration already does).

## Connect in NexCMP

### The interactive reference

Open `https://cloud.example.com/apidoc` (replace the host with your domain; a reseller's customers see the reference on the reseller's domain, with the reseller's name). It has endpoint documentation with request and response examples in curl, the `cmp` CLI, JavaScript, Python and Go, a search box (Ctrl or Cmd + K), and a **Try it** panel. Try it sends requests to the same site only, with a key the visitor pastes (kept in the browser session); it is read-only by default, and write or paid calls need an extra switch and a confirmation.


### Downloads and machine-readable documents

All are public, need no key and are generated per site:

| URL | Content |
|---|---|
| `/apidoc` | Interactive reference. |
| `/v1/public/openapi.json` | OpenAPI 3.1 document. |
| `/v1/public/openapi.yaml` | Same, YAML. |
| `/v1/public/postman.json` | Postman collection. |
| `/v1/public/insomnia.json` | Insomnia export. |
| `/v1/public/api.md` | The reference as one Markdown file. |
| `/v1/public/api-catalog` | Light JSON: brand, base URL, key prefix, every scope and every endpoint with method, path, scope and whether it needs an `Idempotency-Key`. |

```bash
curl -s https://cloud.example.com/v1/public/api-catalog | python3 -m json.tool | head -40
curl -sO https://cloud.example.com/v1/public/openapi.yaml
```

In Postman choose **Import › Link** and paste `https://cloud.example.com/v1/public/postman.json`. The reference page also offers one-file API clients for JavaScript and Python.

### Authentication

Send the key in the `Authorization` header:

```bash
curl -s https://cloud.example.com/v1/billing/wallet \
  -H "Authorization: Bearer vk_XXXX"
```

- Customer keys begin with `vk_`. Keys are **site-bound**: a key created on `cloud.example.com` does not work on a reseller domain.
- Browser session tokens (`pt_...`) are for the portal only; use a key for automation.
- The key's **scopes** decide what it may call; a missing scope answers HTTP 403 `scope_required`. See [API keys](https://nexcmp.com/docs/developers/api-keys/).
- Check a key with `GET /v1/apikeys/self`: it returns the calling key's name, scopes, expiry and the remaining rate-limit allowance (every key may call it).

### Examples

```bash
# List virtual machines
curl -s "https://cloud.example.com/v1/vm?page=1" -H "Authorization: Bearer vk_XXXX"

# Start one (needs vm:write)
curl -s -X POST https://cloud.example.com/v1/vm/3f2a1c9e-0000-4000-8000-000000000000/start \
  -H "Authorization: Bearer vk_XXXX"

# Create a VM (paid: needs vm:create and an Idempotency-Key)
curl -s -X POST https://cloud.example.com/v1/vm \
  -H "Authorization: Bearer vk_XXXX" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d @vm.json
```

Take the exact request fields of each operation from `/apidoc`.

### Errors

Every error is JSON with a stable `code` and a human-readable `message`:

```json
{"error": {"code": "scope_required", "message": "Key lacks scope vm:write"}}
```

| HTTP | Typical `code` | Meaning |
|---|---|---|
| 401 | `unauthenticated`, `key_invalid`, `key_revoked`, `key_expired`, `key_ip_denied`, `site_mismatch`, `api_keys_disabled` | Missing or unknown key, revoked, expired, caller IP outside the key's allow list, a key from another site, or API keys switched off. |
| 403 | `scope_required`, `endpoint_not_public`, `account_blocked`, `quota_exceeded`, `impersonate_forbidden` | Key lacks the scope; the endpoint is not available to keys; the account is locked; a [quota](https://nexcmp.com/docs/billing/quotas/) was reached; action not allowed in an impersonated session. |
| 404 | `not_found` | Not found — also returned for resources that belong to someone else. |
| 422 | `invalid_argument` | Validation error (field named in the response). |
| 429 | `rate_limited` | Too many requests; wait for `Retry-After`. |
| 503 | `maintenance` | Maintenance mode is on: reads work, writes are refused. |

### Rate limits

| Limit | Default |
|---|---|
| Per key | 120 requests per minute |
| Per account (all keys together) | 300 requests per minute |

Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; a 429 adds `Retry-After`.

### Idempotency

Commands that cost money or cannot be repeated (create, change plan, buy, delete ...) **require** an `Idempotency-Key` header (any string up to 200 characters; a UUID works well). Send the same key again after a timeout and the server returns the **original** result (with `Idempotency-Replayed: 1`) instead of repeating the operation. Reusing a key for a different command answers 422; a request still in progress answers 409/`Retry-After`. Without the header a paid call is refused.

## Verify

1. Create a key with scope `vm:read` in the portal (**API keys › Create key**).
2. Run:

   ```bash
   curl -i https://cloud.example.com/v1/apikeys/self -H "Authorization: Bearer vk_XXXX"
   ```

   You get HTTP 200 and the rate-limit headers.
3. `curl -s https://cloud.example.com/v1/vm -H "Authorization: Bearer vk_XXXX"` lists the account's VMs. In the portal the key's **Last used** column and the **Key usage log** show the calls.

## Limits & notes

- **Never available to a key:** top-up, withdraw, change cards or auto top-up, change the password, e-mail or 2FA, delete the account, create, edit or revoke other keys. Those need a signed-in browser session.
- **Hidden modules:** the reference and the OpenAPI document only list features enabled on that site.
- **Response size and paging:** list endpoints are paged (`page`, `per_page`); read the OpenAPI document for each endpoint.
- **Time and money:** money is in USD cents unless the field name says otherwise.
- **Admin API:** the admin console has its own **System › Admin › API** page and administrator keys (`ofw_...`) for administration; they are separate from the customer API described here.
- **Versioning:** the path prefix `/v1` is stable; new fields may be added to responses.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| 401 on a new key | Key from another domain, expired, or typed with spaces | Create the key on the domain you call; check the `Authorization: Bearer vk_...` header. |
| 403 `scope_required` | Missing scope | Edit the key in the portal and add the scope. |
| 401 `key_ip_denied` or `site_mismatch` | Key restricted to other IPs ("allowed IPs") or created on another domain | Add your server's IP to the key's allowed IPs, or call the domain where the key was created. |
| 404 on an existing VM | The VM belongs to another account | Use a key of the owner. |
| 400/422 asking for `Idempotency-Key` | Paid operation without the header | Add `Idempotency-Key: <uuid>`. |
| 429 | Over the limit | Back off; use `Retry-After`. |
| `/apidoc` shows less than expected | Module switched off | Enable the module in License & updates. |


