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.

See also