Skip to main content

Developer Quickstart

Get started in 5 min​

This guide takes you from zero to your first authenticated API call against the Lium platform API.

Step 1 — Get an API key​

  1. Log in to lium.io.
  2. Click Access in the sidebar, then API Keys (lium.io/api-keys).
  3. Click ADD NEW, copy the key, and store it securely — you can copy it again later from the same table.

No browser? lium signup creates the account and its key from the terminal; lium init --no-browser does the same for an existing account.

Step 2 — Make your first request​

List your pods:

export LIUM_API_KEY=your_api_key_here

curl https://lium.io/api/pods \
-H "X-API-Key: $LIUM_API_KEY"

Step 3 — Pick a node and a template​

Renting happens on a node, so list what is available and take an id. Prices are per GPU per hour, in price_per_gpu:

curl https://lium.io/api/executors \
-H "X-API-Key: $LIUM_API_KEY" \
| jq 'sort_by(.price_per_gpu) | .[0] | {id, machine_name, gpu_count, price_per_gpu}'

You also need a template, which decides the image the pod boots:

curl https://lium.io/api/templates \
-H "X-API-Key: $LIUM_API_KEY" \
| jq '.[] | select(.name | contains("Pytorch")) | {id, name}'

Step 4 — Rent the node​

curl -X POST https://lium.io/api/executors/<executor_uuid>/rent \
-H "X-API-Key: $LIUM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pod_name": "my-first-pod",
"user_public_key": "'"$(cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub)"'",
"template_id": "<template_uuid>",
"termination_hours": 1
}'

The key you send is the one you will connect with, so send a public key you hold the private half of. pod_name and user_public_key are required, and you must pass exactly one of template_id or dockerfile_content. termination_hours (1–720) is your safety net: the pod terminates itself after that many hours even if nothing else does.

Renting needs a balance above 15 minutes of the node's price (price_per_gpu × gpu_count / 4); below that the call returns 403 with the required amount in the message.

The response is {"success": true, "pod_id": "<uuid>"}.

Retrying safely. Send an Idempotency-Key header (1–255 letters, digits, -, _, . or :; a UUID is fine) and reuse it when you retry after a lost answer:

  • Within 24 h, the same key with the byte-identical body returns the first call's answer (same pod_id) with Idempotent-Replayed: true, instead of a second pod.
  • A different body with that key is 422 idempotency_key_reused.
  • A repeat while the first call is still running is 409 idempotency_in_progress; retry after Retry-After.
  • If the first call failed (a 5xx or an error) or its answer could not be recorded, it may have rented. The key is then held with no expiry, and every repeat gets 409 idempotency_outcome_unknown. Check lium ps for the pod before you send the request again with a new key.

Without the header nothing changes. POST /executors/rent-by-spec and POST /executors/cluster/rent take the same header when GET /version lists "rent_idempotency" under features; on a backend that does not list it, those two routes ignore the header and a retry rents again. The codes are in Error codes.

Step 4, the short way — rent by spec​

If what you want is a spec — "one H100, cheapest, under $2.50 per GPU-hour" — and any node that meets it will do, skip Step 3 and let the API pick: POST /executors/rent-by-spec selects and rents in one call.

curl -X POST https://lium.io/api/executors/rent-by-spec \
-H "X-API-Key: $LIUM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"gpu_type": "H100",
"gpu_count": 1,
"max_price_per_gpu_hour": 2.5,
"pod_name": "my-first-pod",
"user_public_key": "'"$(cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub)"'",
"termination_hours": 1,
"dry_run": true
}'

The body takes everything POST /executors/{id}/rent takes (pod_name, user_public_key, volume_id, initial_port_count, termination_hours, dockerfile_content …) minus the node, plus the spec:

FieldMeaning
gpu_typeRequired. Short or full name — H100, RTX 4090, NVIDIA H200. Matches a run of name tokens, so H100 takes every H100 variant and H20 does not take an H200
gpu_countGPUs to rent (default 1): the whole node, or a split the provider allows
min_vram_gb, min_download_mbps, min_portsFloors on the host. A host that does not report the figure does not qualify. min_vram_gb is compared with the memory the card reports, in GiB: an "80 GB" H100 reports 79.6, so ask for 79 or less
min_cpus, min_ram_gb, min_disk_gbFloors on what the pod gets: a split rental's share of the host's CPUs, RAM (less the host's reserve, the larger of 4 GiB and 1 % of its RAM; at least 1 GiB) and free disk, in proportion to its GPUs
max_price_per_gpu_hourCeiling on the per-GPU-hour price, checked on the catalog and again on the live node right before the rent
countryISO country code
docker_in_dockerOnly nodes that run Docker-in-Docker (Sysbox)
template_idOptional here: when omitted, the official template of the image recommended for the selected node's GPU and driver is used
dry_runtrue selects and prices but rents nothing — what a client shows before asking to confirm

Among the nodes that satisfy every constraint the API rents the cheapest per GPU-hour; ties go to faster ingress, then higher reliability score (a node without a score ranks at the median score of the matching nodes), then node id, so the same catalog always yields the same pick. If the chosen node is taken between selection and rent (400/409 from the rent path), the next candidate is tried, up to three in all. When all three are gone the answer is the last node's own 400 or 409, its message starting Every matching node was taken before it could be rented.

The response:

{
"success": true,
"dry_run": true,
"pod_id": null,
"template_id": "<template_uuid>",
"price_per_hour": 1.2,
"selected_executor": { "id": "<executor_uuid>", "machine_name": "NVIDIA H100 80GB HBM3", "gpu_count": 8, "price_per_gpu": 1.2, "...": "the node as GET /nodes lists it" },
"candidates": 12,
"alternatives_considered": [ { "id": "…", "machine_name": "…", "price_per_gpu": 1.25, "price_per_hour": 1.25, "download_mbps": 1800, "country_code": "US" } ],
"attempts": 0
}

pod_id is set on a real run (dry_run: false) — continue at Step 5 with it. price_per_hour is what the pod is charged: the node's live per-GPU price × gpu_count (a dry run quotes the catalog price, which can lag the live one by up to 30 s); alternatives_considered lists up to five of the other candidates, cheapest first, including any that were tried before the one rented. attempts is 0 on a dry run; above 1 means an earlier pick was taken and the next candidate was rented.

When nothing matches, the answer is 409 and names the constraint that emptied the list:

{"success": false, "code": "no_executor_matches_spec", "constraint": "min_cpus", "candidates_before": 12,
"message": "min_cpus=64: none of the 12 node(s) matching the earlier constraints satisfies it; the best on offer is 48 for 1 GPU(s)"}

The body also carries the standard error envelope (code, message, hint, request_id) every API error has; the flat fields above are the ones to read.

An unknown gpu_type lists the types on offer; a catalog that is still warming answers 503 with Retry-After. Balance (403) and template (404) errors are the request's own and are not retried on another node. When template_id is omitted and a picked node has no recommended template, that node is skipped like a taken one; on a dry run the answer is 400 asking for a template_id. template_id together with dockerfile_content is still refused (422). Authentication and rate limits are those of the rent path. The Idempotency-Key header works here as it does on the rent path: send one (a UUID is fine) and a retry after a lost response, with the same key and the same body within 24 hours, returns the first call's answer (the same pod_id, with Idempotent-Replayed: true) instead of renting another node. A refusal the rent itself returned is replayed too, so retry it with a new key; a request refused before any rent starts (a 422 validation error or a 429 rate limit) keeps no record, and the same key can be sent again. A call that failed with a 5xx or lost its answer may still have rented, so its key answers 409 idempotency_outcome_unknown from then on: check lium ps and rent again only with a new key. POST /executors/cluster/rent takes the header the same way. Until GET /version lists "rent_idempotency" under features, the backend ignores the header here and a retry rents again. GET /version lists "rent_by_spec" under features, so a client can detect the call with one small request and fall back to the pick-by-id path on a backend that predates it.

Step 5 — Wait for it, then connect​

curl https://lium.io/api/pods/<pod_id> \
-H "X-API-Key: $LIUM_API_KEY" \
| jq '{status, ssh_connect_cmd}'

Poll until status is RUNNING. At that point ssh_connect_cmd carries the command to reach the pod.

Step 6 — Terminate it​

Once the pod is running you are billed per second at the node's hourly price, counted from the moment the pod was created, until you do this (a start that fails is never billed; the full rule is the billing lifecycle table):

curl -X DELETE https://lium.io/api/pods/<pod_id> \
-H "X-API-Key: $LIUM_API_KEY"

End state: You have rented, used and released a pod entirely over the API. The live OpenAPI reference is at lium.io/documents.

Next — the CLI​

Install the CLI for scripted workflows:

curl -fsSL https://lium.io/install.sh | bash # or `pip install lium.io`
lium --help

See the CLI installation guide for platform notes, version pinning, and the Python package alternative.

Next steps​