# Liệt kê video mở

Trả về một trang các video mở được cấp cho tổ chức của bạn, mỗi video kèm link MP4 đã ký có hiệu lực 1 giờ. Hãy duyệt hết mọi trang và cache kết quả trên server của bạn. Xem [Video mở](https://docs.wandura.ai/vi/v1/developers/concepts/open-video) và [Đồng bộ video mở](https://docs.wandura.ai/vi/v1/developers/guides/open-video-sync).

```
GET /api/delivery/v1/videos
```

**Scope:** `videos:read`, trên một key gắn với một app

## Tham số request
| Tên | Vị trí | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|---|
| `Authorization` | header | string | Có | `Bearer <API key>` của một key có scope `videos:read`. |
| `collection` | query | string | Không | Chỉ lấy một bộ sưu tập, theo mã của bộ. |
| `cursor` | query | string | Không | Token opaque lấy từ `next_cursor` trước đó. Đừng tự dựng hay phân tích nó. |
| `limit` | query | integer | Không | Số dòng mỗi trang, từ 1 đến 200. Mặc định `50`. |
| `Wandura-Version` | header | string | Không | Tuỳ chọn. Một phiên bản API theo ngày (YYYY-MM-DD) dùng cho request này thay cho bản ghim vào API key của bạn. Giá trị lạ hoặc đã ngừng trả 400 với `error_code` `unsupported_version`. |

## Ví dụ request
```bash
curl -s "https://www.wandura.ai/api/delivery/v1/videos?limit=50" \
  -H "Authorization: Bearer $WANDURA_API_KEY"
```

## Response
`200 OK`. Trường mới có thể được thêm bất cứ lúc nào: hãy bỏ qua những trường bạn không nhận ra.

| Trường | Kiểu | Mô tả |
|---|---|---|
| `rows` | array | Các video trên trang này, theo thứ tự của bộ sưu tập. |
| `next_cursor` | string hoặc null | Truyền làm `cursor` để lấy trang kế. `null` nghĩa là bạn đã có đủ. |
| `expires_at` | string (ISO 8601, UTC) | Thời điểm mọi link trên trang này ngừng chạy (1 giờ sau response). |
| `refresh_before` | string (ISO 8601, UTC) | Gọi lại danh sách trước thời điểm này (45 phút sau response). |
| `links` | object | URL tuyệt đối dựng sẵn — đi theo `links.next` thay vì tự ráp URL trang kế. |
| `links.self` | string | Chính request này. |
| `links.next` | string hoặc null | Trang sau, hoặc `null` ở trang cuối. |

### Các trường của một video (`rows[]`)
| Trường | Kiểu | Mô tả |
|---|---|---|
| `id` | string (UUID) | Video ID. Không bao giờ đổi; đồng bộ theo trường này. |
| `slug` | string | Định danh video an toàn cho URL. |
| `title` | string | Tên video. |
| `order_code` | string hoặc null | Mã đơn sản xuất (ví dụ `ABS_01_E01`), khớp từng ký tự với file đặt hàng của bạn, hoặc `null` với video không có trong đó. Dùng nó để ghép một lần với file đặt hàng. |
| `collection` | string | Mã bộ sưu tập của dòng này. Video thuộc hai bộ được cấp sẽ xuất hiện một lần dưới mỗi bộ. |
| `position` | integer | Vị trí của video trong bộ sưu tập. Các dòng trả về theo thứ tự của bộ. |
| `duration_ms` | integer hoặc null | Thời lượng tính bằng mili giây. |
| `muscle_groups` | mảng string | Nhóm cơ được tập. Có thể rỗng. |
| `equipment` | mảng string | Dụng cụ cần có. Có thể rỗng. |
| `difficulty` | string hoặc null | Nhãn độ khó. |
| `kcal_est` | integer hoặc null | Lượng calo ước tính tiêu hao. |
| `mp4` | object | Link MP4 đã ký, dùng được tới `expires_at`. Phát nguyên như nhận được; đừng lưu chúng trong app. |
| `mp4.p480` | string | Link MP4 480p. Mặc định phát bản này. |
| `mp4.p720` | string hoặc null | Link MP4 720p, hoặc `null` cho tới khi có bản này. |

Dừng duyệt khi `next_cursor` là `null`, không bao giờ dừng vì một trang ngắn hay rỗng: các video chưa sẵn sàng phát bị bỏ qua.

## Ví dụ response
```json
{
  "rows": [
    {
      "id": "3f6c1a52-8d0e-4b7a-9c21-5e4f2a7b9d10",
      "slug": "core-abs-01",
      "title": "Core abs 01",
      "order_code": "ABS_01_E01",
      "collection": "ABS_01",
      "position": 1,
      "duration_ms": 42000,
      "muscle_groups": ["abs"],
      "equipment": [],
      "difficulty": "beginner",
      "kcal_est": 12,
      "mp4": { "p480": "https://cdn.example.com/v/core-abs-01-480.mp4?sign=example", "p720": null }
    }
  ],
  "next_cursor": null,
  "expires_at": "2026-10-01T13:00:00.000Z",
  "refresh_before": "2026-10-01T12:45:00.000Z"
}
```

## Header của response
| Header | Response | Mô tả |
|---|---|---|
| `Wandura-API-Status` | mọi response | Trạng thái API. Hiện là `beta`, ở mọi response, thành công hay lỗi. |
| `Wandura-Version` | mọi response | Phiên bản API theo ngày (YYYY-MM-DD) mà response này dùng: bản ghim vào API key của bạn, hoặc bản bạn gửi trong header request `Wandura-Version`. Key tạo trước khi có phiên bản ngày dùng `2026-10-02`. |
| `Wandura-Request-Id` | mọi response | Mã định danh mờ của request này (`req_` và 26 ký tự). Mọi response đều có, kể cả lỗi, và body lỗi lặp lại nó ở `request_id`. Hãy ghi log nó, và gửi kèm khi liên hệ Wandura về một request: nó tìm đúng request đó trong log của chúng tôi. Đừng phân tích chuỗi này. |
| `Cache-Control` | mọi response | Luôn là `no-store`. Đừng lưu các response này trong cache dùng chung. |
| `Deprecation` | 200 | Chỉ có khi endpoint này, hoặc phiên bản ngày đang phục vụ response, đã bị ngừng hỗ trợ (RFC 9745): `@<giây unix>` của ngày ngừng. Hãy ghi nó vào hệ giám sát. |
| `Sunset` | 200 | Chỉ có khi endpoint hay phiên bản ngày đã bị ngừng có ngày thôi chạy (RFC 8594), dạng ngày HTTP. |
| `Link` | 200 | Có kèm `Deprecation`: `<https://docs.wandura.ai/…>; rel="deprecation"` (và `rel="sunset"`) trỏ tới trang giải thích dùng gì thay thế. |
| `Retry-After` | 429, 503 | Số giây cần chờ trước khi thử lại. Có ở response 429 và các response 503 nên thử lại. |
| `X-RateLimit-Limit` | 429 | Số request (với playback là số vé) được phép trong một cửa sổ cho nhóm endpoint này và tổ chức của bạn. |
| `X-RateLimit-Window` | 429 | Độ dài cửa sổ tính bằng giây. |

## Lỗi
| Status | `error_code` | Khi nào |
|---|---|---|
| 400 | không có | `cursor` không phải token do endpoint này cấp, hoặc `limit` không phải số nguyên từ 1 đến 200. |
| 400 | `unsupported_version` | Header `Wandura-Version`, hoặc bản ghim của key, là bản lạ hay đã ngừng. Body có thêm `supported_versions` và `latest_version` (và `upgrade_to` khi bản ghim đã ngừng). |
| 401 | không có | Thiếu header `Authorization: Bearer`, hoặc key không tồn tại hay đã bị thu hồi. |
| 403 | không có | Key thiếu scope `videos:read`. |
| 403 | không có (`error` là `app_not_registered`) | Key chưa gắn với app nào. Đăng ký app ở trang Developers rồi dùng key của app đó. |
| 403 | không có (`error` là `org_suspended`) | Quyền truy cập của tổ chức bạn đang bị tạm ngưng. |
| 404 | không có (`error` là `collection_not_found`) | Bộ sưu tập không tồn tại, chưa publish, hoặc chưa cấp cho tổ chức của bạn. |
| 429 | `rate_limited` | Đã chạm rate limit của endpoint này. |
| 500 | không có | Không đọc được danh sách video. Retry với backoff. |
| 503 | `service_unavailable` | Wandura chưa kiểm được API key kịp thời. Key không sai: thử lại sau số giây trong `Retry-After`, có backoff. |

Cách xử lý từng lỗi: [Lỗi](https://docs.wandura.ai/vi/v1/developers/concepts/errors).
