List lines
action=get_lines is the compatibility-layer equivalent of the native GET /panel-api/v1/lines. It returns a page of subscriber lines wrapped in the classic {"status": "STATUS_SUCCESS", "data": {...}} envelope, in ascending numeric id order. Under the hood the request is translated to the native list endpoint and the response is rewrapped, so scoping, ordering and filters are identical.
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.
Endpoint
GET https://<your-panel-domain>/panel-api/xc/{accesscode}/admin/index.php?action=get_lines
Both /admin/index.php and /reseller/index.php are accepted for the same action. The admin-versus-reseller decision is derived from the key itself, not from the path. {accesscode} is decorative (any non-empty value works, see Xtream Codes / XUI.one / OTT Panel compatibility).
Authentication
Send the token in one of three ways. The three forms are exclusive alternatives, not additive.
?api_key=<your-api-key>in the query string (the classic XC form).api_key=<your-api-key>in the POST body (form field), if you want to keep the key out of URLs and logs.Authorization: Bearer <your-api-key>HTTP header, for integrations that already speak Bearer.
See Authentication for how to issue a key and pick its scopes.
Required scope
lines:read.
Query parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
action |
string | yes | Must be get_lines. |
|
api_key |
string | yes (unless sent via Bearer header or body) | Your Panel API token. | |
limit |
int | no | 50 | Page size. Clamped to the range [1, 100] by the native handler. |
start |
int | no | 0 | Offset-style pagination. Best-effort mapped to the native cursor as cursor = start. See the pagination note below. |
username |
string | no | Exact-match filter on username (case-sensitive). |
|
search[value] |
string | no | DataTables-style search value. Mapped to username=<term> as a best-effort exact match. Other DataTables keys (search[regex], columns[...]) are ignored. |
limit and start are read from the query string on GET, or from the POST body on POST. Send whichever fits your existing integration.
Pagination note
The XC dialect exposes start (offset) plus limit, while the underlying native handler uses cursor (keyset on the last returned id). The compat layer maps start > 0 to cursor = start as a bridge. If your ids are dense and monotonic (typical for a healthy panel), pagination works as expected. If ids are sparse, or if you paginate through the whole list by walking start = 0, limit, 2*limit, ..., you may miss or repeat rows. For reliable pagination over a large resource, migrate this call to the native GET /panel-api/v1/lines with next_cursor.
Search note
search[value] maps to an exact username=<term> filter. Classic XC panels searched across several columns; the compat layer only wires up the username filter. If your integration currently searches by email, package_name, or connection state, migrate to the native list endpoint, which exposes more filters.
Response
data is an object with two fields.
| Field | Type | Description |
|---|---|---|
items |
Line[] | Page of line objects in ascending id order. Same shape as the object returned by action=get_line. |
next_cursor |
int or null | Cursor to pass on the next call. null when this was the last page. Pass it as start=<value> on the next request. |
{
"status": "STATUS_SUCCESS",
"data": {
"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
}
],
"next_cursor": 1527378
}
}
HTTP status is always 200, even on failure. Branch on the status field in the envelope, not on the HTTP status.
Examples
cURL
curl "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=get_lines&limit=50"
Filter by exact username:
curl "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=get_lines&username=alice"
Walk through every page:
BASE="https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php"
TOKEN="<your-api-key>"
START=0
while :; do
BODY=$(curl -s "$BASE?api_key=$TOKEN&action=get_lines&limit=100&start=$START")
echo "$BODY" | jq '.data.items[]'
NEXT=$(echo "$BODY" | jq -r '.data.next_cursor // empty')
[ -z "$NEXT" ] && break
START=$NEXT
done
PHP (raw HTTP)
$url = 'https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php'
. '?' . http_build_query([
'api_key' => '<your-api-key>',
'action' => 'get_lines',
'limit' => 50,
'username' => 'alice',
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if (($body['status'] ?? '') !== 'STATUS_SUCCESS') {
throw new RuntimeException($body['data']['message'] ?? 'get_lines failed');
}
foreach ($body['data']['items'] as $line) {
printf("%d %s exp=%d\n", $line['id'], $line['username'], $line['exp_date']);
}
$next = $body['data']['next_cursor'] ?? null;
Python (raw HTTP)
import requests
r = requests.get(
"https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php",
params={
"api_key": "<your-api-key>",
"action": "get_lines",
"limit": 50,
"username": "alice",
},
timeout=30,
)
r.raise_for_status() # only fires on real HTTP errors, not STATUS_FAILURE
body = r.json()
if body.get("status") != "STATUS_SUCCESS":
raise RuntimeError(body.get("data", {}).get("message", "get_lines failed"))
for line in body["data"]["items"]:
print(line["id"], line["username"], line["exp_date"])
next_cursor = body["data"]["next_cursor"]
Errors
Response is always HTTP 200. Branch on status and, for failures, read data.error for the slug and data.message for a human-readable string.
| status | Error slug | When it happens | How to fix |
|---|---|---|---|
STATUS_FAILURE |
invalid_key |
The token is missing, unknown, disabled, expired, or the source IP is not in the key's allow-list. | Verify the token. Reissue if rotated. Check the IP allow-list on the key. |
STATUS_NO_PERMISSIONS |
insufficient_scope |
The key does not have lines:read. |
Grant lines:read, or issue a new key with that scope. |
STATUS_FAILURE |
rate_limited |
The key hit its per-minute rate limit or the panel-wide per-IP cap. | Back off. Look at X-RateLimit-Remaining on successful responses to know how close you are. |
STATUS_FAILURE |
api_disabled |
An admin has switched the Panel API off for this panel. | Contact the panel admin. |