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.
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).
{
"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.
{
"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
}
Not every category id is populated in every panel. If a
category_idfilter 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 and pick a category id from a category you know has content, or list VODs without the filter and pick an id fromcategories[*].idon a returned row.
Examples
cURL
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
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
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. |
| 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. |