---
title: "Create a reseller"
description: "Provision a new reseller (with an admin key) or a sub-reseller under the calling reseller (with a reseller key)."
---

# Create a reseller

Create a new account inside the reseller tree. The 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.

The endpoint runs the check and the insert inside a single transaction with a `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 idempotency key never provision a duplicate account.

## Endpoint

`POST https://<your-panel-domain>/panel-api/v1/resellers`

## Authentication

Send the API key in the `Authorization: Bearer <your-api-key>` header. Both admin and reseller keys can call this endpoint. Which parameters they can send differs (see below).

## Required scope

`subresellers:write`

Admin keys also carry this scope by default. It is called `subresellers:write` (not `resellers:write`) so a reseller key can hold the write half of the reseller graph without being able to touch anyone else's account.

## Idempotency

Every POST must include an `Idempotency-Key` header. Reusing the same key with the same body replays the cached response; reusing it with a different body returns `409 idempotency_conflict`. Use a stable value such as the invoice ID or CRM record ID that triggered the provision, so a network retry never creates a second reseller with a suffixed username.

## Request body

| Field | Type | Required | Default | Description |
| ----- | ---- | -------- | ------- | ----------- |
| `username` | string | yes | | Login name. Minimum 3 characters. Cannot contain `% & ? # / \ = + @ : ;`. |
| `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). |
| `credits` | float | admin only | `0` | Starting credit balance. Forced to `0.0` for reseller-key callers; top up later with `POST /resellers/{id}/billing/adjust`. |
| `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 ID; any other value returns `403 owner_must_be_self`. |
| `notes` | string | no | `""` | Free-form label kept alongside the reseller record. |

```json
{
  "username": "reseller_new",
  "password": "s3cret-p4ssword",
  "email": "reseller_new@example.com",
  "member_group_id": 4,
  "credits": 10.0,
  "billing_mode": "credits",
  "notes": "docs batch fixture"
}
```

## Response

On success the endpoint returns `201 Created` 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
{
  "id": 262260,
  "username": "reseller_new",
  "email": "reseller_new@example.com",
  "member_group_id": 4,
  "owner_id": 260897,
  "credits_charged": 0,
  "billing": {
    "mode": "credits",
    "credits": 10,
    "max_users": null,
    "active_users": null,
    "billing_expires": null
  }
}
```

## Examples

### cURL

```bash
curl -X POST https://<your-panel-domain>/panel-api/v1/resellers \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-signup-90142" \
  -d '{
        "username": "reseller_new",
        "password": "s3cret-p4ssword",
        "email": "reseller_new@example.com",
        "member_group_id": 4,
        "credits": 10.0,
        "billing_mode": "credits",
        "notes": "docs batch fixture"
      }'
```

### PHP SDK

```php
require __DIR__ . '/api-panel-php-sdk-1.0.0/autoload.php';
use XtreamAI\PanelApi\PanelApiClient;

$client = new PanelApiClient(baseUrl: 'https://<your-panel-domain>', token: '<your-api-key>');
$reseller = $client->resellers->create(
    username:       'reseller_new',
    password:       's3cret-p4ssword',
    email:          'reseller_new@example.com',
    memberGroupId:  4,
    credits:        10.0,
    billingMode:    'credits',
    notes:          'docs batch fixture',
    idempotencyKey: 'crm-signup-90142',
);
echo $reseller->id, PHP_EOL;
```

### Python SDK

```python
from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(base_url="https://<your-panel-domain>", token="<your-api-key>")
reseller = client.resellers.create(
    username="reseller_new",
    password="s3cret-p4ssword",
    email="reseller_new@example.com",
    member_group_id=4,
    credits=10.0,
    billing_mode="credits",
    notes="docs batch fixture",
    idempotency_key="crm-signup-90142",
)
print(reseller.id)
```

## Errors

| HTTP | Error slug | When it happens | How to fix |
| ---- | ---------- | --------------- | ---------- |
| 400 | `missing_idempotency_key` | The `Idempotency-Key` header was not sent. | Add the header on every POST. See the [Rate limits and Idempotency](/docs/?page=panel-api-rate-limits-idempotency) page. |
| 401 | `invalid_key` | Missing, malformed, or unknown API key. | Send a live key in `Authorization: Bearer <token>`. |
| 402 | `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 `POST /resellers/{id}/billing/adjust`, then retry. |
| 403 | `insufficient_scope` | The key does not carry `subresellers:write`. | Rotate the key with the correct scopes. |
| 403 | `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. |
| 403 | `sub_reseller_creation_not_allowed` | The caller's group does not have `create_sub_resellers = 1`. | The panel admin turns this on per group in the CMS. |
| 409 | `idempotency_conflict` | The `Idempotency-Key` was reused with a different body. | Generate a new key or resend the original body. |
| 422 | `validation_error` | Username, password, or email fails the validators (length, forbidden characters, invalid email). The message carries the reason. This endpoint returns a human message, not a structured `details.field`. | Fix the offending field and resend. |
| 422 | `insufficient_slots` | A reseller-key caller in `users` mode asked for more `max_users` than they have available. | Free slots on the caller, or lower `max_users`. |
| 422 | `no_sub_reseller_setup` | The panel admin has not configured a sub-reseller package for the caller's group. | The admin configures it in the CMS. |
| 422 | `billing_expires_required` | A reseller-key caller in `users` mode did not pass `billing_expires`. | Add a positive Unix timestamp. |
| 429 | `rate_limited` | The per-key request budget for this minute is spent. | Back off and retry after the minute rolls over. |

## See also

- [Update a reseller](/docs/?page=xai-ref-resellers-update)
- [Adjust reseller billing](/docs/?page=xai-ref-resellers-billing-adjust)
- [Panel API Resellers overview](/docs/?page=xai-ref-resellers-list)
