---
title: "Update a sub-reseller, action=edit_user"
description: "Change a reseller's username, email, member group, password, or notes in the XC dialect. Admin-only. Maps to POST /panel-api/v1/resellers/{id}/update."
---

# Update a sub-reseller, action=edit_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).

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

> [!IMPORTANT]
> 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.

> [!NOTE]
> 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).

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

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

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

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

- [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)
- [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: update a reseller](/docs/?page=xai-ref-resellers-update)
