Create a sub-reseller, POST /ext/user/create
If you are starting a new integration instead of migrating, prefer the native v1 API with the official SDKs. The OneStream dialect exists to let existing OneStream code point at Xtream AI with only a base URL change.
Create a sub-reseller. Admin keys can create sub-resellers freely. Reseller keys can create sub-resellers only if their member group has create_sub_resellers = 1, and the panel operator has configured a sub-reseller setup for that group. On reseller keys the new sub-reseller inherits the parent's billing mode (credits or users) and is charged the group's create_sub_resellers_price in credits mode.
The OneStream body shape is honored, including the optional password_confirmation field. If your client sends both password and password_confirmation and they do not match, the request is rejected before any row is written.
Endpoint
POST https://<your-panel-domain>/panel-api/onestream/ext/user/create
Authentication
X-Api-Key. X-Auth-User and Authorization: Bearer also accepted. See Authentication.
Required scope
resellers:write.
Idempotency
Optional but recommended. A retried rid returns the cached response instead of creating a second sub-reseller. In credits mode this is what prevents a network retry from charging the caller twice.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name (or username) |
string | yes | Login name for the new sub-reseller. Must be unique panel-wide. | |
email |
string | no | Contact email stored on the reg-user row. | |
password |
string | yes | Plain-text password. Stored as the panel stores every user credential. | |
password_confirmation |
string | no | If sent, must equal password. If omitted, the check is skipped. |
|
credits |
number | no | 0 | Initial credit balance. On reseller keys this field is ignored: the child is created with 0 credits and the caller must top up separately using POST /ext/user/{id}/credit. |
notes |
string | no | Free-form note stored on the reg-user row. | |
member_group_id |
int | admin, yes | Reseller group the new user joins. Admin keys must pick a group with is_reseller = 1. On reseller keys the group is forced to the value the panel operator configured for the parent's group, and any value sent in the body is discarded. |
|
rid |
string | no | Idempotency key. |
The OneStream API does not expose member_group_id in its documented shape. If you are migrating a client that never sent this field, add it now for admin keys; a plain create without a group name is rejected with 422 Member group not found.
{
"name": "reseller_billing",
"email": "reseller_billing@example.com",
"password": "topsecret",
"password_confirmation": "topsecret",
"credits": 100,
"notes": "created by billing system",
"member_group_id": 4,
"rid": "user-create-billing-1"
}
Response
200 OK with the new sub-reseller's internal integer id and the echoed rid. Sub-resellers do not use the opaque UUID format that lines use, because the integer id is safe to expose (it is not a target of enumeration attacks the way live subscriber lines are).
{
"id": 5555,
"rid": "user-create-billing-1"
}
Store the returned id next to your own billing record; it is the value you pass to POST /ext/user/{id}/update and POST /ext/user/{id}/credit later on.
Examples
cURL
curl -X POST https://<your-panel-domain>/panel-api/onestream/ext/user/create \
-H "X-Api-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "reseller_billing",
"email": "reseller_billing@example.com",
"password": "topsecret",
"password_confirmation": "topsecret",
"credits": 100,
"notes": "created by billing system",
"member_group_id": 4,
"rid": "user-create-billing-1"
}'
PHP raw
$ch = curl_init('https://<your-panel-domain>/panel-api/onestream/ext/user/create');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-Api-Key: <your-api-key>',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'reseller_billing',
'email' => 'reseller_billing@example.com',
'password' => 'topsecret',
'password_confirmation' => 'topsecret',
'credits' => 100,
'notes' => 'created by billing system',
'member_group_id' => 4,
'rid' => 'user-create-billing-1',
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python raw
import requests
r = requests.post(
"https://<your-panel-domain>/panel-api/onestream/ext/user/create",
headers={"X-Api-Key": "<your-api-key>"},
json={
"name": "reseller_billing",
"email": "reseller_billing@example.com",
"password": "topsecret",
"password_confirmation": "topsecret",
"credits": 100,
"notes": "created by billing system",
"member_group_id": 4,
"rid": "user-create-billing-1",
},
timeout=30,
)
r.raise_for_status()
print(r.json())
Errors
| HTTP | Error slug (or message) | When it happens | How to fix |
|---|---|---|---|
| 401 | Invalid API key |
Token is unknown, expired, disabled, or deleted. | Check the token or issue a new one. |
| 402 | Insufficient credits balance |
Reseller in credits mode lacks the credits configured as create_sub_resellers_price. |
Top up credits before creating. |
| 403 | insufficient_scope |
Token lacks resellers:write. |
Issue a key with the scope. |
| 403 | create_sub_resellers_not_allowed |
Reseller key belongs to a group with create_sub_resellers = 0, or the panel operator has not configured a sub-reseller setup for the group. |
Ask the panel operator to enable sub-reseller creation for the group. |
| 409 | Transaction already processed |
Same rid was reused with a different body. |
Use a fresh rid, or replay with the exact original body. |
| 422 | password and password_confirmation do not match |
Both fields were sent and their values differ. | Send them equal, or omit password_confirmation to skip the check. |
| 422 | Member group not found |
Admin key did not pass member_group_id, or the value points to a non-reseller group. |
Pass a valid reseller group id (is_reseller = 1). |
| 422 | validation_error |
Missing name or password, username collision, malformed email. |
See details.field in the response. |
| 429 | Rate limit exceeded |
The key hit its per-minute cap. | Back off and retry after Retry-After seconds. |
| 501 | not_implemented |
A GET request was sent to this URL. |
Only POST is accepted. |