# Hành vi chuẩn của API

> **TL;DR** — Trang này là hợp đồng. Mọi con số và quy tắc mà nhiều trang cùng dựa vào đều nằm ở đây, đúng một lần: xác thực, phân trang, thời hạn và cách tái sử dụng vé, độ mới của từng câu trả lời, chính sách retry, lỗi, rate limit và phiên bản. Các trang khác giải thích và link về đây; nếu hai trang nói khác nhau, trang này thắng.

## Xác thực
| Quy tắc | Giá trị |
|---|---|
| Header | `Authorization: Bearer <API key>` ở mọi request. Không cookie, không đặt key trong query string. |
| Định dạng key | `wnd_live_` theo sau là 32 ký tự. |
| Đại diện cho ai | Toàn bộ tổ chức sở hữu key. API không có danh tính theo từng người xem. |
| Được nằm ở đâu | Chỉ trên server của bạn. API không gửi header CORS. |
| Giao thức truyền | `https://www.wandura.ai`, TLS 1.2 trở lên. |
| Thứ tự kiểm tra | Header `Wandura-Version` → có key → key hợp lệ → phiên bản đã ghim → scope → tổ chức → rate limit → body của request. |
| Scope của từng endpoint | Một bảng duy nhất, ở [Xác thực](https://docs.wandura.ai/vi/developers/authentication#scopes). |

Header `Wandura-Version` sai bị trả `400` trước cả khi key được xem tới. Key mà Wandura không kiểm tra kịp sẽ trả `503` `service_unavailable`: key vẫn ổn, hãy retry. Thời gian tạo, xoay vòng và thu hồi key nằm ở [Bảo mật](https://docs.wandura.ai/vi/developers/concepts/security#key-lifecycle).

## Phân trang
Ba endpoint trả về theo trang. Cả ba theo cùng quy tắc: cursor là opaque (chép nguyên, không bao giờ tự dựng hay giải mã), và bạn dừng khi cursor kế tiếp là `null`, không bao giờ dừng vì một trang ngắn hay rỗng.

| Endpoint | Kích thước trang | Trang kế | Đơn vị của một trang |
|---|---|---|---|
| `GET /catalog` | Tuỳ chọn: `limit` 1–200. Không có `limit` và không có `cursor` = toàn bộ catalog trong một response. Có `cursor` mà không có `limit` = 50. | `next_cursor` (chỉ có khi phân trang) và `links.next` | Phim; mỗi trang mang **tất cả** các tập của các phim trong trang, kể cả placeholder. |
| `GET /creatives` | `limit` 1–100, mặc định 50 | `data.next_cursor`, `links.next` | Bản ghi creative, bản cập nhật cũ nhất trước. |
| `GET /videos` | `limit` 1–200, mặc định 50 | `next_cursor`, `links.next` | Video mở. Một trang có thể ngắn hoặc rỗng trong khi vẫn còn trang sau. |

### Các trang catalog không phải một ảnh chụp duy nhất
Mỗi trang catalog được cắt ra từ catalog đúng ở thời điểm trang đó được trả. Nếu Wandura publish hoặc rút một phim, hoặc một giấy phép bắt đầu hay kết thúc, trong lúc bạn duyệt các trang, một phim có thể **bị bỏ sót hoặc xuất hiện hai lần** giữa các trang. Đồng bộ an toàn như sau:

1. Nếu catalog của bạn chỉ vài trăm phim trở xuống, hãy gọi `GET /catalog` **không có** `limit`: một response là một ảnh chụp nhất quán.
2. Nếu phân trang, hãy loại trùng theo `id` của phim (bản sau thắng).
3. Chỉ áp dụng việc rút phim sau một lượt duyệt **trọn vẹn**: mọi trang đều trả `200`. Lượt duyệt hỏng giữa chừng không thay đổi gì.
4. Chỉ đánh dấu một phim không khả dụng khi nó vắng mặt trong **hai lượt duyệt trọn vẹn liên tiếp**. Phim bị bỏ sót một lần do catalog đang thay đổi sẽ quay lại ở lượt sau.

[Hướng dẫn đồng bộ catalog](https://docs.wandura.ai/vi/developers/guides/catalog-sync) cài đặt đúng như vậy.

Feed creative không có khoảng hở này với các bản ghi không đổi: nó được sắp theo thời điểm cập nhật rồi theo ID, nên các bản cập nhật mới rơi vào sau những trang bạn đã đọc. Bản ghi được cập nhật trong lúc bạn duyệt sẽ xuất hiện lại ở lần đồng bộ sau.

## Vé playback
### Thời hạn
| Bạn nhận được gì | Sống trong | Hạn chót chính xác |
|---|---|---|
| `play_auth_token` (tập đã mã hoá, Player SDK) | **1.800 giây** (30 phút) | `expires_at` |
| `manifest_url` (chỉ nội dung giao không mã hoá, theo thoả thuận) | **900 giây** (15 phút) | `expires_at` |

Vé không bao giờ được gia hạn, và một khi đã cấp thì không thu hồi được: nó phát đến `expires_at` kể cả khi giấy phép kết thúc hay key bị thu hồi một giây sau đó. `GET /config` cho biết cả hai thời hạn với key của bạn (`playback.grant_ttl_seconds`, `playback.manifest_url_ttl_seconds`).

### Mỗi vé một thiết bị
Vé gắn với các giá trị thiết bị mà nó được cấp cho (`union_info` và `drm_kek`). Nó chỉ giải mã được trên thiết bị đó. Không bao giờ đưa vé sang thiết bị khác, và không bao giờ cache vé dưới một khoá không chứa thiết bị.

### Tái sử dụng
Tái sử dụng vé là được phép và nên làm, theo các quy tắc sau:

| Quy tắc |  |
|---|---|
| Khoá để tái sử dụng | người xem + **thiết bị** + tập + loại intent (`play` hoặc `prefetch`) |
| Bao lâu | Cho đến **5 phút** trước `expires_at`. Sau đó, xin vé mới. |
| Phía Wandura | Khi bạn gửi `viewer_ref`, một request lặp lại có cùng `viewer_ref`, cùng giá trị thiết bị, cùng tập (xác định **theo cùng một cách**: `episode_id`, hoặc `film_ref` + `episode_no`) và cùng loại intent, trong lúc vé trước còn ít nhất 5 phút, sẽ trả về **chính vé đó** (cùng token, cùng `expires_at`). Nó không bị tính vào rate limit và không phải một lượt phát mới. Wandura kiểm tra lại giấy phép của bạn trước khi trả vé. |
| Phía bạn | Bạn cũng có thể giữ vé trên backend theo cùng khoá đó, và phát lại cho người xem. |
| Cố gắng tối đa | Tái sử dụng phía server là một tối ưu, không phải lời hứa: request lặp lại vẫn có thể trả vé mới. Không bao giờ dựa vào việc nhận lại đúng token cũ. |
| Không có `viewer_ref` | Mỗi request tạo một vé mới. |

**Lượt xem được đếm từ các request `play`.** Khi người xem bắt đầu một tập, luôn gửi một request có `"intent": "play"` cho tập đó, kể cả khi app khởi động player bằng một vé đã prefetch trước. Vé prefetch không phải lượt xem.

### Batch
Một request có thể xin tối đa **3** vé: `episodes` (1–3 mục) hoặc một tập neo kèm `next` (0–2). Mỗi vé tính **1** vào giới hạn playback. Mỗi mục có `status` riêng; các mục được nhận lần lượt từng cái, mục prefetch sau cùng. Cả request chỉ bị `429` khi không mục nào được cấp. Chi tiết: [Xin playback](https://docs.wandura.ai/vi/developers/reference/playback).

## Độ mới: một câu trả lời có thể cũ đến đâu
Wandura phục vụ từ các cache ngắn hạn, nên một thay đổi phía Wandura đến tay bạn sau một độ trễ có giới hạn. Đây là các giới hạn trên.

| Thay đổi phía Wandura | Thấy được trong | Thấy được trong request xin vé |
|---|---|---|
| Phim hoặc tập được publish hay bị rút, `stream_ready` thay đổi | Catalog: ≤ 30 giây | ≤ 60 giây |
| Giấy phép được cấp, tạm ngưng, tắt hoặc ra ngoài khung thời gian | Catalog: ≤ 30 giây | ≤ 30 giây |
| Tổ chức bị tạm ngưng | khoảng 1 giây (luôn ≤ 30 giây) | khoảng 1 giây (luôn ≤ 30 giây) |
| Key bị thu hồi hoặc xoay vòng | khoảng 1 giây (luôn ≤ 30 giây) | khoảng 1 giây (luôn ≤ 30 giây) |
| Thứ vừa được tạo (một key, một tập, một giấy phép) | ≤ 5 giây | ≤ 5 giây |
| Phụ đề được gắn hoặc thay | — | ≤ 5 phút |
| Rate limit được nâng cho tổ chức của bạn | khoảng 1 phút | khoảng 1 phút |
| Ngôn ngữ phụ đề trong `GET /config` | ≤ 5 phút | — |

- **Nhiều server.** Mỗi server của Wandura giữ cache riêng, nên hai lệnh gọi cách nhau vài giây có thể trả về ảnh chụp hơi khác nhau. Điều đó là bình thường.
- **Khi cơ sở dữ liệu của Wandura gặp sự cố**, việc kiểm tra catalog và giấy phép được phục vụ từ câu trả lời tốt gần nhất trong tối đa **5 phút** (stale-if-error), thay vì báo lỗi. Một thay đổi Wandura đã thấy (rút phim, tạm ngưng) không bao giờ bị hoàn tác vì cơ chế này.
- **Vé đã cấp** không bao giờ bị thu hồi: chúng phát đến `expires_at` (≤ 30 phút).

Nên cache gì ở phía bạn, và trong bao lâu: [Caching](https://docs.wandura.ai/vi/developers/guides/caching).

## Retry
Chính sách retry duy nhất cho mọi lệnh gọi tới Wandura. Các [ví dụ](https://docs.wandura.ai/vi/developers/guides/catalog-sync#code) cài đặt đúng chính sách này.

| Câu trả lời | Retry? | Cách làm |
|---|---|---|
| Lỗi mạng, timeout | Có | Backoff như bên dưới |
| `429` (mọi endpoint, mọi `reason`) | Có | Sau `Retry-After` |
| `503` `service_unavailable`, creatives `5030` / `5031` | Có | Sau `Retry-After` nếu có, nếu không thì backoff |
| `500`, `502` | Có | Backoff |
| `503` `not_ready` | **Không phải lúc này** | Tập hiện chưa có bản phát được. Hiện "chưa khả dụng"; thử lại ở lần phát sau hoặc lần đồng bộ catalog sau. |
| `400`, `401`, `403`, `404`, `405`, `409`, `422` | **Không** | Cùng request đó sẽ lại lỗi: hãy sửa request, key hoặc giấy phép. |
| Một `4xx` bạn không nhận ra | Không |  |
| Một `5xx` bạn không nhận ra | Có | Như `503` |

**Backoff.** Khi có `Retry-After`, chờ đúng số giây đó cộng thêm ngẫu nhiên 0–1 giây. Nếu không, chờ một khoảng ngẫu nhiên từ 0 đến `min(cap, base × 2^attempt)` (full jitter).

|  | Tương tác (người xem đang chờ) | Nền (job đồng bộ) |
|---|---|---|
| Base / cap | 0,5 giây / 8 giây | 1 giây / 30 giây |
| Timeout mỗi lần thử | 8 giây (không bao giờ dưới 5 giây: Wandura có thể giữ một request `play` tới 0,5 giây) | 15 giây |
| Số lần retry sau lần thử đầu | tối đa 3 | tối đa 5 |
| Tổng thời gian | 15 giây, rồi hiện **Retry** | 2 phút, rồi bỏ cuộc và giữ dữ liệu cũ |

**Ngân sách retry.** Trong một tiến trình, số retry tối đa là **10 %** số request của phút vừa qua (không bao giờ dưới 10 mỗi phút). Khi hết ngân sách, hãy báo lỗi ngay thay vì dồn thêm tải.

**Đồng thời.** Duyệt từng trang một, không bao giờ song song. Chỉ retry các mục lỗi của một batch, mỗi mục sau `retry_after` của riêng nó. Request xin vé được retry với cùng `viewer_ref` và giá trị thiết bị thường trả về cùng vé, nên không bị tính hai lần.

## Lỗi
- Hai định dạng: object phẳng `{ "error", "error_code" }` ở catalog, playback, config và videos; envelope `{ "code", "message", "server_time" }` (không có `data`) ở lỗi của creatives. `429` và `400` `unsupported_version` của creatives dùng định dạng phẳng.
- Rẽ nhánh theo `error_code` (creatives: `code`). Mã lạ: quyết định theo nhóm status, như trong bảng retry ở trên.
- `Retry-After` đi kèm `429` và các `503` nên retry.
- Mọi response đều mang header `Wandura-Request-Id`, và mọi body lỗi JSON đều có trường `request_id`. Hãy dẫn nó khi liên hệ hỗ trợ.

Mọi status và mã lỗi, theo từng endpoint, kèm cách xử lý: [Lỗi](https://docs.wandura.ai/vi/developers/concepts/errors).

## Rate limit
Giới hạn tính theo **tổ chức** (không theo key), theo từng bộ đếm, với đơn vị như trong bảng.

| Bộ đếm | Endpoint | Giới hạn mỗi tổ chức | Đơn vị | Cách nạp lại |
|---|---|---|---|---|
| `playback` | `POST /playback` | 10.800 mỗi 60 giây | **vé**: một batch 3 vé = 3; body sai định dạng = 1; vé tái sử dụng = 0; vé bị từ chối = 0 | Liên tục: 180 mỗi giây, tối đa 10.800 cùng lúc khi đang rảnh |
| `catalog` | `GET /catalog` | 3.000 mỗi 60 giây | request | Liên tục: 50 mỗi giây |
| `creatives` | `GET /creatives`, `GET /creatives/{id}` (dùng chung) | 3.000 mỗi 60 giây | request | Theo phút đồng hồ (UTC) |
| `videos` | `GET /videos` | 3.000 mỗi 60 giây | request | Theo phút đồng hồ (UTC) |
| `video` | `GET /videos/{id}` | 10.800 mỗi 60 giây | request | Theo phút đồng hồ (UTC) |
| `config` | `GET /config` | 600 mỗi 60 giây | request | Theo phút đồng hồ (UTC) |

- Chỉ request có key hợp lệ mới bị đếm.
- Việc đếm là xấp xỉ, có thể cao hơn thực tế tới khoảng 2,5 %.
- Khi gần chạm giới hạn playback, vé `prefetch` bị từ chối trước (`429`, `"reason": "prefetch_shed"`, khi còn dưới khoảng 20 % hạn mức), và một request `play` có thể bị giữ tới 0,5 giây thay vì bị từ chối.
- `GET /config` báo giới hạn hiện tại của bạn cho mọi bộ đếm trừ `video`.
- Wandura có thể nâng bất kỳ giới hạn nào cho tổ chức của bạn khi bạn yêu cầu.

Hành vi khi tải cao, và cách xử lý: [Rate limit](https://docs.wandura.ai/vi/developers/concepts/rate-limits).

## Phiên bản
| Quy tắc |  |
|---|---|
| Phiên bản lớn | Nằm trong path: `/v1`. |
| Phiên bản theo ngày | Mỗi key được ghim vào phiên bản theo ngày hiện hành lúc key được tạo (hôm nay chỉ có một: `2026-10-02`). Mọi response cho biết đã dùng phiên bản nào trong `Wandura-Version`. Gửi `Wandura-Version: <date>` để thử một phiên bản khác cho một request. |
| Xoay vòng | Giữ nguyên phiên bản đã ghim của key. Key **mới** được ghim vào phiên bản mới nhất. |
| Bên trong `/v1` | Chỉ có thay đổi theo kiểu thêm vào: endpoint mới, tham số tuỳ chọn, trường, giá trị và mã lỗi mới. Hãy bỏ qua những gì bạn không nhận ra. |
| Thay đổi phá vỡ tương thích | Một phiên bản theo ngày hoặc phiên bản lớn mới. Phiên bản lớn cũ tiếp tục chạy **12 tháng** sau khi phiên bản mới được công bố; trong giai đoạn beta, thay đổi phá vỡ tương thích bên trong `/v1` được báo trước **30 ngày** (trừ bản vá bảo mật khẩn cấp). |
| Header trạng thái | `Wandura-API-Status: beta` ở mọi response. |

Chính sách và các header báo ngừng hỗ trợ: [Phiên bản và tiến hoá API](https://docs.wandura.ai/vi/developers/concepts/versioning).

## Header về caching
Mọi response của Delivery API, kể cả vé, đều mang `Cache-Control: no-store` và không đặt cookie nào. Không bao giờ lưu chúng trong cache dùng chung (CDN hay proxy): vé, link đã ký và catalog của bạn chỉ thuộc về tổ chức của bạn.

## Liên quan
- [Tổng quan tài liệu tham chiếu API](https://docs.wandura.ai/vi/developers/reference/overview)
- [Lỗi](https://docs.wandura.ai/vi/developers/concepts/errors)
- [Bảo mật](https://docs.wandura.ai/vi/developers/concepts/security)
