---
title: "List sub-resellers, action=get_users"
description: "Return a page of resellers in the XC dialect. Admin-only. Maps to the native GET /panel-api/v1/resellers endpoint."
---

# 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`](/docs/?page=xai-ref-resellers-list) 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](/docs/?page=panel-api-xtream-codes-compatibility#base-url-and-authentication) 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. |

> [!NOTE]
> The XC compat dialect only forwards `limit` and `start`. Other filters that some classic panels accept on this action (for example `member_group_id`, `status`, or `username`) are dropped silently and the full list is returned. If you need to filter, call the native [`GET /panel-api/v1/resellers`](/docs/?page=xai-ref-resellers-list) endpoint directly, which accepts `member_group_id`, `status`, and `username` as 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.

```json
{
  "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

```bash
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`:

```bash
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

```php
$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

```python
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. |

## See also

- [Get one sub-reseller, action=get_user](/docs/?page=xc-ref-users-get)
- [Create a sub-reseller, action=create_user](/docs/?page=xc-ref-users-create)
- [Xtream Codes compatibility overview](/docs/?page=panel-api-xtream-codes-compatibility)
- [Native v1: list resellers](/docs/?page=xai-ref-resellers-list)
