Getting started
Introduction
The KVM Panel API lets you manage the KVM VPS of your LWS account programmatically: inventory, monitoring, power, reinstallation, snapshots, backups, firewall, network and SSH keys. It offers what the KVM customer panel can do, for your scripts, your continuous integration or your monitoring.
Quick start
1. Create your API key. In the KVM panel, open the API / MCP page, available to the main account only. Declare the IP address your calls will come from, enable access, generate the key, then choose the VPS it may manage: all of them, or a selection. A key that does not cover any VPS yet works, but its VPS list is empty. The full key is displayed only once: copy it right away.
2. Store it in an environment variable, as all the examples in this documentation do:
export LWS_API_KEY="lws_live_sk_7Yc2Kp9RtNwQ4mZ8Bx3Vf1Hd6Ls0AjE2"
3. Check that it works:
curl https://panelkvm.lwspanel.com/api/v1/me \
-H "Authorization: Bearer $LWS_API_KEY"
The response shows the account, the key's permission level and the number of VPS it covers.
4. List your VPS:
curl https://panelkvm.lwspanel.com/api/v1/servers \
-H "Authorization: Bearer $LWS_API_KEY"
Each VPS is identified by its vmid, which you will use in every other call.
5. Start an action and track it. Reboot a VPS:
curl -X POST https://panelkvm.lwspanel.com/api/v1/servers/116807/power \
-H "Authorization: Bearer $LWS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"action": "reboot"}'
The response contains a task ("id": "tsk_000002e1"). Poll it until its status is succeeded:
curl https://panelkvm.lwspanel.com/api/v1/tasks/tsk_000002e1 \
-H "Authorization: Bearer $LWS_API_KEY"
Authentication
Every request carries the key in the Authorization header, using the Bearer scheme. Never pass it in a URL: access logs keep URLs.
The key is managed from the API / MCP page of the panel:
- Authorized addresses (required): IPv4, IPv6 or CIDR ranges. Access cannot be enabled without at least one address. A call from any other address is rejected (
403 ip_not_allowed). - Activation: a switch turns access off without deleting the key (
401 access_disabled). - Regeneration: a new key invalidates the previous one immediately, and the account holder is notified by email.
Permissions and scope
Each key is granted a permission level:
| Level | Value | What it allows |
|---|---|---|
| Read only | read |
All read operations: inventory, status, metrics, lists, tasks, activity log. |
| Operations | exploitation |
Read only, plus: power, snapshot and backup creation, ISO images, reverse DNS, SSH keys, creating and editing rule sets, applying a rule set, enabling the firewall, requesting port 25 unblocking. |
| Full | full |
Operations, plus irreversible operations and those that expose the server: reinstallation, rescue mode, root password, snapshot rollback and deletion, backup restore, disabling the firewall, removing and deleting rule sets. |
The required level is shown at the top of each operation. A call beyond the granted level is rejected (403 insufficient_scope), and the response states the required level in required_level.
The level is chosen on the API / MCP page of the panel. Switching to the Full level must be confirmed with a code sent by email to the account holder; returning to a lower level does not require one.
The key's scope covers either all the VPS of the account, including those purchased later, or a selection of VPS. A VPS outside the scope responds exactly like a VPS that does not exist (404 vm_not_found).
Requests and responses
- Data is exchanged as JSON, encoded in UTF-8. Request bodies are sent with
Content-Type: application/json. - A single resource is returned under
data. - A list is returned under
data, along with ametablock. Long lists (VPS, tasks, activity log) are paginated withpage(default1) andper_page(default25, at most100); short lists are returned in full. - Dates use the ISO 8601 format with a time zone offset:
2027-03-14T00:00:00+01:00. - Sizes are expressed in bytes, except
min_memory_gbin the operating system catalog. - Identifiers are strings, including
vmid:"116807". Onlyaccount_id, inGET /me, is a number. - Human-readable texts produced by the API (error
titleanddetail, task and activity logdescription) are written in French. Base your processing on the machine-readable fields. - Every response carries an
X-Request-Idheader. Give it to LWS support whenever you report an issue: it identifies the exact call.
{
"data": [
{
"id": "tsk_000002e1",
"status": "succeeded",
"action": "server.reboot",
"vmid": "116807",
"description": "Demande de reboot du serveur VPS-116807",
"created_at": "2026-08-20T14:31:02+02:00",
"finished_at": "2026-08-20T14:31:40+02:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 1, "total_pages": 1 }
}
Asynchronous operations
Operations marked [Differed] (start, reinstallation, snapshot, backup, firewall…) do not run during the request: they are queued and respond 202 Accepted with the task they created.
{
"data": {
"tasks": [
{
"id": "tsk_000002e1",
"status": "pending",
"action": "server.reboot",
"vmid": "116807",
"description": "Demande de reboot du serveur VPS-116807",
"created_at": "2026-08-20T14:31:02+02:00",
"finished_at": null
}
]
}
}
Then track the task with Retrieve a task every few seconds, until status is succeeded or failed. Polling more often does not speed anything up and uses up your rate limit.
Only one operation of a given type can run at a time on a VPS: a second request is rejected (409 task_already_running) and the response identifies the running task in task_id.
A VPS locked by the hypervisor also rejects write operations (409 server_locked). The reason field tells you what to do: operation_in_progress, an operation is running, retry after the delay given in the Retry-After header; suspended, the VPS is hibernating, start it first; support_notified, the lock is abnormal, the LWS team has been notified and is taking care of it, there is no point in retrying in a loop.
During an intervention announced by LWS on the host server of a VPS (update, reboot…), if that server stops responding, write operations on this VPS and reads that query the hypervisor are rejected (503 server_maintenance). The response's maintenance field gives the nature of the intervention and its expected end, and the Retry-After header the recommended delay before trying again. The VPS inventory and record keep responding, with status: maintenance.
All other write operations (ISO images, reverse DNS, root password, SSH keys, rule sets) take effect immediately and return their result directly.
Errors
Errors are returned in the application/problem+json format:
{
"type": "https://docs.lws.fr/kvm/errors/vm-not-found",
"title": "VPS introuvable",
"status": 404,
"code": "vm_not_found",
"detail": "Aucun VPS 999999 sur ce compte.",
"request_id": "3f9a1c07b2e45d18"
}
Base your error handling on code, which is stable. title and detail are written in French for a human reader and may change. Depending on the error, the response also includes errors (details per parameter), required_level, task_id, lock and reason, or retry_after.
| HTTP | Code | Cause |
|---|---|---|
| 400 | bad_request |
Unreadable body (malformed JSON). |
| 400 | validation_failed |
Invalid parameter; errors details each faulty field. |
| 401 | invalid_token |
Authorization header missing or malformed, or key unknown or since regenerated. |
| 401 | access_disabled |
API access turned off for this account (switch on the "API / MCP" page), or API service temporarily unavailable. |
| 403 | ip_not_allowed |
Source address not among the authorized addresses. |
| 403 | insufficient_scope |
Key level too low; required_level gives the level required. |
| 404 | vm_not_found |
VPS does not exist or is outside the key's scope (the two cases are deliberately indistinguishable). |
| 404 | snapshot_not_found, backup_not_found, set_not_found, ssh_key_not_found, task_not_found |
Resource unknown on this VPS or account. |
| 404 | not_found |
Route does not exist. |
| 405 | method_not_allowed |
HTTP method not supported on an existing path; the Allow header lists the accepted methods. |
| 409 | task_already_running |
An operation of the same type is already running on this VPS; task_id identifies it. |
| 409 | server_locked |
VPS locked by the hypervisor; reason tells you whether to retry, start it, or wait for the LWS team. |
| 409 | service_suspended |
VPS disabled or expired: read operations remain possible, write operations do not. |
| 409 | invalid_state |
Action incompatible with the current state. |
| 409 | quota_exceeded |
Functional limit reached (3 snapshots, 10 rule sets per VPS). |
| 422 | unsupported_operation |
Feature unavailable on this VPS (QEMU guest agent missing, in particular). |
| 429 | rate_limited |
Rate limit exceeded: honor Retry-After. |
| 500 | internal_error |
Unexpected error: give the request_id to support. |
| 502 | hypervisor_unreachable |
Hypervisor temporarily unreachable: try again. |
| 502 | upstream_unavailable |
Internal dependency unavailable (inventory, backups, catalog…): try again. |
| 503 | server_maintenance |
Host server under an intervention announced by LWS and unreachable: write operations and hypervisor reads are rejected until the expected end (maintenance); honor Retry-After. |
Rate limits
Calls are limited per account, over a sliding one-hour window, in three independent families:
| Family | Limit | Operations covered |
|---|---|---|
read |
3,600 calls per hour | All read operations (GET) |
mutation |
300 calls per hour | Regular write operations |
heavy |
6 calls per hour per VPS, 60 per account | Snapshots (create, roll back, delete), backups (start, restore), reinstallation |
A heavy operation is subject to two caps: that of the targeted VPS and that of the account. The per-VPS cap prevents a single server from being hit in a loop, without penalising accounts with many VPS; the refusal message states which of the two caps has been reached.
To spare the hypervisors, the state and instant metrics of a VPS are cached for 15 seconds, and the metrics history for 1 minute to 1 hour depending on the period. Polling more often returns the same value and uses quota for nothing.
Responses to authenticated calls show the rate limit status in the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. An authenticated call that is refused (invalid parameter, insufficient level, conflict…) also counts against the limit; a call rejected at authentication, sent to a non-existent route, or refused for exceeding the limit (429) does not. Once the limit is reached, the API responds 429 rate_limited: wait for the number of seconds given in the Retry-After header before trying again.