Client API
Deploy, inspect, power-cycle and destroy servers 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://chat.b18.io/api/v2/
- Format
- JSON request and response bodies, UTF-8
- Authentication
- An API key in a header. Cookies and sessions are never accepted.
- Included requests
- 5,000 a month, then $1.00 per 100,000 requests
Paths on this page are relative to the base URL, so GET /servers means GET https://chat.b18.io/api/v2/servers. The older /api/v1/ base is legacy: it serves the same endpoints but always acts on the key's first workspace. Use v2 for anything new.
The API opens once hourly billing is unlocked on an active account. Servers you deploy bill hourly from your credit balance, the same as servers deployed from the client area.
This page is the field-by-field reference. For a walkthrough and copy-paste recipes, see API quickstart, API recipes and Rate limits and the monthly allowance.
Authentication
Create keys under Settings → API keys. Only the organization owner can create them.
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 we keep only a one-way fingerprint of it. 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. |
Workspaces
A key can reach every workspace in the organization, or a chosen list.
A workspace keeps its own servers and networks apart from the rest of the organization. In the API a workspace is called a tenant: GET /tenants lists the ones the key can reach, and you name one with ?tenant=<id>, "tenant": <id> in a JSON body, or the X-Tenant-Id header.
Leave it off and the request acts on the first workspace the key can reach: the organization's original workspace when the key can reach it. A server in another workspace reads as 404.
When the API is available
A valid key is not enough. The 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 server 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 Settings → API keys; they 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
- 5,000 requests a month
- Over the allowance
- $1.00 per 100,000 requests, taken from your credit balance
Every authenticated request that is actually served counts, including 4xx responses from the endpoint (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 /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 server is queued. |
| 403 | forbidden | A read-only key attempted a write, the 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 in this workspace. |
| 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; retrying is safe. |
All endpoints
Relative to https://chat.b18.io/api/v2/. A trailing slash is optional on every path.
| Method | Path | Does |
|---|---|---|
| GET | / | Index: who the key belongs to and where everything is. |
| GET | /account | Account status, credit balance, this month's API usage, and the transfer pool with each server's share. |
| GET | /tenants | Workspaces this key can reach. |
| GET | /plans | Plans the API can deploy. |
| GET | /images?plan=<slug> | Operating system images, optionally for one plan. |
| GET | /networks | List your private networks. |
| POST | /networks | Create a private network. |
| GET | /networks/<id> | One private network, including its members. |
| PATCH | /networks/<id> | Rename, change the range, or set or clear the optional gateway reference. |
| DELETE | /networks/<id> | Delete an empty private network. |
| POST | /networks/<id>/sync | Retry setting up a network that is in error. |
| GET | /servers | List your servers. |
| POST | /servers | Deploy a server. |
| GET | /servers/<id> | One server, including its IP address. |
| DELETE | /servers/<id> | Destroy a server permanently. |
| POST | /servers/<id>/actions | Start, stop, restart or shut down. |
| GET | /servers/<id>/bandwidth | This server's transfer this month, plus previous months. |
| GET | /servers/<id>/firewall | Read the firewall policy and rules. |
| PUT | /servers/<id>/firewall | Set the inbound and outbound default policy. |
| POST | /servers/<id>/firewall/rules | Add one rule. |
| DELETE | /servers/<id>/firewall/rules/<rule_id> | Delete one rule. |
| POST | /servers/<id>/networks | Attach a server to a private network. |
| DELETE | /servers/<id>/networks/<network_id> | Detach a server from a private network. |
Create a server
POST /servers
| Field | Required | Notes |
|---|---|---|
| plan | Yes | Plan slug from GET /plans. |
| image | No | Image slug from GET /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 | Hostname set inside the server, 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, then choose private_network or attach one after deploy. Internet access then needs your own router. |
| private_network | No | Id of a ready private network in this workspace. No address is needed: configure addressing and routes inside the server or through your own DHCP server. |
| private_ip | No | Retired. Omit this field; a value is rejected. The platform does not assign private addresses. |
Private networks are isolated networks in your workspace, and a server can join more than one. The platform does not route between them. Walkthrough: create a private network, pfSense on LayerOne.
curl -X POST https://chat.b18.io/api/v2/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 /plans and GET /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/v2/servers/{id} until status is 'running'. ipv4_address is set for public IPv4; private-only servers keep it null; private addresses are configured inside the server or by your DHCP server."
}
Funding
Deployments are billed hourly and draw down your credit balance. 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 the server. A decline returns 402 payment_required and no server is created. 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 need an interactive payment page.
Server 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 addresses are managed inside the server or through your own DHCP server. |
| stopped | Powered off. Still billed, still yours. |
| suspended | Stopped for billing. Settle the balance to restore it. |
| deleting | Being destroyed. Billing has stopped. |
| failed | Provisioning did not complete. Contact support; the server cannot be destroyed until provisioning finishes. |
| destroyed | Gone. Listed only with ?include_destroyed=true. |
| staged | A practice deploy on a development system. You will not see this in production, but treat any unrecognized status as not final rather than failing on it. |
status is our record of the server. power_state beside it is the live power state we last read from the server's host, refreshed about once a minute: running, stopped, paused, unknown (the host did not report it), or "" before it has ever been read. The two normally agree; when they do not, power_state describes the machine. Poll status for the lifecycle and power_state for power.
Power actions
POST /servers/<id>/actions
action is one of start, stop, restart, shutdown. shutdown asks the operating system to power down cleanly; stop cuts the power. There is no pause, and billing does not stop while a server is shut down.
curl -X POST https://chat.b18.io/api/v2/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 /servers/<id>/firewall · POST /servers/<id>/firewall/rules · DELETE /servers/<id>/firewall/rules/<rule_id>
The network firewall in front of one deployed server, applied outside the server itself. It has the same rules as the client area: inbound and outbound default policy, then ordered allow, drop and reject rules. Anti-spoofing filters are managed by the platform and are not part of this API.
When firewall groups are assigned, GET returns the combined policy and rules,
managed_by_groups: true, and a groups list with each
group's id and name. Rules identify their source with group_id
and group_name. Individual policy and rule writes return
409 until every group is removed. Manage assignments under
Networking → Firewall groups in the client area.
curl https://chat.b18.io/api/v2/servers/4192/firewall \
-H "Authorization: Bearer $LAYERONE_API_KEY"
curl -X PUT https://chat.b18.io/api/v2/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://chat.b18.io/api/v2/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. The firewall cannot be turned off, and the platform's IP filter sets cannot be read or written.
Private networks
GET/POST /networks · GET/PATCH/DELETE /networks/<id> · POST /networks/<id>/sync · POST /servers/<id>/networks · DELETE /servers/<id>/networks/<network_id>
Isolated networks on private (RFC 1918) address ranges, the same ones you manage under Networking → Private networks in the client area. The same limits apply: ten networks per account by default, sizes from /29 to /24, one attachment per server per network, and the last network on a private-only server cannot be detached. The platform keeps networks isolated from each other.
curl -X POST https://chat.b18.io/api/v2/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://chat.b18.io/api/v2/servers/4192/networks \
-H "Authorization: Bearer $LAYERONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"network": 12}'
PATCH accepts name, cidr (alias network_cidr) and gateway (an optional reference only; empty or null clears it). The retired cloud_init_assign_ips field only accepts false. Changing the range does not rewrite member addresses; they still have to fit. The server object lists every attachment as private_networks[]. A deploy can still join one existing network with private_network, without an address, on POST /servers.
Bandwidth
GET /servers/<id>/bandwidth · GET /account
Transfer is pooled per account, not capped per server. Server-level figures are attribution: they answer which server used the pool. They are not a per-server allowance. Each deployed server earns up to 500 GB (extra_per_additional_server_gb) per UTC calendar month, prorated by the time it has been deployed, 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.
instance_earned_tb and extra_server_allowance_tb report the same credits earned so far. allowance_tb includes only earned credits, the free base, and purchased blocks. potential_allowance_tb projects the pool at month end if your current servers stay deployed; future credits cannot be used yet. Stopped and suspended servers keep earning. A destroy request stops new credits, while earned transfer stays until the monthly reset.
Inbound and outbound are the server's totals across every interface, public and private, for the UTC calendar month. Traffic between two servers on a private network still counts. Figures update every few minutes; a server that has not been measured yet returns zeros for the current month rather than 404.
curl https://chat.b18.io/api/v2/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-16T12:00:00Z"
},
"periods": [ { "...": "current month first, then older months" } ],
"account_pool": {
"used_tb": "0.0012",
"allowance_tb": "2.2500",
"base_allowance_tb": "2.0000",
"extra_server_allowance_tb": "0.2500",
"extra_server_count": 1,
"extra_per_additional_server_gb": 500,
"instance_earned_tb": "0.2500",
"instance_potential_tb": "0.5000",
"instance_remaining_tb": "0.2500",
"potential_allowance_tb": "2.5000",
"reset_at": "2026-09-01T00:00:00+00:00",
"remaining_tb": "2.2488",
"overage_tb": "0.0000",
"is_over": false
}
}
}
For every server in one request, read bandwidth.servers on GET /account instead of polling each server. That list is this month only; history lives on the per-server endpoint. Walkthrough: Bandwidth: pool, blocks, and overage.
Destroy a server
DELETE /servers/<id>
curl -X DELETE https://chat.b18.io/api/v2/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://chat.b18.io/api/v2/"
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.