Client API
Deploy, inspect, power-cycle and destroy VPS instances from your own code.
No matching sections. Try a method, path, or error code.
Overview
A small JSON API over HTTPS. No SDK required.
- Base URL
- https://layeronecloud.com/api/v1/
- Format
- JSON request and response bodies, UTF-8.
- Authentication
- API key in a header. Cookies and sessions are never accepted.
- Billing
- The API opens after hourly billing is unlocked on an active account. Servers draw hourly usage from account credit; an empty-balance deployment can first charge one month of selected-plan credit to a usable saved card. API requests themselves: 5000 included per month, then $1.00 per 100,000 requests.
Every endpoint acts on the account that owns the key. There is no way to name another account, and a server belonging to someone else reads as 404.
This page is the field-by-field reference. For a walkthrough, copy-paste recipes and how the monthly allowance is billed, see API quickstart, API recipes and Rate limits and the monthly allowance.
Authentication
Create keys under Developer in the client area.
Send the key on every request, in either header:
Authorization: Bearer l1_a1b2c3d4e5f60718_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# or
X-API-Key: l1_a1b2c3d4e5f60718_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The secret half of a key is shown once, when you create it, and is stored only as a one-way digest. If you lose it, revoke the key and create another.
Permissions
| Scope | Can do |
|---|---|
| read_only | GET on every endpoint. Any other method returns 403 forbidden. |
| full | Everything, including creating, power-cycling and destroying servers. |
When the API is available
A valid key is not enough. The billing account has to be unlocked and not suspended.
Every authenticated request is refused until hourly billing is unlocked on the account: a usable saved card, positive credit, credit already bought up front, or a server that is already running. Until then the response is 402 payment_required.
Creating another server has a funding check of its own. Any positive credit balance makes the hourly order $0.00 due and queues it immediately. With a usable saved card and a balance at or below zero, LayerOne charges exactly one month of credit for the selected plan before queueing the server. A decline or unconfirmed payment returns 402 payment_required and no provisioning is queued. Without a usable card, add credit or complete the one-month checkout in the client area first.
A suspended or closed account is 403 forbidden on every endpoint, including reads. Keys can still be created and revoked under Developer; they simply cannot call the API until the account is active and unlocked again.
Rate limits
- Requests
- 120 per minute, per key.
- Deployments
- 100 new servers per hour, per account.
Over either limit returns 429 with the error code rate_limited. The per-minute request limit sends a Retry-After header; wait that long and retry. The deploy limit does not, so back off for the rest of the hour rather than looking for the header.
Usage and pricing
Counted per account, not per key. Resets on the 1st of each month, UTC.
- Included
- 5000 requests per month.
- Overage
- $1.00 per 100,000 requests, billed against account credit.
Every authenticated request that is actually served counts, including 4xx from the view (a 404 for a missing server still used the API). Failed authentication, CORS preflight, rate-limited requests, a locked or suspended account, and a read-only key attempting a write do not count. Current usage is on GET /api/v1/account and on every authenticated response as X-Api-Quota-Limit, X-Api-Quota-Remaining and X-Api-Quota-Used.
Errors
Every failure has the same body. Branch on error.code, not on the message.
{
"error": {
"code": "invalid_request",
"message": "'root_password' must be at least 12 characters.",
"details": { "field": "root_password" }
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body or a parameter is malformed or fails validation. |
| 401 | unauthorized | No key was sent, or the key is unknown, revoked or expired. |
| 402 | payment_required | Hourly billing is not unlocked, this deploy has neither positive credit nor a usable saved card, or the immediate saved-card charge failed. No provisioning is queued. |
| 403 | forbidden | A read-only key attempted a write, the billing account is suspended or closed, or the account has no billing profile yet. Sign in to the client area once to create one. |
| 404 | not_found | No such resource on this account. |
| 405 | method_not_allowed | Wrong HTTP method for this endpoint. |
| 409 | conflict | The server is in a state that refuses the request, or another job is already running on it. |
| 413 | invalid_request | The request body is larger than 16 KB. No endpoint needs a body that size. |
| 429 | rate_limited | Over a rate limit. |
| 500 | server_error | Something broke on our side. It is logged and alerted; retry is safe. |
Endpoints
A trailing slash is optional on every path.
| Method | Path | Does |
|---|---|---|
| GET | /api/v1/ | Index: who the key belongs to and where everything is. |
| GET | /api/v1/account | Account status, credit balance, this month's API quota, and the transfer pool with each server's share. |
| GET | /api/v1/plans | Plans the API can deploy. |
| GET | /api/v1/images?plan=<slug> | Operating system images, optionally for one plan. |
| GET | /api/v1/networks | List your private networks. |
| POST | /api/v1/networks | Create a private network. |
| GET | /api/v1/networks/<id> | One private network, including members. |
| PATCH | /api/v1/networks/<id> | Rename, change CIDR, set or clear the gateway, or toggle Cloud-Init addressing. |
| DELETE | /api/v1/networks/<id> | Delete an empty private network. |
| POST | /api/v1/networks/<id>/sync | Retry hypervisor sync when the network is in error. |
| GET | /api/v1/servers | List your servers. |
| POST | /api/v1/servers | Deploy a server. |
| GET | /api/v1/servers/<id> | One server, including its IP address. |
| DELETE | /api/v1/servers/<id> | Destroy a server permanently. |
| POST | /api/v1/servers/<id>/actions | Start, stop, restart or shut down. |
| GET | /api/v1/servers/<id>/bandwidth | This server's transfer this month, plus previous months. |
| GET | /api/v1/servers/<id>/firewall | Read the firewall policy and rules. |
| PUT | /api/v1/servers/<id>/firewall | Set the inbound and outbound default policy. |
| POST | /api/v1/servers/<id>/firewall/rules | Add one rule. |
| DELETE | /api/v1/servers/<id>/firewall/rules/<rule_id> | Delete one rule. |
| POST | /api/v1/servers/<id>/networks | Attach a server to a private network. |
| DELETE | /api/v1/servers/<id>/networks/<network_id> | Detach a server from a private network. |
Create a server
POST /api/v1/servers
| Field | Required | Notes |
|---|---|---|
| plan | Yes | Plan slug from GET /api/v1/plans. |
| image | No | Image slug from GET /api/v1/images?plan=<slug>. Defaults to the plan's default image. |
| label | No | Display name. Letters, digits, dots and hyphens; must start with a letter or digit. |
| hostname | No | Guest hostname, same character rules as label. |
| root_password | No | 12 to 72 printable ASCII characters, no spaces, and it must pass the same strength rules as an account password. Omit it and one is generated and returned to you. |
| public_ipv4 | No | Defaults to true. Set false to deploy without a public address. Pair with private_network and private_ip, or omit both and attach a private interface after deploy. A server with no public IPv4 has no internet path. |
| private_network | No | Id of a ready private network on this account. Requires private_ip. Cloud-Init uses that VNet's gateway if one is set; there is no per-server override. |
| private_ip | No | IPv4 address inside that network, not the network or broadcast address. |
Virtual networks are account-scoped isolated LANs. A VM may attach to more than one. The platform does not route between VNets. Walkthrough: create a private network, pfSense on LayerOne.
curl -X POST https://layeronecloud.com/api/v1/servers \
-H "Authorization: Bearer $LAYERONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"plan": "layerone-4g",
"image": "ubuntu-24-04",
"label": "web-01",
"hostname": "web-01.example.com",
"root_password": "correct-horse-Battery9"
}'
The plan and image slugs above are real but illustrative. Read them from GET /api/v1/plans and GET /api/v1/images?plan=<slug> rather than hardcoding them: the catalog is live, and image availability is per plan because an image whose minimum disk exceeds the plan's is filtered out.
A 201 means the deployment is queued, not finished. The response carries the server id and, once only, the root password:
{
"server": {
"id": 4192,
"label": "web-01",
"hostname": "web-01.example.com",
"status": "provisioning",
"ipv4_address": null,
"private_ipv4": null,
"private_networks": [],
"location": "Tampa",
"plan": "layerone-4g",
"image": { "slug": "ubuntu-24-04", "name": "Ubuntu 24.04", "os_family": "debian" },
"specs": { "cpu_cores": 2, "memory_mb": 4096, "disk_gb": 60, "bandwidth_tb": "3.00" },
"login_username": "root",
"billing": { "cadence": "hourly", "hourly_rate": "0.0109", "monthly_equivalent": "7.96" },
"created_at": "2026-08-19T14:02:11.417Z",
"provisioned_at": null,
"destroyed_at": null
},
"root_password": "correct-horse-Battery9",
"root_password_generated": false,
"order_id": 88213,
"message": "Deployment queued. Poll GET /api/v1/servers/{id} until status is 'running'. ipv4_address is set for public IPv4; private-only servers keep it null and set private_ipv4."
}
Funding
Deployments are billed hourly and draw down account credit. With any positive balance, the hourly order has $0.00 due and is queued immediately. With a usable saved card and a balance at or below zero, LayerOne first charges exactly one month of credit for the selected plan; only a successful payment queues provisioning. A decline returns 402 payment_required and no server is provisioned. Without a usable saved card, add credit or complete a one-month checkout in the client area. Monthly and annual commitments are not available through the API because they require an interactive payment page.
Statuses
The status field on a server.
| Value | Meaning |
|---|---|
| pending | Accepted, not yet picked up. |
| provisioning | Being built. No IP address yet. |
| running | Booted. ipv4_address is set when the server has public IPv4; private-only servers set private_ipv4 instead. |
| stopped | Powered off. Still billed, still yours. |
| suspended | Stopped for billing. Settle the balance to restore it. |
| deleting | Destruction is in progress. Billing has stopped. |
| failed | Provisioning did not complete. Contact support; the server cannot be deleted until provisioning finishes. |
| destroyed | Gone. Listed only with ?include_destroyed=true. |
| staged | A dry-run deploy on a development instance. You will not see this in production, but treat any unrecognised status as non-terminal rather than crashing on it. |
Power actions
POST /api/v1/servers/<id>/actions
action is one of start, stop, restart, shutdown. shutdown is the ACPI halt: it asks the guest to power down cleanly. stop cuts the power. There is no pause: a QEMU RAM freeze does not survive a node reboot, and billing does not stop while a server is halted.
curl -X POST https://layeronecloud.com/api/v1/servers/4192/actions \
-H "Authorization: Bearer $LAYERONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"action": "restart"}'
Returns 202 with the queued action. Repeating a request that is already queued returns the same action rather than queueing a second one. A server that is still provisioning, already running a job, or suspended for billing returns 409 conflict with the reason in the message.
Firewall
GET/PUT /api/v1/servers/<id>/firewall · POST /api/v1/servers/<id>/firewall/rules · DELETE /api/v1/servers/<id>/firewall/rules/<rule_id>
Hypervisor firewall for one server after it has deployed. The same rules as the portal: inbound and outbound default policy, then ordered allow/drop/reject rules. Anti-spoofing IP filters are platform-managed and are not part of this API.
curl https://layeronecloud.com/api/v1/servers/4192/firewall \
-H "Authorization: Bearer $LAYERONE_API_KEY"
curl -X PUT https://layeronecloud.com/api/v1/servers/4192/firewall \
-H "Authorization: Bearer $LAYERONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"inbound_policy": "DROP", "outbound_policy": "ACCEPT", "confirm_ssh_lockout": true}'
curl -X POST https://layeronecloud.com/api/v1/servers/4192/firewall/rules \
-H "Authorization: Bearer $LAYERONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"direction": "in", "action": "ACCEPT", "protocol": "tcp", "port": "22"}'
inbound_policy and outbound_policy are ACCEPT or DROP. Switching inbound to DROP without a rule that allows TCP 22 requires confirm_ssh_lockout: true. There is no way to disable the hypervisor firewall or to read or write IP filter sets.
Private networks
GET/POST /api/v1/networks · GET/PATCH/DELETE /api/v1/networks/<id> · POST /api/v1/networks/<id>/sync · POST /api/v1/servers/<id>/networks · DELETE /api/v1/servers/<id>/networks/<network_id>
Isolated RFC1918 VNets, the same objects as /client/network/private/. Caps and rules are inherited, not reinvented: ten networks per account by default, /29 to /24, no two attachments to the same VNet, and the last NIC on a private-only server cannot be detached. Isolation FORWARD rules stay platform-owned.
curl -X POST https://layeronecloud.com/api/v1/networks \
-H "Authorization: Bearer $LAYERONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "office", "cidr": "10.20.0.0/24"}'
curl -X POST https://layeronecloud.com/api/v1/servers/4192/networks \
-H "Authorization: Bearer $LAYERONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"network": 12, "address": "10.20.0.10"}'
PATCH accepts name, cidr (alias network_cidr), gateway (empty or null clears it), and cloud_init_assign_ips. Changing CIDR does not rewrite member addresses; they still have to fit. The server object exposes every attachment as private_networks[]. Deploy can still join one existing VNet via private_network and private_ip on POST /api/v1/servers.
Bandwidth
GET /api/v1/servers/<id>/bandwidth · GET /api/v1/account
Transfer is pooled per account, not capped per server. Device-level figures are attribution: they answer which server burned the pool. They are not a per-server allowance. Each billable server adds 500 GB (extra_per_additional_server_gb) to the same pool, on top of the 2 TB base. specs.bandwidth_tb on a server object is the plan's published figure, not this month's usage and not a cap.
Inbound and outbound are the guest's totals across every interface, public and private, for the UTC calendar month. Traffic between two servers on a VNet still counts. The poller updates every few minutes; a server that has not been sampled yet still returns zeros for the current month rather than 404.
curl https://layeronecloud.com/api/v1/servers/4192/bandwidth \
-H "Authorization: Bearer $LAYERONE_API_KEY"
{
"bandwidth": {
"server_id": 4192,
"label": "web-01",
"current": {
"period_start": "2026-08-01",
"period_end": "2026-08-31",
"used_bytes": 1234567890,
"used_tb": "0.0012",
"inbound_bytes": 800000000,
"outbound_bytes": 434567890,
"updated_at": "2026-08-22T18:04:11.417Z"
},
"periods": [ { "...": "current month first, then older months" } ],
"account_pool": {
"used_tb": "0.0012",
"allowance_tb": "2.5000",
"base_allowance_tb": "2.0000",
"extra_server_allowance_tb": "0.5000",
"extra_server_count": 1,
"extra_per_additional_server_gb": 500,
"remaining_tb": "2.4988",
"overage_tb": "0.0000",
"is_over": false
}
}
}
For the whole fleet in one request, read bandwidth.servers on GET /api/v1/account instead of polling each server. That array is this month only; history lives on the per-server endpoint. Walkthrough: Bandwidth: pool, blocks, and overage.
Destroy a server
DELETE /api/v1/servers/<id>
curl -X DELETE https://layeronecloud.com/api/v1/servers/4192 \
-H "Authorization: Bearer $LAYERONE_API_KEY"
Deploy and wait for the IP
The whole flow, in Python, with only the standard library.
import json, os, time, urllib.request
BASE = "https://layeronecloud.com/api/v1/"
KEY = os.environ["LAYERONE_API_KEY"]
def call(method, path, body=None):
request = urllib.request.Request(
BASE + path,
method=method,
data=json.dumps(body).encode() if body else None,
headers={
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request) as response:
return json.load(response)
created = call("POST", "servers", {
"plan": "layerone-4g",
"image": "ubuntu-24-04",
"label": "web-01",
"root_password": os.environ["SERVER_ROOT_PASSWORD"],
})
server_id = created["server"]["id"]
print("queued", server_id)
# The address is assigned during provisioning, so poll for it.
for _ in range(60):
server = call("GET", f"servers/{server_id}")["server"]
if server["status"] == "running" and server["ipv4_address"]:
print("ready:", server["ipv4_address"])
break
if server["status"] == "failed":
raise SystemExit("provisioning failed")
time.sleep(10)
# When you are finished with it:
# call("DELETE", f"servers/{server_id}")
Unlock hourly billing before your first API call
Add account credit or save a usable card in Billing. Saving a card does not charge it; if you deploy with no available credit, one month of selected-plan credit is charged before the server is queued.