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}. 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.
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. |
The
passwordfilter 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, useGET /lines/{id}instead. When you need to look up by password, prefer the alternateusername=+password=combo over purepassword=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}. |
next_cursor |
int or null | Cursor to pass on the next call, or null if this was the last page. |
{
"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
curl -X GET "https://<your-panel-domain>/panel-api/v1/lines?limit=50" \
-H "Authorization: Bearer <your-api-key>"
Paginating through the whole set:
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
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
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. |