---
sidebar_position: 2.5
title: API Authentication
description: How requests to the Lium platform API are authenticated — the X-API-Key header, where the CLI and SDK read the key from and in what order, how to verify a key, and what the error responses look like.
tags: [api, agents]
---

> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lium.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/pod-users/api-keys); for a first walkthrough see the [Developer Quickstart](./quickstart).

## The header

```bash
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 send `X-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 `Bearer` prefix, 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](/pod-users/api-keys).

The OpenAPI document at [`https://lium.io/api/openapi.json`](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 balance` shows one account and a script shows another, the script most likely has `LIUM_API_KEY` set to a different key. `lium config get api.api_key` prints 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 with `No API key configured` and tells you to run `lium init`, which opens a browser or, with `lium init --no-browser`, prints an approval URL and session id (see [AI Agents](./agents#authentication)).
- 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:

```bash
# 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
```

```python
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 is always:

```json
{
  "success": false,
  "error": "HTTP error",
  "message": "API key not found",
  "status_code": 401,
  "error_id": null,
  "timestamp": "2026-09-05T20:05:18.392799",
  "api_context": null
}
```

| 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](/pod-users/security).
- One key per integration, so a leaked key can be revoked on its own. See [API Keys](/pod-users/api-keys#key-hygiene).
- Log the key's **prefix**, never the key, when you record which credential a job used.

## Related

- [API Keys](/pod-users/api-keys) — create, rotate and delete keys in the dashboard
- [Developer Quickstart](./quickstart) — first authenticated request
- [API](./api) — every endpoint, payload and response (live OpenAPI 3.1 spec)
- [AI Agents](./agents) — getting a key with no human in the loop (`lium signup`, `lium init --no-browser`)
