Create a sub-reseller, action=create_user
If you are starting a new integration instead of migrating, prefer the native v1 API with the official 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 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). See Xtream Codes compatibility 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 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.
{
"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
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
$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
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. |