# ApiLux API

Tài liệu này **sinh tự động** từ contract đang chạy của ApiLux. Mọi endpoint dưới đây đều đã triển khai.

- **Base URL**: `https://apilux.net/api/v1`
- **Xác thực**: `Authorization: Bearer apl_YOUR_API_KEY`
- **Biến môi trường**: `APILUX_BASE_URL`, `APILUX_API_KEY`
- **Đơn vị tiền**: 1 credit = 1 VND

## Bảo mật API key

- Khoá hiện ra **đúng một lần** lúc tạo — ApiLux chỉ lưu bản băm, không lưu bản gốc.
- Chỉ dùng khoá ở **phía máy chủ**. Đặt vào mã frontend là công khai nó.
- Khoá bị lộ thì thu hồi ngay ở `/api-keys`; thu hồi có hiệu lực tức thì.
- Đặt hạn mức cho từng khoá để một khoá bị lạm dụng không tiêu hết ví.

## Bắt đầu nhanh

Thay `MODEL_ID` bằng một ID lấy từ `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"])
```

> **Timeout phía client**: chat vài giây là đủ; **tạo ảnh nên đặt 180–360 giây**; video trả về job ngay rồi poll.

## Danh sách endpoint

### GET /models

**Danh sách model** — Trả về các model bạn gọi được ngay lúc này. Model chưa cấu hình giá hoặc đang tắt sẽ không xuất hiện.

Tính tiền: Miễn phí, không trừ credit.

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `data[].id` | `string` | Public model ID dùng trong field `model` của các request khác. |
| `data[].kind` | `"TEXT" | "IMAGE" | "VIDEO"` | Loại model. |
| `data[].capabilities` | `string[]` | Các endpoint model này dùng được. |
| `data[].price_credit_per_unit` | `integer` | Giá bán, tính bằng credit (1 credit = 1 VND) trên một đơn vị. |
| `data[].billing_unit` | `"million_tokens" | "image" | "second"` | Đơn vị tính tiền tương ứng. |

<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** — Sinh văn bản, trả về theo hình dạng response quen thuộc của OpenAI.

Tính tiền: Theo tổng token (input + output), giá tính trên 1.000.000 token. Tối thiểu 1 credit mỗi request thành công.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID lấy từ `GET /models`. |
| `messages` | `Message[]` | có | Mảng `{ role, content }`, `role` ∈ `system` | `user` | `assistant`. |
| `stream` | `boolean` | — | `true` trả về luồng SSE tương thích OpenAI, chunk cuối mang `usage`. Mặc định `false`. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `choices[0].message.content` | `string` | Nội dung model sinh ra. |
| `usage` | `object` | `prompt_tokens`, `completion_tokens`, `total_tokens`. |
| `apilux.credit_charged` | `integer` | Số credit đã trừ cho request này. |
| `apilux.balance_after` | `integer` | Số dư còn lại sau khi trừ. |

<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>

> Response có thêm object `apilux` ngoài chuẩn OpenAI. Client OpenAI bỏ qua field lạ nên không ảnh hưởng gì.

### POST /chat

**Chat (contract gốc)** — Endpoint public đầu tiên của ApiLux, response gọn hơn. Được giữ nguyên vĩnh viễn cho các tích hợp cũ — dự án mới nên dùng `/chat/completions`.

Tính tiền: Giống `/chat/completions`.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID. |
| `messages` | `Message[]` | có | Mảng `{ role, content }`. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `reply` | `string` | Nội dung model sinh ra. |
| `tokensUsed` | `integer` | Tổng token đã dùng. |
| `creditCharged` | `integer` | Số credit đã trừ. |
| `balanceAfter` | `integer` | Số dư sau khi trừ. |

<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>

> Ở endpoint này, model đang bị tắt trả về `unknown_model` (400) thay vì `model_disabled` (503) — hành vi cũ được giữ nguyên có chủ đích.

### POST /messages

**Messages (tương thích Anthropic)** — Sinh văn bản theo giao thức Anthropic Messages, có `tools` và streaming. Đây là endpoint dành cho Claude Code và Anthropic SDK.

Tính tiền: Theo tổng token (input + output + token cache), giá tính trên 1.000.000 token. Tối thiểu 1 credit mỗi request thành công.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID lấy từ `GET /models`. |
| `max_tokens` | `integer` | có | Số token tối đa cho câu trả lời. Giao thức Anthropic bắt buộc field này. |
| `messages` | `Message[]` | có | Mảng `{ role, content }` với `role` ∈ `user` | `assistant`. `content` là chuỗi hoặc mảng khối nội dung (`text`, `image`, `tool_use`, `tool_result`). |
| `system` | `string | Block[]` | — | System prompt. Ở giao thức Anthropic đây là tham số riêng, không phải một phần tử trong `messages`. |
| `tools` | `Tool[]` | — | Danh sách công cụ model được phép gọi. Chuyển tiếp nguyên vẹn. |
| `stream` | `boolean` | — | `true` trả về luồng SSE theo đúng giao thức sự kiện Anthropic (`message_start` … `message_stop`). |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `content` | `Block[]` | Mảng khối nội dung model sinh ra. |
| `stop_reason` | `string` | Lý do dừng, ví dụ `end_turn` hoặc `tool_use`. |
| `usage` | `object` | `input_tokens`, `output_tokens`, và các field cache nếu có. |

<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>

> Dùng header `Authorization: Bearer apl_...` như mọi endpoint khác của ApiLux.
> `cache_control` được chuyển tiếp nguyên vẹn. Token cache hiện tính theo đơn giá thường vì ApiLux chưa có bảng giá cache riêng.

### POST /messages/count_tokens

**Đếm token** — Đếm số token đầu vào của một hội thoại trước khi gọi model.

Tính tiền: KHÔNG trừ credit. Vẫn tính vào hạn mức tốc độ của tài khoản.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID lấy từ `GET /models`. |
| `messages` | `Message[]` | có | Cùng hình dạng với `POST /messages`. |
| `system` | `string | Block[]` | — | System prompt, nếu có. |
| `tools` | `Tool[]` | — | Định nghĩa công cụ cũng chiếm token, nên gửi kèm để có con số đúng. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `input_tokens` | `integer` | Số token đầu vào. |

<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 không bao giờ tự ước lượng token. Con số này đến từ chính bộ đếm của model.

### POST /responses

**Responses** — Sinh văn bản theo giao thức OpenAI Responses, có `tools` và streaming. Đây là endpoint dành cho Codex.

Tính tiền: Theo tổng token (input + output), giá tính trên 1.000.000 token. Tối thiểu 1 credit mỗi request thành công.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID lấy từ `GET /models`. |
| `input` | `string | Item[]` | có | Chuỗi, hoặc mảng item hội thoại (`message`, `function_call`, `function_call_output`…). |
| `instructions` | `string` | — | System prompt của giao thức này. |
| `tools` | `Tool[]` | — | Danh sách công cụ model được phép gọi. Chuyển tiếp nguyên vẹn. |
| `max_output_tokens` | `integer` | — | Giới hạn token cho câu trả lời. |
| `stream` | `boolean` | — | `true` trả về luồng SSE theo giao thức sự kiện Responses (`response.created` … `response.completed`). |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `output` | `Item[]` | Mảng item model sinh ra. |
| `status` | `string` | `completed`, `incomplete` hoặc `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 cần đúng giao thức này. Trong `~/.codex/config.toml` đặt `wire_api = "responses"` rồi khởi động lại Codex/VS Code.

### POST /images/generations

**Tạo ảnh** — Tạo ảnh mới từ mô tả bằng chữ.

Tính tiền: Theo **số ảnh thực tế được tạo**, không theo `n` bạn yêu cầu — một số model chỉ trả 1 ảnh dù xin nhiều hơn.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID có `kind` là `IMAGE`. |
| `prompt` | `string` | có | Mô tả ảnh cần tạo. Tối đa 4000 ký tự. |
| `size` | `string` | — | Ví dụ `"1024x1024"`. Chỉ có tác dụng với model hỗ trợ. |
| `n` | `integer` | — | Số ảnh muốn tạo, 1–4. Mặc định 1. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `data[].b64_json` | `string` | Ảnh dạng base64. ApiLux luôn trả base64, không trả URL của nhà cung cấp. |
| `data[].mime_type` | `string` | Ví dụ `image/png`. |
| `usage.images` | `integer` | Số ảnh đã tính phí. |
| `apilux.credit_charged` | `integer` | Số credit đã trừ. |

<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>

> Tạo ảnh chậm hơn chat đáng kể. Đặt timeout client từ 180 đến 360 giây.

### POST /images/edits

**Sửa ảnh** — Sửa một ảnh có sẵn theo hướng dẫn bằng chữ: giữ nhân vật/bố cục gốc, đổi màu, làm mịn, thay chi tiết.

Tính tiền: Theo số ảnh trả về, giống endpoint tạo ảnh.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID có `kind` là `IMAGE`. |
| `prompt` | `string` | có | Hướng dẫn sửa. Càng cụ thể càng giữ đúng ảnh gốc. Tối đa 4000 ký tự. |
| `image.b64_json` | `string` | — | Ảnh nguồn dạng base64 thuần. Cần kèm `mime_type`. |
| `image.data_url` | `string` | — | Hoặc data URL `data:image/png;base64,...` — khi đó không cần `mime_type`. |
| `image.mime_type` | `"image/png" | "image/jpeg" | "image/webp"` | — | Bắt buộc khi dùng `b64_json`. Tối đa 8 MB sau khi decode. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `data[].b64_json` | `string` | Ảnh đã sửa, dạng base64. |
| `usage.images` | `integer` | Số ảnh đã tính phí. |

<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>

> Gửi ảnh dưới dạng JSON + base64, không dùng multipart. Giới hạn 8 MB mỗi ảnh, chỉ nhận PNG, JPEG, WEBP.

### POST /videos/generations

**Tạo video** — Tạo video từ prompt, từ một ảnh, hoặc đi từ frame đầu tới frame cuối. Trả về job ngay lập tức — video mất vài chục giây đến vài phút.

Tính tiền: Trừ **ngay khi tạo job**, theo `duration_seconds` bạn yêu cầu. Poll trạng thái không tốn thêm. Job `failed` được hoàn tiền tự động, đúng một lần.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `model` | `string` | có | Public model ID có `kind` là `VIDEO`. |
| `prompt` | `string` | có | Mô tả cảnh, chuyển động, phong cách. Tối đa 4000 ký tự. |
| `duration_seconds` | `4 | 6 | 8` | có | Chỉ nhận 4, 6 hoặc 8. Giá trị khác bị từ chối trước khi gọi nhà cung cấp. |
| `aspect_ratio` | `"16:9" | "9:16"` | — | Mặc định `16:9`. Dùng `9:16` cho video dọc. |
| `image` | `ImageInput` | — | Frame đầu, dùng cho image-to-video. Cùng định dạng với `image` của endpoint sửa ảnh. |
| `last_frame` | `ImageInput` | — | Frame cuối. Bắt buộc phải gửi kèm `image`. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `id` | `string` | Job ID của ApiLux (`vidjob_...`). Dùng để poll và tải video. |
| `status` | `"queued" | "running" | "completed" | "failed"` | Trạng thái tại thời điểm tạo, thường là `queued`. |
| `requested_duration_seconds` | `integer` | Số giây đã dùng để tính tiền. |
| `apilux.credit_charged` | `integer` | Số credit đã trừ khi tạo job. |

<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>

> Nếu request tạo job đã trả về `id`, đừng gửi lại request đó — hãy poll `id` đó. Gửi lại sẽ tạo job mới và bị tính tiền lần nữa.

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

**Trạng thái job video** — Kiểm tra tiến độ của một job video. Chỉ chủ sở hữu job mới xem được.

Tính tiền: Miễn phí. Poll bao nhiêu lần cũng không bị trừ thêm.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `jobId` | `string (path)` | có | Job ID nhận được khi tạo. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `status` | `"queued" | "running" | "completed" | "failed"` | Trạng thái hiện tại. |
| `content_url` | `string` | Chỉ có khi `completed`. Đường dẫn tải MP4 qua ApiLux. |
| `completed_at` | `integer (unix)` | Chỉ có khi `completed`. Thời điểm job hoàn thành. |
| `expires_at` | `integer (unix)` | Chỉ có khi `completed`. Hạn TRUY CẬP qua ApiLux (`completed_at` + 24h). Sau mốc này endpoint tải trả 404. ApiLux không lưu file — đây là cửa sổ ApiLux còn proxy được, không phải cam kết nhà cung cấp giữ kết quả đủ 24h. |
| `actual_duration_seconds` | `integer` | Số giây thực tế, nếu nhà cung cấp báo về. |
| `apilux.credit_refunded` | `integer` | Số credit đã hoàn (khác 0 khi 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>

> Nên poll mỗi 5 giây. Video thường xong sau 40–180 giây tuỳ model và độ dài.

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

**Tải video** — Tải file MP4 của một job đã hoàn tất. File được truyền qua ApiLux nên bạn chỉ cần API key của ApiLux.

Tính tiền: Miễn phí. Tiền đã trừ lúc tạo job.

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `jobId` | `string (path)` | có | Job ID đã ở trạng thái `completed`. |

| Trường response | Kiểu | Mô tả |
|---|---|---|
| `(binary)` | `video/mp4` | Nội dung file MP4. |

<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 không cam kết lưu trữ video vĩnh viễn. Tải file về ngay sau khi job hoàn tất.

## Mã lỗi

Lỗi luôn có hình dạng `{ "error": { "code", "message" } }`. `code` là chuỗi **ổn định** — phân nhánh theo nó, đừng phân nhánh theo `message`.

| HTTP | `code` | Ý nghĩa |
|---|---|---|
| 401 | `auth_missing_or_malformed` | Thiếu header `Authorization` hoặc sai định dạng. Phải là `Bearer apl_...`. |
| 401 | `auth_invalid` | API key không tồn tại. |
| 401 | `auth_revoked` | API key đã bị thu hồi. |
| 401 | `auth_expired` | API key đã hết hạn. |
| 503 | `model_catalog_disabled` | Danh mục model công khai đang tạm ẩn. Các model ID bạn đã biết vẫn gọi được — chỉ endpoint liệt kê này tạm dừng. |
| 400 | `invalid_request` | Body không hợp lệ (thiếu field, sai kiểu, giá trị ngoài phạm vi). |
| 400 | `unknown_model` | Model không tồn tại trong ApiLux. |
| 400 | `capability_not_supported` | Model có tồn tại nhưng không dùng được ở endpoint này (ví dụ dùng model text cho endpoint ảnh). |
| 503 | `pricing_not_configured` | Model đã có route nhưng chưa được cấu hình giá trong ApiLux. Đây là trạng thái phía chúng tôi, không phải lỗi của bạn. |
| 503 | `model_disabled` | Model đang bị tắt. |
| 503 | `model_temporarily_unavailable` | Model đang bật và có giá, nhưng nhà cung cấp phía sau hiện không phục vụ nó. Đây là trạng thái tạm thời và có thể tự hết — model sẽ xuất hiện lại trong `GET /v1/models` khi nhà cung cấp có nó trở lại. Bạn không bị trừ tiền và không có lời gọi nào tới nhà cung cấp. |
| 402 | `insufficient_balance` | Số dư ví không đủ để thực hiện request. |
| 409 | `duplicate_request` | Đã có một request TẠO với cùng `Idempotency-Key` đang chạy hoặc đã hoàn tất. ApiLux KHÔNG gọi nhà cung cấp lần hai và KHÔNG trừ tiền lần hai. Nếu request gốc đã xong, response kèm `job_id` để bạn lấy kết quả ở đó. Chỉ áp cho các endpoint tạo media (`images/generations`, `images/edits`, `videos/generations`). |
| 429 | `api_key_limit_exceeded` | API key này đã chạm hạn mức bạn tự cấu hình. Ví vẫn còn tiền — chỉ key này không được tiêu thêm cho tới khi chu kỳ reset. Response kèm `limit_type` (`spending` | `requests` | `tokens`) và `period` (`daily` | `monthly` | `lifetime`). Hạn mức token của endpoint text được tính theo usage thực tế nên tổng có thể vượt tối đa một request. |
| 503 | `api_key_quota_reconciliation_required` | Một request text trước đó của chính key này chưa được đối soát xong. Thường gặp nhất: nhà cung cấp trả kết quả thành công nhưng **không kèm usage token**, nên hạn mức token của kỳ đó không còn đáng tin và ApiLux dừng lại thay vì để hạn mức bị vượt trong im lặng. Thử lại sẽ không qua được cho tới khi việc đó xong — hãy liên hệ hỗ trợ. Bạn không bị trừ tiền cho request bị chặn, không có lời gọi nào tới nhà cung cấp, và endpoint ảnh/video không bị ảnh hưởng. |
| 429 | `rate_limit_exceeded` | Vượt hạn mức tốc độ (request mỗi giây hoặc mỗi phút). Kèm header `Retry-After` và trường `retry_after_seconds`; `limit_type` cho biết trục nào chặn. Request bị chặn KHÔNG gọi nhà cung cấp và KHÔNG bị trừ tiền. Chờ đúng số giây rồi thử lại. |
| 503 | `system_maintenance` | ApiLux đang bảo trì. Toàn bộ endpoint sinh nội dung tạm dừng; bạn không bị trừ tiền và không có lời gọi nào tới nhà cung cấp. Thử lại sau. |
| 503 | `ai_requests_disabled` | Người vận hành đã tạm ngừng nhận request AI, trong khi phần còn lại của dịch vụ vẫn chạy. Tách khỏi `system_maintenance` vì đây là công tắc hẹp hơn. |
| 503 | `provider_catalog_stale` | ApiLux không còn bằng chứng đủ mới về việc nhà cung cấp còn phục vụ model nào, nên dừng lại thay vì tính tiền một request có thể hỏng. Đây là sự cố phía chúng tôi, **không phải** model bị gỡ — nó tự hết ngay khi một lần đồng bộ thành công quay lại. Thử lại sau vài phút là hợp lý. |
| 502 | `upstream_error` | Nhà cung cấp mô hình lỗi. Không bị trừ tiền. |
| 504 | `upstream_timeout` | Nhà cung cấp không phản hồi kịp. Không bị trừ tiền. |
| 429 | `upstream_rate_limited` | Bị giới hạn tốc độ. Thử lại sau vài giây. |
| 404 | `not_found` | Job không tồn tại, hoặc không thuộc về bạn — hai trường hợp trả về giống hệt nhau. |

### 429 và cách thử lại

> ApiLux áp hạn mức tốc độ theo **request/giây** và **request/phút**. Vượt hạn mức trả `429 rate_limit_exceeded` kèm header `Retry-After`. Request bị chặn **không** gọi nhà cung cấp và **không** bị trừ tiền.

- `api_key_limit_exceeded` — khoá này chạm hạn mức **bạn tự cấu hình**; ví vẫn còn tiền. Kèm `limit_type` và `period` — chờ hết chu kỳ, đừng thử lại ngay.
- `rate_limit_exceeded` — vượt hạn mức tốc độ của ApiLux. Đọc `Retry-After`, chờ đúng ngần đó giây rồi thử lại; `limit_type` cho biết bị chặn theo giây hay theo phút.
- `upstream_rate_limited` — nhà cung cấp phía sau đang giới hạn. Thử lại sau vài giây với backoff.
- **Đừng** thử lại các lỗi 4xx khác: chúng sẽ hỏng y như vậy ở lần sau.
- 5xx và 504 có thể thử lại với backoff luỹ tiến. Nhưng **không bao giờ** gửi lại `POST /videos/generations` nếu đã nhận `id` — hãy poll `id` đó; gửi lại là tạo job mới và bị tính tiền lần nữa.

## Chưa khả dụng

Những năng lực dưới đây **chưa** được ApiLux triển khai. Cố ý không kèm ví dụ code — có ví dụ là mời bạn viết code không chạy được.

- **Usage & Balance theo API key** — Hiện chỉ xem được trong dashboard bằng tài khoản, chưa có endpoint gọi bằng API key.
