---
title: "List live streams"
description: "Paginated list of live channels. Supports keyset pagination, category filtering and case-insensitive name substring search."
---

# List live streams

Returns a paginated list of live channels defined in the panel. Filterable by category and by a case-insensitive substring of the channel name. Both admin and reseller keys see the full catalog: streams are not filtered by reseller, because a storefront often needs to render what exists, not only what the caller personally sells.

The catalog is a management surface. It never returns source URLs, primary origins, DRM keys, or FFmpeg command flags. If your integration needs to play a channel, use the subscriber's Xtream Codes playlist after creating a line.

## Endpoint

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

## Authentication

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

## Required scope

`streams: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 channel you saw. The server returns channels with `id > cursor`. Omit for the first page. |
| `q` | string | no | (none) | Case-insensitive substring match on the channel name. |
| `category_id` | int | no | `0` | Restrict the response to channels that belong to this category (categories of `type='channel'`). 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 channel id. |
| `name` | string | Channel display name. |
| `icon` | string | Logo URL. Empty string when the channel has no logo. |
| `categories` | array | List of `{id, name}` objects. A channel 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.

```json
{
  "items": [
    {
      "id": 30,
      "name": "Channel 30 HD",
      "icon": "https://cdn.example.com/logos/channel-30.png",
      "categories": [
        {"id": 1, "name": "Regional TV"}
      ]
    },
    {
      "id": 38,
      "name": "Channel 38 SD",
      "icon": "https://cdn.example.com/logos/channel-38.png",
      "categories": [
        {"id": 2, "name": "Movies SD"}
      ]
    },
    {
      "id": 39,
      "name": "Channel 39 SD",
      "icon": "https://cdn.example.com/logos/channel-39.png",
      "categories": [
        {"id": 2, "name": "Movies SD"}
      ]
    },
    {
      "id": 40,
      "name": "Channel 40 SD",
      "icon": "https://cdn.example.com/logos/channel-40.png",
      "categories": [
        {"id": 2, "name": "Movies SD"}
      ]
    },
    {
      "id": 41,
      "name": "Channel 41 SD",
      "icon": "https://cdn.example.com/logos/channel-41.png",
      "categories": [
        {"id": 2, "name": "Movies SD"}
      ]
    }
  ],
  "next_cursor": 41
}
```

Fetching the next page.

```json
{
  "items": [
    {
      "id": 48,
      "name": "Channel 48 SD",
      "icon": "https://cdn.example.com/logos/channel-48.png",
      "categories": [
        {"id": 2, "name": "Movies SD"}
      ]
    }
  ],
  "next_cursor": 52
}
```

Filtering by category (`category_id=1`) returns only channels attached to that category, keeping the same shape and pagination.

## Examples

### cURL

```bash
curl -G -H "Authorization: Bearer <your-api-key>" \
     --data-urlencode "limit=50" \
     --data-urlencode "category_id=1" \
     https://<your-panel-domain>/panel-api/v1/streams
```

### 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->streams(limit: 50, categoryId: 1);
foreach ($page->items as $s) {
    echo $s->id, ' ', $s->name, 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.streams(limit=50, category_id=1)
for s in page.items:
    print(s.id, s.name)
next_cursor = page.next_cursor  # pass back as cursor for the next page
```

> [!NOTE]
> The response does not include a top level `category_id`. A channel can belong to more than one category, so the API returns the full list under `categories`. Read from `categories[*].id` and `categories[*].name` on your side; do not look for a scalar `category_id` on the channel object.

## 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 `streams:read`. | Regenerate the key with `streams: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 live stream](/docs/?page=xai-ref-catalog-stream)
- [List VODs](/docs/?page=xai-ref-catalog-vods)
- [Catalog overview](/docs/?page=xai-ref-overview)
