Update a sub-reseller, action=edit_user

If you are starting a new integration instead of migrating, prefer the native v1 API with the official SDKs.

Update the profile fields of an existing reseller in the classic Xtream Codes envelope. The compat layer translates this call to the native POST /panel-api/v1/resellers/{id}/update and wraps the result in the XC {"status": "STATUS_SUCCESS", "data": {...}} shape.

This route intentionally does not touch billing. Credits, billing_mode, max_users, and billing_expires are read-only here. Move them with action=adjust_credits. That split keeps billing behind a single audited path with its own concurrency guards, so a routine profile edit cannot accidentally reset a balance to zero.

Only admin API keys can call this action.

Endpoint

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

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, as Authorization: Bearer <your-api-key> in the header, or as a form field in the POST body. Admin key only. See Xtream Codes compatibility for the full auth contract.

Required scope

resellers:write. Reseller keys never carry this scope, so a reseller-issued key always sees STATUS_NO_PERMISSIONS with insufficient_scope.

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". See Idempotency via rid for the full contract.

Request body

XC integrations POST form-encoded (application/x-www-form-urlencoded). Only the fields you send are updated; the rest of the reseller record is preserved.

Field Type Required Default Description
id int yes Numeric reg-user ID of the reseller.
username string no current value Login name. Minimum 3 characters. Cannot contain % & ? # / \ = + @ : ;. Must be unique across the panel.
password string no preserved Login password. Minimum 8 characters, same character restrictions as username. An empty string is treated as "preserve current".
email string no current value Validated with FILTER_VALIDATE_EMAIL.
member_group_id int no current value Move the reseller to a different member group. Use with care: the new group determines every permission the account inherits.
notes string no current value Free-form label kept alongside the reseller record.
rid string no Idempotency token (see above). Optional, but strongly recommended for automated CRM sync flows.

Fields the classic Xtream Codes edit_user accepted that this route does NOT process: credits, billing_mode, max_users, billing_expires, owner_id. They are ignored silently on this call. Use action=adjust_credits for credits and slot moves, and change ownership from the CMS.

The response echoes id, username, email, member_group_id, and the current billing shape, but it does NOT echo notes even when you have just written it. If your integration reconciles state by comparing what it sent to what came back, fetch the reseller separately with action=get_user after the update.

Response

On success the endpoint returns STATUS_SUCCESS with the updated identity fields and the current billing snapshot (unchanged by this call).

{
  "status": "STATUS_SUCCESS",
  "data": {
    "id": 100262261,
    "username": "reseller_bob",
    "email": "reseller_bob-new@example.com",
    "member_group_id": 4,
    "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=edit_user" \
  -d "id=100262261" \
  -d "email=reseller_bob-new@example.com" \
  -d "notes=Email updated 2026-01-15" \
  -d "rid=edit-262261-2026-01-15"

PHP raw

$url = 'https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php'
     . '?' . http_build_query([
         'api_key' => '<your-api-key>',
         'action'  => 'edit_user',
       ]);
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'id'    => 100262261,
        'email' => 'reseller_bob-new@example.com',
        'notes' => 'Email updated 2026-01-15',
        'rid'   => 'edit-262261-2026-01-15',
    ]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($body['status'] !== 'STATUS_SUCCESS') {
    throw new RuntimeException($body['data']['message'] ?? 'edit_user failed');
}

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": "edit_user"},
    data={
        "id":    100262261,
        "email": "reseller_bob-new@example.com",
        "notes": "Email updated 2026-01-15",
        "rid":   "edit-262261-2026-01-15",
    },
    timeout=30,
)
r.raise_for_status()
body = r.json()
if body["status"] != "STATUS_SUCCESS":
    raise RuntimeError(body["data"].get("message", "edit_user failed"))

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 The id parameter is missing, or a field fails the validators (username too short, forbidden characters, invalid email, duplicate username, unknown member_group_id). The message carries the reason. Fix the offending field and resend.
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:write. Reseller-issued keys always land here. Use an admin key.
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 not_found No reseller exists with that ID. Verify the ID with action=get_user first.
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