# ApiLux API

This document is **generated** from ApiLux's live contract. Every endpoint below is implemented.

- **Base URL**: `https://apilux.net/api/v1`
- **Authentication**: `Authorization: Bearer apl_YOUR_API_KEY`
- **Environment variables**: `APILUX_BASE_URL`, `APILUX_API_KEY`
- **Currency**: 1 credit = 1 VND

## API key security

- A key is shown **exactly once** at creation — ApiLux stores only a hash, never the original.
- Use keys **server-side only**. Putting one in frontend code publishes it.
- If a key leaks, revoke it in `/api-keys`; revocation is immediate.
- Set per-key limits so one abused key cannot drain the wallet.

## Quick start

Replace `MODEL_ID` with an ID from `GET /models`.

### cURL

```bash
curl https://apilux.net/api/v1/chat/completions \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "MODEL_ID",
    "messages": [{ "role": "user", "content": "Xin chào" }]
  }'
```

### Node.js

```javascript
const res = await fetch("https://apilux.net/api/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APILUX_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "MODEL_ID",
    messages: [{ role: "user", content: "Xin chào" }],
  }),
});
const data = await res.json();
console.log(data.choices[0].message.content);
```

### Python

```python
import os, requests

res = requests.post(
    "https://apilux.net/api/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {os.environ['APILUX_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "model": "MODEL_ID",
        "messages": [{"role": "user", "content": "Xin chào"}],
    },
)
print(res.json()["choices"][0]["message"]["content"])
```

> **Client timeouts**: seconds are enough for chat; **use 180–360s for image generation**; video returns a job immediately, then poll.

## Endpoints

### GET /models

**List models** — Returns the models you can call right now. Models without a configured price, or that are disabled, are not listed.

Billing: Free — no credit is charged.

| Response field | Type | Description |
|---|---|---|
| `data[].id` | `string` | Public model ID to send in the `model` field of other requests. |
| `data[].kind` | `"TEXT" | "IMAGE" | "VIDEO"` | Model kind. |
| `data[].capabilities` | `string[]` | Endpoints this model can be used with. |
| `data[].price_credit_per_unit` | `integer` | Sale price in credits (1 credit = 1 VND) per unit. |
| `data[].billing_unit` | `"million_tokens" | "image" | "second"` | The unit the price applies to. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/models \
  -H "Authorization: Bearer apl_YOUR_API_KEY"
```

</details>

<details><summary>Node.js</summary>

```javascript
const res = await fetch("https://apilux.net/api/v1/models", {
  headers: { Authorization: `Bearer ${process.env.APILUX_API_KEY}` },
});
const { data } = await res.json();
console.log(data.map((m) => m.id));
```

</details>

<details><summary>Python</summary>

```python
import os, requests

res = requests.get(
    "https://apilux.net/api/v1/models",
    headers={"Authorization": f"Bearer {os.environ['APILUX_API_KEY']}"},
)
print([m["id"] for m in res.json()["data"]])
```

</details>

### POST /chat/completions

**Chat Completions** — Generate text, returned in the familiar OpenAI response shape.

Billing: By total tokens (input + output), priced per 1,000,000 tokens. Minimum 1 credit per successful request.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID from `GET /models`. |
| `messages` | `Message[]` | yes | Array of `{ role, content }` where `role` is `system` | `user` | `assistant`. |
| `stream` | `boolean` | — | `true` returns an OpenAI-compatible SSE stream; the final chunk carries `usage`. Defaults to `false`. |

| Response field | Type | Description |
|---|---|---|
| `choices[0].message.content` | `string` | The generated content. |
| `usage` | `object` | `prompt_tokens`, `completion_tokens`, `total_tokens`. |
| `apilux.credit_charged` | `integer` | Credits charged for this request. |
| `apilux.balance_after` | `integer` | Wallet balance after the charge. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/chat/completions \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "MODEL_ID",
    "messages": [{ "role": "user", "content": "Xin chào" }]
  }'
```

</details>

<details><summary>Node.js</summary>

```javascript
const res = await fetch("https://apilux.net/api/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APILUX_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "MODEL_ID",
    messages: [{ role: "user", content: "Xin chào" }],
  }),
});
const data = await res.json();
console.log(data.choices[0].message.content);
```

</details>

<details><summary>Python</summary>

```python
import os, requests

res = requests.post(
    "https://apilux.net/api/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {os.environ['APILUX_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "model": "MODEL_ID",
        "messages": [{"role": "user", "content": "Xin chào"}],
    },
)
print(res.json()["choices"][0]["message"]["content"])
```

</details>

> The response carries an extra `apilux` object beyond the OpenAI shape. OpenAI clients ignore unknown fields, so nothing breaks.

### POST /chat

**Chat (original contract)** — ApiLux's first public endpoint, with a leaner response. Kept permanently for existing integrations — new projects should use `/chat/completions`.

Billing: Same as `/chat/completions`.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID. |
| `messages` | `Message[]` | yes | Array of `{ role, content }`. |

| Response field | Type | Description |
|---|---|---|
| `reply` | `string` | The generated content. |
| `tokensUsed` | `integer` | Total tokens used. |
| `creditCharged` | `integer` | Credits charged. |
| `balanceAfter` | `integer` | Balance after the charge. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/chat \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "MODEL_ID",
    "messages": [{ "role": "user", "content": "Xin chào" }]
  }'
```

</details>

> On this endpoint a disabled model returns `unknown_model` (400) rather than `model_disabled` (503) — the original behaviour is preserved deliberately.

### POST /messages

**Messages (Anthropic-compatible)** — Generate text over the Anthropic Messages protocol, with `tools` and streaming. This is the endpoint for Claude Code and the Anthropic SDK.

Billing: By total tokens (input + output + cache tokens), priced per 1,000,000 tokens. Minimum 1 credit per successful request.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID from `GET /models`. |
| `max_tokens` | `integer` | yes | Maximum tokens to generate. Required by the Anthropic protocol. |
| `messages` | `Message[]` | yes | Array of `{ role, content }` where `role` is `user` | `assistant`. `content` is a string or an array of content blocks (`text`, `image`, `tool_use`, `tool_result`). |
| `system` | `string | Block[]` | — | System prompt. In the Anthropic protocol this is a separate parameter, not an element of `messages`. |
| `tools` | `Tool[]` | — | Tools the model may call. Forwarded unchanged. |
| `stream` | `boolean` | — | `true` returns an SSE stream using the Anthropic event protocol (`message_start` … `message_stop`). |

| Response field | Type | Description |
|---|---|---|
| `content` | `Block[]` | The content blocks the model produced. |
| `stop_reason` | `string` | Why generation stopped, e.g. `end_turn` or `tool_use`. |
| `usage` | `object` | `input_tokens`, `output_tokens`, plus cache fields when present. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/messages \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "MODEL_ID",
    "max_tokens": 1024,
    "messages": [{ "role": "user", "content": "Xin chào" }]
  }'
```

</details>

<details><summary>Node.js</summary>

```javascript
const res = await fetch("https://apilux.net/api/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APILUX_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "MODEL_ID",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Xin chào" }],
  }),
});
const data = await res.json();
console.log(data.content[0].text);
```

</details>

<details><summary>Python</summary>

```python
import os, requests

res = requests.post(
    "https://apilux.net/api/v1/messages",
    headers={
        "Authorization": f"Bearer {os.environ['APILUX_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "model": "MODEL_ID",
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": "Xin chào"}],
    },
)
print(res.json()["content"][0]["text"])
```

</details>

> Use `Authorization: Bearer apl_...` just like every other ApiLux endpoint.
> `cache_control` is forwarded unchanged. Cache tokens are billed at the standard rate today because ApiLux has no separate cache pricing yet.

### POST /messages/count_tokens

**Count tokens** — Count the input tokens of a conversation before calling the model.

Billing: No credits are charged. Account rate limits still apply.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID from `GET /models`. |
| `messages` | `Message[]` | yes | Same shape as `POST /messages`. |
| `system` | `string | Block[]` | — | System prompt, if any. |
| `tools` | `Tool[]` | — | Tool definitions consume tokens too, so include them for an accurate count. |

| Response field | Type | Description |
|---|---|---|
| `input_tokens` | `integer` | The input token count. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/messages/count_tokens \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "MODEL_ID",
    "messages": [{ "role": "user", "content": "Xin chào" }]
  }'
```

</details>

> ApiLux never estimates tokens. This number comes from the model's own counter.

### POST /responses

**Responses** — Generate text over the OpenAI Responses protocol, with `tools` and streaming. This is the endpoint for Codex.

Billing: By total tokens (input + output), priced per 1,000,000 tokens. Minimum 1 credit per successful request.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID from `GET /models`. |
| `input` | `string | Item[]` | yes | A string, or an array of conversation items (`message`, `function_call`, `function_call_output`, …). |
| `instructions` | `string` | — | The system prompt for this protocol. |
| `tools` | `Tool[]` | — | Tools the model may call. Forwarded unchanged. |
| `max_output_tokens` | `integer` | — | Cap on generated tokens. |
| `stream` | `boolean` | — | `true` returns an SSE stream using the Responses event protocol (`response.created` … `response.completed`). |

| Response field | Type | Description |
|---|---|---|
| `output` | `Item[]` | The items the model produced. |
| `status` | `string` | `completed`, `incomplete` or `failed`. |
| `usage` | `object` | `input_tokens`, `output_tokens`, `total_tokens`. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/responses \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "MODEL_ID",
    "input": "Xin chào"
  }'
```

</details>

> Codex CLI requires exactly this protocol. Set `wire_api = "responses"` in `~/.codex/config.toml`, then restart Codex/VS Code.

### POST /images/generations

**Image generation** — Create a new image from a text prompt.

Billing: By the **number of images actually produced**, not the `n` you requested — some models return a single image regardless.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID whose `kind` is `IMAGE`. |
| `prompt` | `string` | yes | Description of the image. Max 4000 characters. |
| `size` | `string` | — | For example `"1024x1024"`. Only meaningful for models that support it. |
| `n` | `integer` | — | How many images to create, 1–4. Defaults to 1. |

| Response field | Type | Description |
|---|---|---|
| `data[].b64_json` | `string` | The image as base64. ApiLux always returns base64, never a provider URL. |
| `data[].mime_type` | `string` | For example `image/png`. |
| `usage.images` | `integer` | Number of images charged. |
| `apilux.credit_charged` | `integer` | Credits charged. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/images/generations \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 360 \
  -d '{
    "model": "IMAGE_MODEL_ID",
    "prompt": "Giao diện SaaS sáng, tối giản",
    "size": "1024x1024"
  }' \
  | jq -r '.data[0].b64_json' | base64 -d > image.png
```

</details>

<details><summary>Node.js</summary>

```javascript
import { writeFileSync } from "node:fs";

const res = await fetch("https://apilux.net/api/v1/images/generations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APILUX_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "IMAGE_MODEL_ID",
    prompt: "Giao diện SaaS sáng, tối giản",
  }),
});
const data = await res.json();
writeFileSync("image.png", Buffer.from(data.data[0].b64_json, "base64"));
```

</details>

<details><summary>Python</summary>

```python
import base64, os, requests

res = requests.post(
    "https://apilux.net/api/v1/images/generations",
    headers={"Authorization": f"Bearer {os.environ['APILUX_API_KEY']}"},
    json={"model": "IMAGE_MODEL_ID", "prompt": "Giao diện SaaS sáng, tối giản"},
    timeout=360,
)
with open("image.png", "wb") as f:
    f.write(base64.b64decode(res.json()["data"][0]["b64_json"]))
```

</details>

> Image generation is much slower than chat. Use a client timeout between 180 and 360 seconds.

### POST /images/edits

**Image editing** — Edit an existing image from a text instruction: keep the subject and composition, adjust colours, retouch, change details.

Billing: By the number of images returned, same as generation.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID whose `kind` is `IMAGE`. |
| `prompt` | `string` | yes | The edit instruction. Being specific keeps the original intact. Max 4000 characters. |
| `image.b64_json` | `string` | — | Source image as raw base64. Requires `mime_type`. |
| `image.data_url` | `string` | — | Or a `data:image/png;base64,...` data URL — then `mime_type` is not needed. |
| `image.mime_type` | `"image/png" | "image/jpeg" | "image/webp"` | — | Required with `b64_json`. Max 8 MB decoded. |

| Response field | Type | Description |
|---|---|---|
| `data[].b64_json` | `string` | The edited image, as base64. |
| `usage.images` | `integer` | Number of images charged. |

<details><summary>cURL</summary>

```bash
IMG=$(base64 -w 0 source.png)

curl https://apilux.net/api/v1/images/edits \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 360 \
  -d "{
    \"model\": \"IMAGE_MODEL_ID\",
    \"prompt\": \"Giữ nguyên nhân vật, chỉ làm sáng nền\",
    \"image\": { \"b64_json\": \"$IMG\", \"mime_type\": \"image/png\" }
  }"
```

</details>

<details><summary>Node.js</summary>

```javascript
import { readFileSync, writeFileSync } from "node:fs";

const res = await fetch("https://apilux.net/api/v1/images/edits", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APILUX_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "IMAGE_MODEL_ID",
    prompt: "Giữ nguyên nhân vật, chỉ làm sáng nền",
    image: {
      b64_json: readFileSync("source.png").toString("base64"),
      mime_type: "image/png",
    },
  }),
});
const data = await res.json();
writeFileSync("edited.png", Buffer.from(data.data[0].b64_json, "base64"));
```

</details>

> Images are sent as JSON + base64, not multipart. Limit is 8 MB per image; PNG, JPEG and WEBP only.

### POST /videos/generations

**Video generation** — Create a video from a prompt, from an image, or from a first and last frame. Returns a job immediately — rendering takes tens of seconds to minutes.

Billing: Charged **at job creation**, by the `duration_seconds` you requested. Polling costs nothing extra. A `failed` job is refunded automatically, exactly once.

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | `string` | yes | Public model ID whose `kind` is `VIDEO`. |
| `prompt` | `string` | yes | Describe the scene, motion and style. Max 4000 characters. |
| `duration_seconds` | `4 | 6 | 8` | yes | Only 4, 6 or 8. Anything else is rejected before the provider is called. |
| `aspect_ratio` | `"16:9" | "9:16"` | — | Defaults to `16:9`. Use `9:16` for vertical video. |
| `image` | `ImageInput` | — | First frame, for image-to-video. Same shape as `image` on the edit endpoint. |
| `last_frame` | `ImageInput` | — | Last frame. Must be sent together with `image`. |

| Response field | Type | Description |
|---|---|---|
| `id` | `string` | The ApiLux job ID (`vidjob_...`). Use it to poll and download. |
| `status` | `"queued" | "running" | "completed" | "failed"` | Status at creation time, usually `queued`. |
| `requested_duration_seconds` | `integer` | The number of seconds you were charged for. |
| `apilux.credit_charged` | `integer` | Credits charged at creation. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/videos/generations \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "VIDEO_MODEL_ID",
    "prompt": "Sáng sớm trên biển, chuyển động điện ảnh",
    "duration_seconds": 4,
    "aspect_ratio": "16:9"
  }'
```

</details>

<details><summary>Python</summary>

```python
import os, time, requests

BASE = "https://apilux.net/api/v1"
HEAD = {"Authorization": f"Bearer {os.environ['APILUX_API_KEY']}"}

job = requests.post(
    f"{BASE}/videos/generations",
    headers=HEAD,
    json={
        "model": "VIDEO_MODEL_ID",
        "prompt": "Sáng sớm trên biển, chuyển động điện ảnh",
        "duration_seconds": 4,
    },
).json()

while True:
    time.sleep(5)
    state = requests.get(f"{BASE}/videos/generations/{job['id']}", headers=HEAD).json()
    if state["status"] in ("completed", "failed"):
        break

if state["status"] == "completed":
    mp4 = requests.get(BASE.rsplit("/v1", 1)[0] + state["content_url"], headers=HEAD)
    open("video.mp4", "wb").write(mp4.content)
```

</details>

> If the create request already returned an `id`, do not resend it — poll that `id`. Resending creates a second job and charges again.

### GET /videos/generations/{jobId}

**Video job status** — Check a video job's progress. Only the job's owner can read it.

Billing: Free. Polling never charges anything.

| Field | Type | Required | Description |
|---|---|---|---|
| `jobId` | `string (path)` | yes | The job ID returned at creation. |

| Response field | Type | Description |
|---|---|---|
| `status` | `"queued" | "running" | "completed" | "failed"` | Current status. |
| `content_url` | `string` | Present only when `completed`. Path to download the MP4 through ApiLux. |
| `completed_at` | `integer (unix)` | Present only when `completed`. When the job finished. |
| `expires_at` | `integer (unix)` | Present only when `completed`. The ApiLux ACCESS deadline (`completed_at` + 24h). After it, the download endpoint returns 404. ApiLux stores no file — this is how long ApiLux will proxy the result, not a guarantee the provider retains it for the full 24h. |
| `actual_duration_seconds` | `integer` | Actual seconds, when the provider reports it. |
| `apilux.credit_refunded` | `integer` | Credits refunded (non-zero when the job failed). |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/videos/generations/vidjob_YOUR_JOB_ID \
  -H "Authorization: Bearer apl_YOUR_API_KEY"
```

</details>

> Poll about every 5 seconds. Videos usually finish in 40–180 seconds depending on model and length.

### GET /videos/generations/{jobId}/content

**Download video** — Download the MP4 of a completed job. The file is streamed through ApiLux, so your ApiLux API key is all you need.

Billing: Free. You were charged at job creation.

| Field | Type | Required | Description |
|---|---|---|---|
| `jobId` | `string (path)` | yes | A job ID in the `completed` state. |

| Response field | Type | Description |
|---|---|---|
| `(binary)` | `video/mp4` | The MP4 file contents. |

<details><summary>cURL</summary>

```bash
curl https://apilux.net/api/v1/videos/generations/vidjob_YOUR_JOB_ID/content \
  -H "Authorization: Bearer apl_YOUR_API_KEY" \
  -o video.mp4
```

</details>

> ApiLux does not promise permanent video storage. Download the file soon after the job completes.

## Error codes

Errors always take the shape `{ "error": { "code", "message" } }`. `code` is the **stable** string — branch on it, never on `message`.

| HTTP | `code` | Meaning |
|---|---|---|
| 401 | `auth_missing_or_malformed` | Missing `Authorization` header, or it is not in the `Bearer apl_...` form. |
| 401 | `auth_invalid` | The API key does not exist. |
| 401 | `auth_revoked` | The API key has been revoked. |
| 401 | `auth_expired` | The API key has expired. |
| 503 | `model_catalog_disabled` | The public model catalog is temporarily hidden. Model IDs you already know still work — only this listing endpoint is paused. |
| 400 | `invalid_request` | Invalid body (missing field, wrong type, value out of range). |
| 400 | `unknown_model` | The model does not exist on ApiLux. |
| 400 | `capability_not_supported` | The model exists but cannot be used on this endpoint (e.g. a text model on an image endpoint). |
| 503 | `pricing_not_configured` | The model is implemented but has no price configured on ApiLux yet. This is our state, not a client error. |
| 503 | `model_disabled` | The model is currently disabled. |
| 503 | `model_temporarily_unavailable` | The model is enabled and priced, but the upstream provider is not currently serving it. This is temporary and can clear on its own — the model reappears in `GET /v1/models` once the provider serves it again. You are not charged and no provider call is made. |
| 402 | `insufficient_balance` | Wallet balance is not enough for this request. |
| 409 | `duplicate_request` | A create request with the same `Idempotency-Key` is already in progress or has completed. ApiLux does NOT call the provider again and does NOT charge twice. If the original request finished, the response carries `job_id` so you can fetch the result there. Applies to the media create endpoints (`images/generations`, `images/edits`, `videos/generations`). |
| 429 | `api_key_limit_exceeded` | This API key hit a limit you configured yourself. The wallet still has funds — only this key is capped until the window resets. The response carries `limit_type` (`spending` | `requests` | `tokens`) and `period` (`daily` | `monthly` | `lifetime`). Token limits on text endpoints are measured from actual usage, so the total can exceed the limit by at most one request. |
| 503 | `api_key_quota_reconciliation_required` | An earlier text request on this same key has not been reconciled yet. The usual cause: the provider returned a successful result **without token usage**, so the token limit for that window can no longer be trusted and ApiLux stops rather than let the limit be exceeded silently. Retrying will not help until that is done — contact support. You are not charged for the blocked request, no provider call is made, and image/video endpoints are unaffected. |
| 429 | `rate_limit_exceeded` | Rate limit exceeded (requests per second or per minute). Carries a `Retry-After` header and a `retry_after_seconds` field; `limit_type` says which axis blocked. A blocked request never reaches the provider and is never charged. Wait the indicated seconds, then retry. |
| 503 | `system_maintenance` | ApiLux is under maintenance. All generation endpoints are paused; you are not charged and no provider call is made. Retry later. |
| 503 | `ai_requests_disabled` | An operator has paused AI requests while the rest of the service keeps running. Distinct from `system_maintenance` because it is a narrower switch. |
| 503 | `provider_catalog_stale` | ApiLux no longer has recent enough evidence of what the provider is serving, so it stops rather than charge for a request that may fail. This is our problem, **not** a removed model — it clears as soon as one successful sync returns. Retrying in a few minutes is reasonable. |
| 502 | `upstream_error` | The model provider failed. You are not charged. |
| 504 | `upstream_timeout` | The provider did not respond in time. You are not charged. |
| 429 | `upstream_rate_limited` | Rate limited. Retry after a few seconds. |
| 404 | `not_found` | The job does not exist, or is not yours — both cases return the same response. |

### 429 and retry guidance

> ApiLux enforces rate limits on **requests per second** and **requests per minute**. Exceeding one returns `429 rate_limit_exceeded` with a `Retry-After` header. A blocked request never reaches the provider and is never charged.

- `api_key_limit_exceeded` — this key hit a limit **you configured**; the wallet still has funds. Carries `limit_type` and `period` — wait for the window, do not retry immediately.
- `rate_limit_exceeded` — you exceeded ApiLux's rate limit. Read `Retry-After`, wait that many seconds, then retry; `limit_type` says whether the per-second or per-minute axis blocked.
- `upstream_rate_limited` — the upstream provider is throttling. Retry after a few seconds with backoff.
- Do **not** retry other 4xx errors: they fail identically next time.
- 5xx and 504 may be retried with exponential backoff. But **never** resend `POST /videos/generations` once you have an `id` — poll that `id`; resending creates a second job and charges again.

## Not available yet

The capabilities below are **not** implemented on ApiLux. Deliberately no code examples — an example would invite code that cannot run.

- **Usage & balance by API key** — Available in the dashboard with an account login; there is no API-key endpoint yet.
