---
title: "List lines, GET /ext/lines and /ext/lines/index"
description: "List subscriber lines in the OneStream shape. Plain array or paginated envelope, opaque UUIDs, exact-match filters."
---

# GET /ext/lines and /ext/lines/index

Two endpoints, one dataset, two response shapes. Both list subscriber lines the calling key can see and both return each line as an object with the OneStream `line_id` UUID contract, ISO 8601 timestamps, and real JSON booleans.

- `GET /ext/lines` returns a plain JSON array of line objects. Use it when your integration expects the classic OneStream shape.
- `GET /ext/lines/index` wraps the same array in an envelope with pagination metadata. Use it when your integration reads `has_more` and walks pages until the flag comes back `false`.

The list is auto-scoped by caller 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 a cursor value that points at another tenant's line simply returns an empty page.

## Endpoints

```
GET https://<your-panel-domain>/panel-api/onestream/ext/lines
GET https://<your-panel-domain>/panel-api/onestream/ext/lines/index
```

## Authentication

Send the API key in `X-Api-Key`, `X-Auth-User`, or `Authorization: Bearer`. See the [OneStream overview](/docs/?page=os-ref-overview#base-url-and-authentication).

## Required scope

`lines:read`.

## Query parameters

| Name | Type | Required | Default | Description |
| ---- | ---- | -------- | ------- | ----------- |
| `username` | string | no | (none) | Exact-match filter on `username`. Case-sensitive. |
| `per_page` | int | no | 50 | Page size. Clamped internally to `[1, 100]`. |
| `limit` | int | no | 50 | Alias of `per_page`. If both are supplied, `limit` wins. |
| `page` | int | no | 1 | Page number for callers that walk pages by index. Translated internally as offset (see the pagination note below). |

## Pagination

The OneStream dialect exposes `page` + `per_page`. Under the hood, the panel uses cursor-based pagination on line id. The dialect translates by computing an offset (`(page - 1) * per_page`) and passing it as `cursor` to the underlying v1 endpoint.

This translation is exact when line ids are dense (a freshly seeded panel) and approximate when line ids are sparse (a mature panel where rows have been deleted over time). On sparse panels, walking `page=1`, `page=2`, `page=3` can return overlapping items, because a cursor value equal to `(page - 1) * per_page` does not necessarily land on the boundary between two consecutive pages.

Two safe patterns:

1. Call `/ext/lines/index` and honor the `has_more` field in the pagination envelope. Iterate until it is `false`.
2. Move the listing flow to the native [`GET /panel-api/v1/lines?cursor=<value>`](/docs/?page=xai-ref-lines-list) endpoint, which returns a `next_cursor` you pass into the next request. It is exact under any distribution of ids.

## Response shapes

### `/ext/lines`

A plain JSON array. Each element is a line object with the fields below.

```json
[
  {
    "line_id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "customer_001",
    "password": "d7dca035",
    "expire_at": "2027-06-20T00:00:00+00:00",
    "is_enabled": true,
    "is_restreamer": false,
    "is_trial": false,
    "package_id": null,
    "bouquets": [2, 4, 14, 15, 82, 107, 108, 110, 112, 114, 115, 118, 120, 134],
    "max_connections": 4,
    "reseller_notes": "",
    "mac_addr": null,
    "owner": "billing",
    "type": "regular"
  },
  {
    "line_id": "6ba7b810-9dad-41d1-80b4-00c04fd430c8",
    "username": "customer_002",
    "password": "a3b91c74",
    "expire_at": "2026-10-03T00:00:00+00:00",
    "is_enabled": true,
    "is_restreamer": false,
    "is_trial": false,
    "package_id": null,
    "bouquets": [2, 4, 15, 82, 107, 108, 110, 112, 114, 115, 118, 119, 120, 134],
    "max_connections": 3,
    "reseller_notes": "",
    "mac_addr": null,
    "owner": "billing",
    "type": "regular"
  }
]
```

### `/ext/lines/index`

An object with a `status` string and a `data` object containing pagination metadata and the items.

```json
{
  "status": "success",
  "data": {
    "pagination": {
      "current_page": 1,
      "per_page": 2,
      "total": 2,
      "has_more": true
    },
    "items": [
      {
        "line_id": "550e8400-e29b-41d4-a716-446655440000",
        "username": "customer_001",
        "password": "d7dca035",
        "expire_at": "2027-06-20T00:00:00+00:00",
        "is_enabled": true,
        "is_restreamer": false,
        "is_trial": false,
        "package_id": null,
        "bouquets": [2, 4, 14, 15, 82, 107, 108, 110, 112, 114, 115, 118, 120, 134],
        "max_connections": 4,
        "reseller_notes": "",
        "mac_addr": null,
        "owner": "billing",
        "type": "regular"
      }
    ]
  }
}
```

`pagination.total` is set to the count of items on the current page, not the total row count of the tenant. The underlying cursor pagination does not compute a global total, and computing one on demand would be expensive on large panels. Trust `has_more` as the signal for "keep walking".

## Line object fields

Every line, whether returned bare in `/ext/lines` or inside `data.items` of `/ext/lines/index`, has this shape.

| Field | Type | Description |
| ----- | ---- | ----------- |
| `line_id` | string (UUID) | Opaque, license-scoped identifier. See [Line IDs are opaque](/docs/?page=os-ref-overview#line-ids-are-opaque). |
| `username` | string | Line username. |
| `password` | string | Line password. |
| `expire_at` | string or null | ISO 8601 timestamp with offset, or `null` if the line has no expiration. `null` here is distinct from `0`, which the engine would treat as an expired line. |
| `is_enabled` | bool | Reseller-visible toggle. `false` blocks logins without deleting the line. |
| `is_restreamer` | bool | `true` when the line has restreamer permissions. |
| `is_trial` | bool | `true` when the line was created off a trial variant of a package. |
| `package_id` | int or null | Currently always `null`. The underlying v1 line object does not carry the source package; if you need it, store it locally at creation time. |
| `bouquets` | int[] | Array of bouquet ids attached to the line. |
| `max_connections` | int | Maximum concurrent devices allowed to be logged in. |
| `reseller_notes` | string | Currently always empty (`""`). The v1 line formatter does not project reseller notes into this field. |
| `mac_addr` | null | Always `null`. This product does not provision physical MAG or Enigma devices. |
| `owner` | string | Fixed value `"billing"`. |
| `type` | string | Fixed value `"regular"`. |

## Examples

### cURL

```bash
# Plain array
curl -H "X-Api-Key: <your-api-key>" \
     "https://<your-panel-domain>/panel-api/onestream/ext/lines?per_page=50"

# Paginated envelope
curl -H "X-Api-Key: <your-api-key>" \
     "https://<your-panel-domain>/panel-api/onestream/ext/lines/index?per_page=50&page=1"
```

### PHP raw

```php
$url = 'https://<your-panel-domain>/panel-api/onestream/ext/lines/index?per_page=50&page=1';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-Key: <your-api-key>']);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);

foreach ($body['data']['items'] as $line) {
    echo $line['line_id'] . ' ' . $line['username'] . "\n";
}
$hasMore = $body['data']['pagination']['has_more'] ?? false;
```

### Python raw

```python
import requests

r = requests.get(
    "https://<your-panel-domain>/panel-api/onestream/ext/lines/index",
    headers={"X-Api-Key": "<your-api-key>"},
    params={"per_page": 50, "page": 1},
    timeout=30,
)
r.raise_for_status()
body = r.json()

for line in body["data"]["items"]:
    print(line["line_id"], line["username"])

has_more = body["data"]["pagination"]["has_more"]
```

## Errors

| HTTP | Error slug | When it happens | How to fix |
| ---- | ---------- | --------------- | ---------- |
| 401 | `invalid_key` | Header is missing, the token is unknown, the key was disabled, expired, deleted, or the caller IP is not on the key's IP allow-list. | Check the header. If the key was rotated, mint a new one from the panel. |
| 403 | `insufficient_scope` | The key does not carry `lines:read`. | Regenerate the key with `lines:read`, or use a key that has it. |
| 429 | `rate_limited` | The per-key or per-IP rate limit was hit. Response carries `Retry-After` and `X-RateLimit-*` headers. | Back off for the number of seconds in `Retry-After`. |
| 403 | `api_disabled` | An admin has turned the Panel API off for this panel. | Ask the admin to re-enable it. |

## See also

- [Find a line by username](/docs/?page=os-ref-lines-find)
- [OneStream overview](/docs/?page=os-ref-overview)
- [Native lines listing](/docs/?page=xai-ref-lines-list) for exact cursor pagination.
