Error codes
An error response from https://lium.io/api has this shape (the routes listed under Routes with the older body are the exception):
{
"success": false,
"error": {
"code": "node_unavailable",
"message": "Node is not available.",
"hint": "The node was rented or went offline between listing and renting; pick another (`lium ls`) or retry in a minute.",
"request_id": "0fd17bdf43884d48af88bca99c884936"
},
"message": "Node is not available.",
"status_code": 400,
"error_id": null,
"request_id": "0fd17bdf43884d48af88bca99c884936",
"timestamp": "2026-09-07T01:48:42.756086",
"api_context": null
}
error.codeis stable: key on it, not on the message text. The message may be reworded; the code will not.error.hintsays what to do next in one sentence.error.request_ididentifies this request in the platform's logs. The same value comes back in theX-Request-Idresponse header on every response, success or failure, and is theerror_idof a 5xx. Quote it when you ask for help (support@lium.io, Discord#support): it points at every log line and the exception of that exact request.- You can send your own
X-Request-Id(8–64 characters ofA-Z a-z 0-9 . _ : -) and the API will keep it, so an agent's run id can be found in the platform logs. - The flat fields (
message,status_code,error_id,validation_errors,details) are the older shape and stay for compatibility.
The codes​
error.code | HTTP | When | What to do |
|---|---|---|---|
unauthorized | 401 | No key or session, or the key is unknown | Send a valid key in X-API-Key (API keys); lium config get api.api_key shows which key the CLI uses, masked |
api_key_inactive | 401 | The key was deactivated or has expired | Create a new key |
insufficient_balance | 403 | Balance is zero or negative (renting needs a positive balance covering 15 minutes of the node) | Top up at lium.io/billing |
account_balance_invalid | 403 | The account's balance could not be read | Contact support with the request_id; topping up does not clear it |
email_not_verified | 403 | The account's e-mail is not verified | Verify it, then retry |
forbidden | 403 | The resource belongs to another account, or this key may not do that | Check which key the CLI uses with lium config get api.api_key |
pod_not_found | 404 | No such pod on this account | lium ps lists yours; a deleted pod's entries stay in the account's event log, GET /users/me/events (API key or signed-in session) |
node_not_found | 404 | The node left the marketplace or the id is wrong | Pick another from lium ls (UUID or the huid it prints) |
template_not_found | 404 | No such template, or a private one of another account | lium templates |
not_found | 404 | Anything else | Check the id |
node_rent_in_progress | 400/409 | Another rental is starting on this node | Retry in ~30 s or pick another node |
node_not_verified | 400 | The validator has not passed this node yet | Choose a node lium ls marks available |
node_unavailable | 400 | The node was rented or went offline between listing and renting, or has no free GPU | Pick another node or retry in a minute |
node_paused_by_provider | 409 | The provider paused new rentals on this node | Pick another node |
provider_banned | 409 | The provider is banned | Pick a node from another provider |
gpu_count_invalid | 400 | The requested GPU count is not available, is below the node's minimum, or a whole-node rental is required | lium ls --format json shows gpu_count per node |
gpu_split_not_allowed | 400 | This node cannot be split | Rent all of its GPUs or pick a node that allows splitting |
template_invalid | 400 | The template is not yours, or a one-time/temporary template is being reused | Use a template you own or an official one |
ttl_invalid | 400 | Termination outside 1–720 hours | Fix the value |
pod_state_conflict | 400 | The pod is not in a state that allows this (already deleting, not running, stuck pending) | lium ps shows its status; wait or delete it |
bad_request | 400 | Refused for the reason in message | Fix the input |
validation_error | 422 | A field failed validation | Fix the fields listed in validation_errors |
idempotency_key_invalid | 422 | POST /executors/{executor_uuid}/rent with an Idempotency-Key header that is not 1–255 letters, digits, -, _, . or : | Send a well-formed key (a UUID is fine) or omit the header |
idempotency_key_reused | 422 | The same Idempotency-Key was sent within 24 h with a different request body | Use a new key for a new request; repeat the original request unchanged to get its first answer again (Idempotent-Replayed: true) |
idempotency_in_progress | 409 | The first request with this Idempotency-Key is still running | Retry after the Retry-After seconds (1) with the same key to receive its answer |
conflict | 409 | The resource changed under you | Re-read and retry |
no_executor_matches_spec | 409 | POST /executors/rent-by-spec: no node satisfies the spec; the body names the first constraint that left no candidate and the best value the remaining nodes offered for it | Relax that constraint (or raise max_price_per_gpu_hour); lium ls shows what is available now |
method_not_allowed | 405 | The path exists, but not for this HTTP method | GET /api/openapi.json lists the methods per path |
rate_limited | 429 | Too many requests from this IP | Retry after a minute |
client_error | other 4xx | Refused for the reason in message | Fix the request; status_code says which rule |
upstream_unavailable | 502–504 | A dependency (validator, payment provider) did not answer | Retry in a minute; quote the request_id if it persists |
internal_error | 500 | Unhandled error | Retry once; quote the request_id to support |
A few endpoints send their own, upper-case codes in the same field (for example BACKUP_STORAGE_BUSY, POD_NOT_FOUND on the Discord support routes); they are documented on those endpoints.
Routes with the older body​
The /volumes, /bookmark and /machine-requests handlers catch their own errors and answer {"success": false, "message": "…"} with the HTTP status and no error object. Key on the status there. /backup-configs and /backup-logs re-raise their 4xx (so those carry the error object) and answer the flat body only on an unexpected 500. volume_not_found is sent by GET /volumes/{volume_id}/current-stats (404, through the global handler); the five CRUD handlers in routes/volume.py answer the flat body.
In the SDK and the CLI​
Coming with lium#190 (not in CLI 0.0.45, which prints the message only): every SDK exception carries the three fields:
from lium.sdk import Lium, LiumError
try:
Lium().up(executor_id="…")
except LiumError as e:
if e.code == "insufficient_balance":
... # top up and retry
elif e.code in ("node_unavailable", "node_rent_in_progress"):
... # pick another node
else:
raise # e.hint says what to do; e.request_id is what to quote
The CLI prints the hint and the request id under the error and exits non-zero:
$ lium up brave-wolf-26
Node brave-wolf-26 could not be rented: API error 400: Node is not available. Run 'lium ps' to check whether a pod was created. Run 'lium ls --format json' for the nodes rentable now.
The node was rented or went offline between listing and renting; pick another (`lium ls`) or retry in a minute.
request_id: 0fd17bdf43884d48af88bca99c884936
On the commands that take --json (lium exec, lium balance, lium describe, …; lium up has no --json) the envelope on stderr carries the server's code: {"ok": false, "error": {"code": "node_unavailable", "message": "…", "hint": "…", "exit_code": 3}, "data": {"request_id": "…"}}.