API Authentication
Every call to https://lium.io/api/... that touches your account is authenticated with an API key sent in the X-API-Key request header. This page is the reference for that mechanism: the header, where the key comes from, how to check that a key works before you rent anything, and how failures are reported. For creating and rotating keys see API Keys; for a first walkthrough see the Developer Quickstart.
The header
curl https://lium.io/api/pods -H "X-API-Key: $LIUM_API_KEY"
- Header name:
X-API-Key(header names are case-insensitive; the CLI and SDK sendX-API-KEY). - Value: the key as the Access page's copy button gives it (the table shows the first 10 and last 5 characters and keeps the full key; copy it again any time). No
Bearerprefix, no encoding. - One header per request; there is no session or cookie to keep.
- The key identifies the account. There are no scopes yet: a key can read and change everything the account can, and everything it rents is billed to the account. Read / rent / manage scopes are lium-platform#208, not released — see API Keys.
The OpenAPI document at https://lium.io/api/openapi.json declares the session-token scheme the lium.io web app uses, JwtAccessBearer (type: http, scheme: bearer), on account-bound operations. API-key requests go in X-API-Key whatever an operation is marked with; the two are accepted side by side. Before you generate a client, look at components.securitySchemes in the document you fetched: if it lists only JwtAccessBearer, the generated security handler cannot send an API key, so add the header yourself; if it also lists an apiKey scheme with in: header and name: X-API-Key, the generator wires the key up for you.
Where the key comes from
The CLI and the Python SDK read the same key from the same places. When both are set, the environment variable wins:
| Order | Source | Set by |
|---|---|---|
| 1 | LIUM_API_KEY environment variable | you (export LIUM_API_KEY=...) |
| 2 | ~/.lium/config.ini, [api] section, api_key | lium init, lium signup, lium config set api.api_key |
Consequences worth knowing:
- The environment variable overrides the config file silently. If
lium balanceshows one account and a script shows another, the script most likely hasLIUM_API_KEYset to a different key.lium config get api.api_keyprints the first 8 and last 4 characters of the key the CLI resolves (environment first, then the file) — enough to tell two keys apart. - With no key anywhere, the SDK raises
ValueError("No API key found. Set LIUM_API_KEY or ~/.lium/config.ini"); the CLI exits 2 withNo API key configuredand tells you to runlium init, which opens a browser or, withlium init --no-browser, prints an approval URL and session id (see AI Agents). - The SDK also accepts a key directly:
Lium(Config(api_key="...")).
The base URL can be overridden the same way (LIUM_BASE_URL, default https://lium.io/api), which matters only for staging environments.
Verify a key before you rent
Not every endpoint requires authentication. GET /executors (the marketplace) and GET /templates answer 200 with any or no key, so lium ls and lium templates succeeding tells you nothing about the key. Use an endpoint that is tied to the account:
# REST
curl -sS https://lium.io/api/users/me -H "X-API-Key: $LIUM_API_KEY"
# → 200 with the account record (id, email, balance, …) or 401 (see below)
# CLI — the same call, reduced to the balance
lium balance --json
# → {"balance_usd": 12.34} exit 0
# → exit 3 with the error text when the key is rejected
from lium.sdk import Lium, LiumAuthError
try:
balance = Lium().balance() # GET /users/me
except LiumAuthError as exc:
raise SystemExit(f"API key rejected: {exc}")
Check the balance as well as the key: renting needs a balance above about 15 minutes of the node's hourly price, and a lower balance is refused with 403 even though the key itself is valid.
Error responses
Authentication failures return the platform's standard JSON error envelope with an HTTP status code that says which kind of failure it is. The body carries an error object with a stable code, the message, what to do next and the request id, plus the flat fields older clients read; the same id is in the X-Request-Id response header:
{
"success": false,
"error": {
"code": "unauthorized",
"message": "API key not found",
"hint": "Send a valid key in X-API-Key (lium.io/api-keys) or sign in again; `lium config get api.api_key` shows the key the CLI uses.",
"request_id": "2510eff403c14e4e9ff4bf744bb05df6"
},
"message": "API key not found",
"status_code": 401,
"error_id": null,
"request_id": "2510eff403c14e4e9ff4bf744bb05df6",
"timestamp": "2026-09-08T22:04:34.701777",
"api_context": null
}
An agent branches on error.code, shows error.hint, and quotes error.request_id when it asks for help.
| Status | message | Meaning | What to do |
|---|---|---|---|
401 | API key missing from headers | No X-API-Key header on an authenticated endpoint | Send the header; check the key was actually exported into the process that made the call |
401 | API key not found | The header was present but the key is unknown, deleted or mistyped | Compare the key's prefix with Access → API Keys on lium.io; create a new key if it was deleted |
403 | varies, includes the amount needed | The key is valid but the account may not do this — most often an insufficient balance for the rent (POST /executors/{id}/rent) or an unverified account | Top up (lium topup) or complete verification; the key does not need to change |
429 | (rate limit text) | Too many requests from this key | Back off and retry |
Authentication is checked before the resource is looked up: an unknown key against an unknown pod id returns 401 API key not found, not 404, so fix the key before reading anything into a "not found".
The CLI maps these to exit codes so scripts can branch without parsing text: authentication and other API refusals exit 3 (EXIT_API_ERROR), permission problems exit 6, missing configuration exits 2. The SDK raises LiumAuthError for 401, LiumPermissionError for 403, LiumNotFoundError for 404 and LiumRateLimitError for 429; all of them subclass LiumError.
Endpoints that do not require a key never return 401, which is why they make poor health checks for the key (see above).
Key handling rules of thumb
- Keep the key out of pod filesystems, Docker images and templates; pass it as an environment variable at run time. See Pod security.
- One key per integration, so a leaked key can be revoked on its own. See API Keys.
- Log the key's prefix, never the key, when you record which credential a job used.
Say which client you are
Every rental records the client that made it (rental_history.source), read from the X-Source request header: ui, cli, sdk or mcp, matched case-insensitively; any other value is ignored.
- The web app sends
ui. - The Python SDK sends
sdkon every request to the platform API (Lium()'s default), so a client built on the SDK is recorded assdkwithout doing anything. lium upbuildsLium(source="cli"), so CLI rentals are recorded ascli.mcpis for the MCP server; it counts an agent's rental apart from the SDK the server wraps. The server does not send it yet (asked on lium-mcp#5).
A request that carries an API key and no recognised X-Source is recorded as programmatic, which is where a client of your own lands today; there is no value for it yet. A request with neither an API key nor a recognised header counts as ui.
Related
- API Keys — create, rotate and delete keys in the dashboard
- Developer Quickstart — first authenticated request
- API — every endpoint, payload and response (live OpenAPI 3.1 spec)
- AI Agents — getting a key with no human in the loop (
lium signup,lium init --no-browser)