---
title: "List VODs"
description: "Paginated list of on-demand movies and series. Supports keyset pagination, category filtering and case-insensitive name substring search."
---

# List VODs

Returns a paginated list of on-demand entries (movies and series episodes). Filterable by category and by a case-insensitive substring of the entry name. Rows with `hide_vod = 1` (hidden by the panel operator) are excluded automatically.

Both admin and reseller keys see the full catalog. Series and movies share the same VOD id space in Xtream AI, so a single response can mix rows with `is_serie: true` (episodes of series) and `is_serie: false` (movies). Use the `is_serie` flag to route the row to the right UI treatment.

Metadata such as `year` and `rating` is enriched from the panel's local TMDB cache. The catalog exposes a curated subset of the underlying `movie_propeties` blob; if you need richer metadata (cast, plot, runtime, backdrops) query TMDB directly on your side using the entry name and year.

## Endpoint

`GET https://<your-panel-domain>/panel-api/v1/vods`

## Authentication

Bearer token in the `Authorization` header. See [Authentication](/docs/?page=panel-api-authentication).

## Required scope

`vods:read`.

## Query parameters

| Name | Type | Required | Default | Description |
| ---- | ---- | -------- | ------- | ----------- |
| `limit` | int | no | `50` | Page size. Minimum `1`, maximum `100`. Values above `100` are silently clamped. |
| `cursor` | int | no | `0` | Numeric id of the last VOD you saw. The server returns entries with `id > cursor`. Omit for the first page. |
| `q` | string | no | (none) | Case-insensitive substring match on the entry name. |
| `category_id` | int | no | `0` | Restrict the response to entries that belong to this category (categories of `type='movie'`). Ignored when `0`. |

Pagination is keyset. Feed the previous response's `next_cursor` back in as `cursor` for the next page. When the last page is reached, `next_cursor` is `null`. Offset or page number parameters (`?page=2`, `?offset=100`) are ignored; use `cursor`.

## Response

The response wraps the page in `items` and adds `next_cursor`.

| Field | Type | Description |
| ----- | ---- | ----------- |
| `id` | int | Numeric VOD id. |
| `name` | string | Entry display name. For series episodes this typically includes the season and episode markers baked in by the operator. |
| `icon` | string | Poster URL (`cover_big` from TMDB). Empty string when the entry has no poster. |
| `year` | int or null | Release year, derived from the release date in the TMDB cache. `null` when the panel has no release date for the entry. |
| `rating` | float or null | TMDB rating on a 0.0 to 10.0 scale. `null` when not available. |
| `is_serie` | bool | `true` for series episodes, `false` for movies. |
| `categories` | array | List of `{id, name}` objects. An entry can belong to more than one category. |
| `next_cursor` | int or null | Feed back as `cursor` to fetch the next page. `null` on the last page. |

Example first page (mostly series episodes).

```json
{
  "items": [
    {
      "id": 83287,
      "name": "Series 42 - S08E01 - Starling City",
      "icon": "",
      "year": 2019,
      "rating": 7.1,
      "is_serie": true,
      "categories": []
    },
    {
      "id": 83288,
      "name": "Series 42 - S08E02 - Welcome to Hong Kong",
      "icon": "",
      "year": 2019,
      "rating": 7.1,
      "is_serie": true,
      "categories": []
    },
    {
      "id": 83289,
      "name": "Series 42 - S08E03 - Leap of Faith",
      "icon": "",
      "year": 2019,
      "rating": 8.6,
      "is_serie": true,
      "categories": []
    },
    {
      "id": 83290,
      "name": "Series 42 - S08E04 - Present Tense",
      "icon": "",
      "year": 2019,
      "rating": 8.2,
      "is_serie": true,
      "categories": []
    },
    {
      "id": 83291,
      "name": "Series 43 - S06E01 - Into the Void",
      "icon": "",
      "year": 2019,
      "rating": 5.7,
      "is_serie": true,
      "categories": []
    }
  ],
  "next_cursor": 83291
}
```

Filtering by `q=matrix` returns movies whose name matches.

```json
{
  "items": [
    {
      "id": 109898,
      "name": "Movie 42 (1999)",
      "icon": "https://cdn.example.com/posters/movie-42.jpg",
      "year": 1999,
      "rating": 8,
      "is_serie": false,
      "categories": [
        {"id": 44, "name": "Franchise Films"}
      ]
    },
    {
      "id": 109899,
      "name": "Movie 42 Reloaded (2003)",
      "icon": "https://cdn.example.com/posters/movie-42-reloaded.jpg",
      "year": 2003,
      "rating": 7,
      "is_serie": false,
      "categories": [
        {"id": 44, "name": "Franchise Films"}
      ]
    }
  ],
  "next_cursor": 109899
}
```

> [!NOTE]
> Not every category id is populated in every panel. If a `category_id` filter returns `{"items": [], "next_cursor": null}` it usually means that category exists but has no VODs attached, not that the filter is broken. Fetch [List bouquets](/docs/?page=xai-ref-catalog-bouquets) and pick a category id from a category you know has content, or list VODs without the filter and pick an id from `categories[*].id` on a returned row.

## Examples

### cURL

```bash
curl -G -H "Authorization: Bearer <your-api-key>" \
     --data-urlencode "limit=100" \
     --data-urlencode "q=matrix" \
     https://<your-panel-domain>/panel-api/v1/vods
```

### PHP SDK

```php
require __DIR__ . '/api-panel-php-sdk-1.0.0/autoload.php';
use XtreamAI\PanelApi\PanelApiClient;

$client = new PanelApiClient(baseUrl: 'https://<your-panel-domain>', token: '<your-api-key>');

$page = $client->catalog->vods(limit: 100, q: 'matrix');
foreach ($page->items as $v) {
    echo $v->id, ' ', $v->name, ' ', ($v->year ?? 'unknown'), PHP_EOL;
}
$next = $page->nextCursor;  // pass back as cursor for the next page
```

### Python SDK

```python
from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(base_url="https://<your-panel-domain>", token="<your-api-key>")

page = client.catalog.vods(limit=100, q="matrix")
for v in page.items:
    print(v.id, v.name, v.year)
next_cursor = page.next_cursor  # pass back as cursor for the next page
```

## Errors

| HTTP | Error slug | When it happens | How to fix |
| ---- | ---------- | --------------- | ---------- |
| 401 | `invalid_key` | The `Authorization` header is missing, malformed, or names a key that does not exist. | Check the header. See [Authentication](/docs/?page=panel-api-authentication). |
| 403 | `insufficient_scope` | The key does not carry `vods:read`. | Regenerate the key with `vods:read` in its scope list, or use a key that has it. |
| 429 | `rate_limited` | The per key rate limit has been exceeded. Response carries `Retry-After: 60` and `X-RateLimit-*` headers. | Back off for the number of seconds in `Retry-After` and retry. |

## See also

- [Get a VOD](/docs/?page=xai-ref-catalog-vod)
- [List live streams](/docs/?page=xai-ref-catalog-streams)
- [Catalog overview](/docs/?page=xai-ref-overview)
