Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Client API

API recipes

Copy-paste snippets for the things people automate first.

5 min read Reviewed 24 Aug 2026

All of these assume L1_API_KEY is exported and jq is installed.

export L1_API_KEY="l1_..."
export L1="https://layeronecloud.com/api/v1"
auth=(-H "Authorization: Bearer $L1_API_KEY")

A one-line fleet listing

curl -sS "$L1/servers" "${auth[@]}" \
  | jq -r '.servers[] | [.id, .label, .status, (.ipv4_address // "-"), .plan] | @tsv' \
  | column -t

Cheapest plan that fits a requirement

curl -sS "$L1/plans" "${auth[@]}" | jq -r '
  .plans
  | map(select(.specs.memory_mb >= 4096))
  | sort_by(.pricing.monthly | tonumber)
  | .[0] | "\(.slug)  $\(.pricing.monthly)/mo  \(.specs.cpu_cores) vCPU  \(.specs.memory_mb)MB"'

Deploy and wait for the address

#!/usr/bin/env bash
set -euo pipefail

created=$(curl -sS -X POST "$L1/servers" \
  -H "Authorization: Bearer $L1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plan":"layerone-starter","image":"ubuntu-24-04","label":"web-01"}')

id=$(echo "$created" | jq -r '.server.id')
echo "$created" | jq -r '.root_password' > "./root-password-$id"
chmod 600 "./root-password-$id"
echo "server $id queued; password saved to ./root-password-$id"

for _ in $(seq 1 60); do
  server=$(curl -sS "$L1/servers/$id" "${auth[@]}" | jq -r '.server')
  status=$(echo "$server" | jq -r '.status')
  address=$(echo "$server" | jq -r '.ipv4_address // ""')
  if [ "$status" = "running" ] && [ -n "$address" ]; then
    echo "ready: $address"
    exit 0
  fi
  echo "  $status ..."
  sleep 10
done

echo "still not running after 10 minutes; check the portal" >&2
exit 1

The password is written to a file rather than echoed, because a terminal scrollback is not a secret store. Move it into your password manager and delete the file.

Lock the firewall down to your own address

me=$(curl -sS https://api.ipify.org)

curl -sS -X POST "$L1/servers/123/firewall/rules" \
  -H "Authorization: Bearer $L1_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"direction\": \"in\", \"action\": \"ACCEPT\", \"protocol\": \"tcp\", \"port\": \"22\", \"source\": \"$me/32\", \"comment\": \"admin ssh\"}"

curl -sS -X PUT "$L1/servers/123/firewall" \
  -H "Authorization: Bearer $L1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"inbound_policy": "DROP", "outbound_policy": "ACCEPT"}'

direction is in or out, in lower case. action, inbound_policy and outbound_policy are ACCEPT or DROP, in upper case. Neither is normalised for you, so the wrong case is a 400. protocol is tcp, udp, icmp, or omitted for any. A server holds at most 40 rules.

Order matters

Add the allow rule before setting the inbound policy to drop, or you lock yourself out. If the policy change would leave no way in, the API refuses it; passing "confirm_ssh_lockout": true overrides that refusal, so only send it when you are certain you have another route in, such as the browser console.

Read the firewall back

curl -sS "$L1/servers/123/firewall" "${auth[@]}" | jq '.firewall | {
  inbound_policy, outbound_policy, inbound_ssh_blocked,
  rules: [.rules[] | "\(.direction) \(.action) \(.protocol) \(.port // "any") from \(.source // "any")"]
}'

inbound_ssh_blocked is the field to check before you disconnect.

Which server is using the transfer pool

Transfer is pooled per account, not capped per server. GET /api/v1/account returns this month's pool and each server's share in one request. Use that for fleet monitoring. The per-server endpoint adds previous months and inbound versus outbound. Each billable server adds 500 GB (extra_per_additional_server_gb) to the same pool.

curl -sS "$L1/account" "${auth[@]}" | jq '.account.bandwidth | {
  used_tb, allowance_tb, remaining_tb, extra_per_additional_server_gb, is_over,
  servers: [.servers[] | {id, label, used_tb, inbound_bytes, outbound_bytes}]
}'
curl -sS "$L1/servers/123/bandwidth" "${auth[@]}" | jq '.bandwidth | {
  current, account_pool: .account_pool | {used_tb, allowance_tb, remaining_tb}
}'

specs.bandwidth_tb on a server object is the plan's published figure, not this month's usage. Inbound and outbound are the guest's totals across every interface, public and private, so VNet traffic counts. A server that has not been sampled yet still returns zeros for the current month rather than 404. See Bandwidth: pool, blocks, and overage.

Power actions

action is one of start, stop, restart, shutdown.

curl -sS -X POST "$L1/servers/123/actions" \
  -H "Authorization: Bearer $L1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "restart"}'

shutdown asks the operating system to shut down cleanly (ACPI halt). stop cuts the power. Prefer shutdown unless the server is unresponsive. There is no pause: a QEMU RAM freeze does not survive a node reboot, and billing does not stop while a server is halted. See Power actions.

Private networks

Create a VNet, then attach servers. Caps and CIDR rules are the same as the portal (ten networks per account by default, RFC1918 /29 to /24). A server may join more than one network. The server object lists every attachment as private_networks[].

curl -sS -X POST "$L1/networks" \
  -H "Authorization: Bearer $L1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "office", "cidr": "10.20.0.0/24"}'

curl -sS -X POST "$L1/servers/123/networks" \
  -H "Authorization: Bearer $L1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"network": 12, "address": "10.20.0.10"}'

PATCH /networks/<id> accepts name, cidr, gateway (empty or null clears it), and cloud_init_assign_ips. POST /networks/<id>/sync retries applying the saved network. DELETE /servers/<id>/networks/<network_id> detaches. See Private networks.

Check your quota before a batch job

curl -sS "$L1/account" "${auth[@]}" | jq '.account.api_usage'

Python, with the errors handled

import os
import time
import requests

BASE = "https://layeronecloud.com/api/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['L1_API_KEY']}"


class LayerOneError(RuntimeError):
    def __init__(self, code: str, message: str):
        super().__init__(f"{code}: {message}")
        self.code = code


def call(method: str, path: str, **kwargs):
    for attempt in range(5):
        response = SESSION.request(method, f"{BASE}{path}", timeout=30, **kwargs)
        if response.status_code == 429:
            time.sleep(int(response.headers.get("Retry-After", 60)))
            continue
        payload = response.json()
        if response.ok:
            return payload
        error = payload.get("error", {})
        code = error.get("code", "server_error")
        if code == "server_error" and attempt < 4:
            time.sleep(2 ** attempt)
            continue
        raise LayerOneError(code, error.get("message", ""))
    raise LayerOneError("rate_limited", "Gave up after repeated rate limits.")


def deploy(plan: str, image: str, label: str) -> tuple[int, str]:
    created = call(
        "POST",
        "/servers",
        json={"plan": plan, "image": image, "label": label},
    )
    return created["server"]["id"], created["root_password"]


def wait_for_address(server_id: int, timeout: int = 600) -> str:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        server = call("GET", f"/servers/{server_id}")["server"]
        if server["status"] == "running" and server["ipv4_address"]:
            return server["ipv4_address"]
        time.sleep(10)
    raise TimeoutError(f"server {server_id} never came up")

Note the retry policy: 429 waits for Retry-After, 500 backs off, and every other error raises straight away because waiting will not change the answer.

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.