---
title: "List lines, GET /lines"
description: "Paginated listing of subscriber lines. Reseller keys are auto-scoped to their own lines. Supports cursor pagination and exact-match filters on username, password, enabled, and is_trial."
---

# List lines

`GET /lines` returns subscriber lines in creation order (ascending numeric id), one page at a time. It is the workhorse endpoint for reconciliation loops, bulk exports, and any dashboard that wants to render "the lines I own".

The list is auto-scoped by key type. An **admin key** sees every line on the panel. A **reseller key** sees only lines whose `member_id` matches the reseller's own `reg_user_id`. There is no way to widen or narrow the scope from the query, and cursor values cannot be forged to jump across tenants (a reseller cursor pointing to another reseller's line simply returns an empty page).

Every item in the list has the same shape as the object returned by [`GET /lines/{id}`](/docs/?page=xai-ref-lines-get). See the "line object" section on that page for the meaning of every field.

## Endpoint

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

## Authentication

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

## Required scope

`lines:read`.

## Pagination

Cursor-based, forward-only. The response includes `next_cursor` when more rows are available, and `null` when you have reached the end.

The cursor is opaque from the caller's perspective, but the server implements it as `id > cursor`, so the ordering is stable across concurrent inserts and no row is ever returned twice. Persist the last cursor you received and pass it back on the next call.

Requesting `limit=N` reads up to `N + 1` rows internally. If the extra row is present the response contains exactly `N` items and sets `next_cursor` to the id of the last returned item. If it is absent, `next_cursor` is `null`.

## Query parameters

| Name | Type | Required | Default | Description |
| ---- | ---- | -------- | ------- | ----------- |
| `limit` | int | no | 50 | Page size. Clamped to the range `[1, 100]`. Values outside the range are silently coerced to the nearest bound. |
| `cursor` | int | no | (none) | Opaque forward cursor. Pass the `next_cursor` from the previous response. Omit on the first call. |
| `username` | string | no | (none) | Exact-match filter on `username`. Case-sensitive. |
| `password` | string | no | (none) | Exact-match filter on `password`. Case-sensitive. See the security note below. |
| `enabled` | bool | no | (none) | Filter by the reseller-visible toggle. Accepts `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off`. |
| `is_trial` | bool | no | (none) | Filter by the trial flag. Accepts the same values as `enabled`. |

> [!WARNING]
> The `password` filter is intended for the reconciliation flow that mirrors OneStream's "find line" behavior. The value ends up in the query string and therefore in every access log along the way (your panel's Nginx, any client-side proxy, and any intermediary). Our own access-log format masks the query string on Panel API requests, but we cannot mask logs owned by third parties. When you already know the id, use [`GET /lines/{id}`](/docs/?page=xai-ref-lines-get) instead. When you need to look up by password, prefer the alternate `username=` + `password=` combo over pure `password=` searches.

## Response

An object with two fields:

| Field | Type | Description |
| ----- | ---- | ----------- |
| `items` | Line[] | Page of lines in ascending `id` order. Each item has the shape documented on [`GET /lines/{id}`](/docs/?page=xai-ref-lines-get). |
| `next_cursor` | int or null | Cursor to pass on the next call, or `null` if this was the last page. |

```json
{
  "items": [
    {
      "id": 1512227,
      "username": "u_ab12cd34",
      "password": "9f3c1a77",
      "member_id": 42,
      "exp_date": 1813449600,
      "max_connections": 4,
      "is_trial": false,
      "is_restreamer": false,
      "enabled": true,
      "admin_enabled": true,
      "bouquets": [2, 4, 14, 15, 82, 107, 108, 110, 112, 114, 115, 118, 120, 134],
      "created_at": 1574874852
    },
    {
      "id": 1527378,
      "username": "u_e4f56789",
      "password": "1a2b3c4d",
      "member_id": 42,
      "exp_date": 1790985600,
      "max_connections": 3,
      "is_trial": false,
      "is_restreamer": false,
      "enabled": true,
      "admin_enabled": true,
      "bouquets": [2, 4, 15, 82, 107, 108, 110, 112, 114, 115, 118, 119, 120, 134],
      "created_at": 1578154802
    },
    {
      "id": 1532884,
      "username": "u_9a0b1c2d",
      "password": "e5f6a7b8",
      "member_id": 260641,
      "exp_date": 1784513884,
      "max_connections": 5,
      "is_trial": false,
      "is_restreamer": false,
      "enabled": true,
      "admin_enabled": true,
      "bouquets": [15, 110, 118],
      "created_at": 1579290632
    }
  ],
  "next_cursor": 1532884
}
```

## Examples

### cURL

```bash
curl -X GET "https://<your-panel-domain>/panel-api/v1/lines?limit=50" \
  -H "Authorization: Bearer <your-api-key>"
```

Paginating through the whole set:

```bash
CURSOR=""
while :; do
  URL="https://<your-panel-domain>/panel-api/v1/lines?limit=100${CURSOR:+&cursor=$CURSOR}"
  BODY=$(curl -s -H "Authorization: Bearer <your-api-key>" "$URL")
  echo "$BODY" | jq '.items[]'
  CURSOR=$(echo "$BODY" | jq -r '.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done
```

### 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>');
$cursor = null;
do {
    $page = $client->lines->list(limit: 100, cursor: $cursor);
    foreach ($page->items as $line) {
        echo $line->id . " " . $line->username . "\n";
    }
    $cursor = $page->nextCursor;
} while ($cursor !== null);
```

### Python SDK

```python
from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(base_url="https://<your-panel-domain>", token="<your-api-key>")
cursor = None
while True:
    page = client.lines.list(limit=100, cursor=cursor)
    for line in page.items:
        print(line.id, line.username)
    if page.next_cursor is None:
        break
    cursor = str(page.next_cursor)
```

## Errors

| HTTP | Error slug | When it happens | How to fix |
| ---- | ---------- | --------------- | ---------- |
| 401 | `invalid_key` | The `Authorization` header is missing, malformed, points to an unknown key, or the key is disabled, expired, deleted, or IP-restricted. | Verify the token. Reissue if it was rotated. |
| 403 | `insufficient_scope` | The key does not have `lines:read`. | Grant `lines:read` to the key from the panel, or issue a new key with that scope. |
| 429 | `rate_limited` | You exceeded the key's per-minute rate limit or the panel-wide per-IP limit. | Honor the `Retry-After` header. `X-RateLimit-Remaining` on successful responses tells you how close you are to the limit. |
| 503 | `api_disabled` | An admin has turned the Panel API off for this panel. | Contact the panel admin. |

## See also

- [Get a single line, GET /lines/{id}](/docs/?page=xai-ref-lines-get)
- [Get live connections for a line](/docs/?page=xai-ref-lines-connections)
- [Panel API Lines (full guide)](/docs/?page=xai-ref-lines-list)
- [Errors, retries and idempotency](/docs/?page=panel-api-errors)
