Create a line, POST /ext/line/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. New integrations get typed models, automatic retries, and richer error surfaces.
Create a subscriber line under a package. The classic OneStream body shape is honored: package, username, password, bouquets, reseller_notes, max_connections, and rid. If the caller is a reseller key in credits mode the package cost is deducted atomically before the line row is written. Admin keys must pass member_id to say which reseller owns the new line.
The response returns the opaque line UUID that OneStream integrators already expect. Store it next to your own record. See OneStream compatibility for how the UUID is generated and why it is stable across time.
Endpoint
POST https://<your-panel-domain>/panel-api/onestream/ext/line/create
Authentication
Send the token as X-Api-Key. The legacy OneStream header X-Auth-User is accepted as an alias. Authorization: Bearer <token> also works for clients that have moved to standard Bearer auth. See Authentication.
Required scope
lines:write.
Idempotency
Optional but strongly recommended. Pass a unique rid (any string up to 255 characters) in the JSON body. Retrying the same rid with the same body returns the original response verbatim. Retrying the same rid with a different body returns 409 Transaction already processed. If rid is omitted the request still executes, but a naive retry after a timeout would create a second line.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
package (or package_id) |
int | yes | Package the line inherits from. Must exist. Reseller keys must have access. | |
member_id |
int | admin only, required | Owner reseller id. Reseller keys must not send this and receive 403 admin_only_field if they do. |
|
username |
string | no | autogenerated u_<8hex> |
Must be unique panel-wide. |
password |
string | no | autogenerated (8 hex chars) | Reseller keys whose group has allow_change_pass=0 cannot set this and receive 403 password_change_not_allowed. |
bouquets |
int[] | no | package default | On reseller keys every id must be visible to the reseller's group. |
reseller_notes |
string | no | Free-form note stored on the line. Translates to notes internally. |
|
max_connections |
int | admin only | package default | Clamped to [1, 100]. |
mac_addr |
string | no | Accepted for backwards compatibility with clients that always send it. Silently ignored (the product does not provision physical devices). | |
rid |
string | no | Idempotency key. See above. |
{
"package": 42,
"member_id": 100,
"username": "reseller1_20260808",
"password": "s3cret-pw",
"bouquets": [1, 2],
"reseller_notes": "created from invoice INV-42",
"rid": "invoice-INV-2026-00814"
}
Response
200 OK with a compact object. The full line record is not returned here; if you need every field, call GET /ext/lines?username=<name> right after the create, or use the native create endpoint which returns the complete row.
{
"line_id": "b32c1a04-11ea-4c67-8fd1-0001000000f1",
"expire_at": "2026-09-08T00:00:00+00:00",
"transaction_amount": 0,
"rid": "invoice-INV-2026-00814"
}
expire_at is ISO 8601 in UTC, derived from the package's official duration added to the current instant. transaction_amount is the credits that were debited (0 for admin keys and users-mode resellers). rid echoes back only if it was sent in the request.
Examples
cURL
curl -X POST https://<your-panel-domain>/panel-api/onestream/ext/line/create \
-H "X-Api-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"package": 42,
"member_id": 100,
"username": "reseller1_20260808",
"password": "s3cret-pw",
"reseller_notes": "invoice INV-42",
"rid": "invoice-INV-2026-00814"
}'
PHP raw
$ch = curl_init('https://<your-panel-domain>/panel-api/onestream/ext/line/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([
'package' => 42,
'member_id' => 100,
'username' => 'reseller1_20260808',
'password' => 's3cret-pw',
'reseller_notes' => 'invoice INV-42',
'rid' => 'invoice-INV-2026-00814',
]),
]);
$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/line/create",
headers={"X-Api-Key": "<your-api-key>"},
json={
"package": 42,
"member_id": 100,
"username": "reseller1_20260808",
"password": "s3cret-pw",
"reseller_notes": "invoice INV-42",
"rid": "invoice-INV-2026-00814",
},
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 credits for the package. | Top up credits, or pick a cheaper package. |
| 403 | admin_only_field |
Reseller key sent member_id. details.fields lists the offending name. |
Remove member_id. Reseller keys always create lines under themselves. |
| 403 | insufficient_scope |
Token lacks lines:write. |
Issue a key with the scope. |
| 403 | package_not_accessible |
Reseller cannot sell from that package. | Use a package inside the reseller's member group. |
| 403 | password_change_not_allowed |
Reseller group has allow_change_pass=0. |
Omit password and let the API autogenerate it. |
| 409 | Transaction already processed |
Same rid was reused with a different body. |
Use a fresh rid, or replay with the exact original body. |
| 422 | validation_error |
Missing package, admin key without member_id, unknown package, unknown member_id, username collision, bouquet not accessible. |
See details.field (or details.invalid_ids for bouquets). |
| 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. |