---
title: "Create a sub-reseller, action=create_user"
description: "Provision a new reseller (with an admin key) or a sub-reseller under the calling reseller (with a reseller key), in the XC dialect. Maps to POST /panel-api/v1/resellers."
---

# Create a sub-reseller, action=create_user

> [!NOTE]
> If you are starting a new integration instead of migrating, prefer the [native v1 API](/docs/?page=xai-ref-resellers-create) with the [official SDKs](/docs/?page=panel-api-sdks).

Create a new account inside the reseller tree in the classic Xtream Codes envelope. The compat layer translates this call to the native [`POST /panel-api/v1/resellers`](/docs/?page=xai-ref-resellers-create) and wraps the result in the XC `{"status": "STATUS_SUCCESS", "data": {...}}` shape.

Behavior depends on the calling key. An admin key can create a top-level reseller in either billing mode with any starting balance. A reseller key can only create a sub-reseller under its own account, in the same billing mode as the parent, with a zero starting balance, and only if the panel admin has enabled `create_sub_resellers` on the caller's member group and configured a sub-reseller package for that group.

The endpoint runs the check and the insert inside a single transaction with `SELECT ... FOR UPDATE` on the caller's own row, so two concurrent creates can never over-allocate slots or double-spend credits. Retries covered by the `rid` idempotency parameter never provision a duplicate account.

## Endpoint

`POST https://<your-panel-domain>/panel-api/xc/{accesscode}/{admin|reseller}/index.php?action=create_user`

The `{accesscode}` segment is decorative. Any non-empty value works. Both `/admin/index.php` and `/reseller/index.php` accept the same key type; the API infers admin-versus-reseller from the key itself. 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, as `Authorization: Bearer <your-api-key>` in the header, or as a form field in the POST body. Both admin and reseller keys can call this action; the field set they may send differs (see [Request body](#request-body)). See [Xtream Codes compatibility](/docs/?page=panel-api-xtream-codes-compatibility#base-url-and-authentication) for the full auth contract.

## Required scope

`subresellers:write`. Admin keys carry this scope by default. Reseller-issued keys must have it explicitly.

## Idempotency

Every write action in the XC dialect accepts an optional `rid` parameter (in the query string or the POST body). Repeating the exact same request with the same `rid` and the same body replays the original response instead of running twice. Reusing the same `rid` with a different body returns `STATUS_FAILURE` with `error: "idempotency_conflict"`. Use a stable value such as the invoice ID or CRM record ID that triggered the provision. Values up to 255 characters, scoped per API key, retained for 24 hours. See [Idempotency via rid](/docs/?page=panel-api-xtream-codes-compatibility#idempotency-via-rid) for the full contract.

## Request body

XC integrations POST form-encoded (`application/x-www-form-urlencoded`).

| Field | Type | Required | Default | Description |
| ----- | ---- | -------- | ------- | ----------- |
| `username` | string | yes | | Login name. Minimum 3 characters. Cannot contain `% & ? # / \ = + @ : ;`. Must be unique across the panel. |
| `password` | string | yes | | Login password. Minimum 8 characters, same character restrictions as `username`. |
| `email` | string | yes | | Validated with `FILTER_VALIDATE_EMAIL`. |
| `member_group_id` | int | admin only | | Which member group the new reseller belongs to. Ignored on reseller keys (the target group is fixed by the panel admin's sub-reseller setup for the caller's group). |
| `credits` | float | admin only | `0` | Starting credit balance. Forced to `0.0` for reseller-key callers; top up later with `action=adjust_credits`. |
| `billing_mode` | string | admin only | `"credits"` | Either `"credits"` or `"users"`. Any other value falls back to `"credits"`. Reseller-key callers inherit the parent's mode; the field is ignored. |
| `max_users` | int | conditional | `0` | Required when `billing_mode` is `"users"`, and must be positive. |
| `billing_expires` | int | conditional | | Unix timestamp in seconds. Required when a reseller-key caller in `users` mode creates a sub-reseller. Optional for admin-key calls. |
| `owner_id` | int | admin only | panel admin | Parent account for the new reseller. Reseller-key callers must either omit this field or send their own reg-user ID; any other value returns `STATUS_NO_PERMISSIONS` with `owner_must_be_self`. |
| `notes` | string | no | `""` | Free-form label kept alongside the reseller record. |
| `rid` | string | no | | Idempotency token (see above). Optional, but strongly recommended for automated billing. |

## Response

On success the endpoint returns `STATUS_SUCCESS` with the new ID, the billing snapshot the account starts with, and `credits_charged`, which reflects how many credits were deducted from the caller. For admin-key calls that field is always `0`. For reseller-key calls in `credits` mode it matches the per-creation price configured on the caller's group.

```json
{
  "status": "STATUS_SUCCESS",
  "data": {
    "id": 100262261,
    "username": "reseller_new",
    "email": "reseller_new@example.com",
    "member_group_id": 4,
    "owner_id": 100260897,
    "credits_charged": 0,
    "billing": {
      "mode": "credits",
      "credits": 100,
      "max_users": null,
      "active_users": null,
      "billing_expires": null
    }
  }
}
```

HTTP status is always `200`. Your client must branch on `body.status`, not on the HTTP code.

## Examples

### cURL

```bash
curl -X POST "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=create_user" \
  -d "username=reseller_new" \
  -d "password=SuperSecret123" \
  -d "email=reseller_new@example.com" \
  -d "member_group_id=4" \
  -d "billing_mode=credits" \
  -d "credits=100" \
  -d "notes=CRM signup 90142" \
  -d "rid=crm-signup-90142"
```

### 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'  => 'create_user',
       ]);
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'username'        => 'reseller_new',
        'password'        => 'SuperSecret123',
        'email'           => 'reseller_new@example.com',
        'member_group_id' => 4,
        'billing_mode'    => 'credits',
        'credits'         => 100,
        'notes'           => 'CRM signup 90142',
        'rid'             => 'crm-signup-90142',
    ]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($body['status'] !== 'STATUS_SUCCESS') {
    throw new RuntimeException($body['data']['message'] ?? 'create_user failed');
}
$resellerId = $body['data']['id'];
```

### Python raw

```python
import requests

r = requests.post(
    "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php",
    params={"api_key": "<your-api-key>", "action": "create_user"},
    data={
        "username":        "reseller_new",
        "password":        "SuperSecret123",
        "email":           "reseller_new@example.com",
        "member_group_id": 4,
        "billing_mode":    "credits",
        "credits":         100,
        "notes":           "CRM signup 90142",
        "rid":             "crm-signup-90142",
    },
    timeout=30,
)
r.raise_for_status()
body = r.json()
if body["status"] != "STATUS_SUCCESS":
    raise RuntimeError(body["data"].get("message", "create_user failed"))
reseller_id = body["data"]["id"]
```

## Errors

HTTP is always 200 for this dialect. The `status` field is the branch signal, and `data.error` carries the exact slug.

| status | Error slug | When it happens | How to fix |
| ------ | ---------- | --------------- | ---------- |
| `STATUS_INVALID_DATA` | `validation_error` | Username, password, or email fails the validators (length, forbidden characters, invalid email, duplicate username, unknown `member_group_id`). The `message` carries the reason. | Fix the offending field and resend. |
| `STATUS_INVALID_DATA` | `validation_error` | A reseller-key caller in `users` mode sent `billing_expires_required` or a non-positive `max_users`. | Add a positive Unix timestamp for `billing_expires` and a positive `max_users`. |
| `STATUS_FAILURE` | `invalid_key` | The `api_key` (or `Authorization: Bearer`) is missing, malformed, or unknown. | Send a live key. |
| `STATUS_NO_PERMISSIONS` | `insufficient_scope` | The key does not carry `subresellers:write`. | Rotate the key with the correct scopes. |
| `STATUS_NO_PERMISSIONS` | `owner_must_be_self` | A reseller-key caller passed an `owner_id` other than their own. | Omit `owner_id`, or set it to the caller's own reg-user ID. |
| `STATUS_NO_PERMISSIONS` | `sub_reseller_creation_not_allowed` | The reseller-key caller's group does not have `create_sub_resellers = 1`. | The panel admin turns this on per group in the CMS. |
| `STATUS_INVALID_DATA` | `no_sub_reseller_setup` | The panel admin has not configured a sub-reseller package for the reseller-key caller's group, or the configured package is not usable. | The admin configures it in the CMS. |
| `STATUS_INSUFFICIENT_CREDITS` | `insufficient_credits` | A reseller-key caller in `credits` mode does not have enough balance to pay the per-creation price. | Top up the caller with `action=adjust_credits`, then retry. |
| `STATUS_INSUFFICIENT_CREDITS` | `insufficient_slots` | A reseller-key caller in `users` mode asked for more `max_users` than they have available. XC has no slot-specific status value, so the slot error surfaces under `STATUS_INSUFFICIENT_CREDITS`. | Free slots on the caller, or lower `max_users`. |
| `STATUS_FAILURE` | `idempotency_conflict` | The `rid` was reused with a different body. | Generate a new `rid` or resend the original body. |
| `STATUS_FAILURE` | `rate_limited` | The per-key request budget for the current minute is spent. | Back off and retry after the minute rolls over. |

## See also

- [List sub-resellers, action=get_users](/docs/?page=xc-ref-users-list)
- [Update a sub-reseller, action=edit_user](/docs/?page=xc-ref-users-edit)
- [Adjust reseller credits, action=adjust_credits](/docs/?page=xc-ref-adjust-credits)
- [Xtream Codes compatibility overview](/docs/?page=panel-api-xtream-codes-compatibility)
- [Native v1: create a reseller](/docs/?page=xai-ref-resellers-create)
