List sub-resellers, action=get_users
Return the resellers registered under the panel in the classic Xtream Codes envelope. The compat layer translates this call to the native GET /panel-api/v1/resellers and wraps the result in the XC {"status": "STATUS_SUCCESS", "data": {...}} shape. Rows are ordered by numeric ID and returned with the billing summary already embedded, so a reconciliation loop that walks the whole tree does not need a second call per reseller.
Only admin API keys can call this action. Reseller keys are rejected before the handler runs because they never receive resellers:read at issuance. If you need a reseller-side view of its own account, call action=user_info instead.
Endpoint
GET https://<your-panel-domain>/panel-api/xc/{accesscode}/admin/index.php?action=get_users
The {accesscode} segment is decorative. Any non-empty value works. Security is enforced through the API key, not the path.
Authentication
Send the API key either as api_key=<your-api-key> in the query string or as Authorization: Bearer <your-api-key> in the header. Admin key only. See Xtream Codes compatibility for the full auth contract.
Required scope
resellers:read. Reseller keys never carry this scope, so a reseller-issued key always sees STATUS_NO_PERMISSIONS with insufficient_scope.
Query parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
limit |
int | no | 50 |
Page size. Clamped to [1, 100] on the native handler. |
start |
int | no | 0 |
Offset-style cursor from the classic panels. Values greater than zero are forwarded to the native cursor (rows with id > start are returned). Best-effort translation, see the note below. |
The XC compat dialect only forwards
limitandstart. Other filters that some classic panels accept on this action (for examplemember_group_id,status, orusername) are dropped silently and the full list is returned. If you need to filter, call the nativeGET /panel-api/v1/resellersendpoint directly, which acceptsmember_group_id,status, andusernameas first-class query parameters.
start maps to the native keyset cursor. On a healthy panel with dense monotonic IDs this behaves like a plain offset. On a panel where rows have been deleted historically, a given start value may skip more or fewer rows than a pure offset would. If exact pagination matters, migrate this call to the native dialect and consume next_cursor instead.
Response
The data payload is the native list envelope: an items array plus next_cursor. Pass next_cursor as the next call's start value to advance. When next_cursor is null, you have reached the last page.
active_users is populated only for resellers in users billing mode. For credits-mode resellers the field is always null. created_at is a Unix timestamp in seconds, or null for historical rows migrated before that column was added.
{
"status": "STATUS_SUCCESS",
"data": {
"items": [
{
"id": 100002432,
"username": "reseller_alice",
"email": "alice@example.com",
"member_group_id": 65,
"member_group_name": "RESELLER MASTER PREMIUM",
"status": 1,
"billing_mode": "credits",
"credits": 947,
"max_users": 0,
"active_users": null,
"billing_expires": null,
"created_at": null
},
{
"id": 100260595,
"username": "reseller_bob",
"email": "bob@example.com",
"member_group_id": 4,
"member_group_name": "RESELLER",
"status": 1,
"billing_mode": "credits",
"credits": 0.25,
"max_users": 0,
"active_users": null,
"billing_expires": null,
"created_at": 1578098423
},
{
"id": 100260641,
"username": "reseller_carol",
"email": "carol@example.com",
"member_group_id": 65,
"member_group_name": "RESELLER MASTER PREMIUM",
"status": 1,
"billing_mode": "credits",
"credits": 2538,
"max_users": 0,
"active_users": null,
"billing_expires": 1783911599,
"created_at": 1579225251
}
],
"next_cursor": 100260641
}
}
HTTP status is always 200. Your client must branch on body.status, not on the HTTP code.
Examples
cURL
curl "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=get_users&limit=50"
To fetch the next page, pass the previous next_cursor as start:
curl "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=get_users&limit=50&start=100260641"
PHP raw
$url = 'https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php'
. '?' . http_build_query([
'api_key' => '<your-api-key>',
'action' => 'get_users',
'limit' => 50,
]);
$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_users failed');
}
foreach ($body['data']['items'] as $reseller) {
echo $reseller['id'], "\t", $reseller['username'], PHP_EOL;
}
Python raw
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_users", "limit": 50},
timeout=30,
)
r.raise_for_status()
body = r.json()
if body["status"] != "STATUS_SUCCESS":
raise RuntimeError(body["data"].get("message", "get_users failed"))
for reseller in body["data"]["items"]:
print(reseller["id"], reseller["username"])
Errors
HTTP is always 200 for this dialect. The status field is the branch signal, and data.error carries the exact slug so your logs can distinguish causes.
| status | Error slug | When it happens | How to fix |
|---|---|---|---|
STATUS_FAILURE |
invalid_key |
The api_key (or Authorization: Bearer) is missing, malformed, or unknown. |
Send a live admin key. |
STATUS_NO_PERMISSIONS |
insufficient_scope |
The key does not carry resellers:read. Reseller-issued keys always land here. |
Use an admin key. To view the caller's own reseller account from a reseller-side integration, call action=user_info. |
STATUS_NO_PERMISSIONS |
admin_only_endpoint |
A key that carried the scope but is still tagged as a reseller reached the handler. | Use an admin key. |
STATUS_FAILURE |
rate_limited |
The per-key request budget for the current minute is spent. | Slow the polling loop, then retry after the minute rolls over. |