Create a customer line
Provision a new subscriber account on the panel. A line is a username plus password pair that end customers plug into an IPTV app. Every line belongs to an owner (an admin or a reseller) and inherits its duration, connection cap, default bouquets, and trial flag from the package_id you pick.
On a reseller key, credits are deducted atomically before the line row is written. If the row insert fails (for example, a username collision), the credits are refunded automatically. Admin keys skip the billing checks entirely.
Endpoint
POST https://<your-panel-domain>/panel-api/v1/lines
Authentication
Send your API key as a Bearer token in the Authorization header. See Panel API authentication for how to issue one.
Required scope
lines:write.
Idempotency
Required. Every write must send an Idempotency-Key header (any string up to 128 bytes, one per business intent). Retrying the same key with the same body returns the original response. Retrying with a different body returns 409 idempotency_conflict. Requests without the header are rejected with 400 missing_idempotency_key.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
package_id |
int | yes | Package the line inherits from. Must exist. | |
member_id |
int | admin only, required | Owner reseller id. Reseller keys MUST NOT send this; the owner is forced to the key's own reg user id, and passing the field returns 403 admin_only_field. |
|
username |
string | no | autogenerated u_<8hex> (or trial_<8hex> for trials) |
Must be unique panel-wide. |
password |
string | no | autogenerated (8 hex chars) | Reseller keys whose member 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. |
is_trial |
bool | no | package default | Only valid on a trial package; sending true on a non-trial package returns 422 trial_flag_requires_trial_package. |
email |
string | no | Stored on the contact-info row (reseller_user_contact_info for resellers, admin_user_contact_info for admins). |
|
notes |
string | no | Same row as email. |
|
exp_date |
int (UTC epoch) | admin only | package duration | Must be in the future and within 5 years from now. |
max_connections |
int | admin only | package default | Clamped to [1, 100]. |
is_restreamer |
bool | admin only | package default | |
allowed_ips |
string[] | admin only | [] |
IPv4 allow-list, up to 50 entries. Invalid entries drop silently. |
allowed_ua |
string[] | admin only | [] |
User-Agent allow-list, up to 50 entries capped at 500 chars each. |
is_isplock |
bool | admin only | false |
Admin-only fields sent by a reseller key trigger 403 admin_only_field with the offending names in details.fields.
{
"package_id": 42,
"member_id": 100,
"username": "johndoe",
"password": "s3cret!",
"bouquets": [1, 4, 9],
"email": "johndoe@example.com",
"notes": "created from invoice INV-42"
}
Response
201 Created with the full line object. Password is returned in clear because the panel stores it as plaintext by design (users need it for login).
{
"id": 172504291,
"username": "u_a1b2c3d4",
"password": "4102fec6",
"member_id": 100,
"exp_date": 1788885051,
"max_connections": 1,
"is_trial": false,
"is_restreamer": false,
"enabled": true,
"admin_enabled": true,
"bouquets": [],
"created_at": 1786206651
}
Examples
cURL
curl -X POST https://<your-panel-domain>/panel-api/v1/lines \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-INV-2026-00814" \
-d '{
"package_id": 42,
"member_id": 100,
"email": "johndoe@example.com",
"notes": "created from invoice INV-42"
}'
PHP SDK
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>');
$line = $client->lines->create(
packageId: 42,
memberId: 100,
email: 'johndoe@example.com',
notes: 'created from invoice INV-42',
idempotencyKey: 'invoice-INV-2026-00814',
);
echo $line->id, ' ', $line->username, PHP_EOL;
Python SDK
from xtream_ai_panel_api import PanelApiClient
client = PanelApiClient(base_url="https://<your-panel-domain>", token="<your-api-key>")
line = client.lines.create(
package_id=42,
member_id=100,
email="johndoe@example.com",
notes="created from invoice INV-42",
idempotency_key="invoice-INV-2026-00814",
)
print(line.id, line.username)
Errors
| HTTP | Error slug | When it happens | How to fix |
|---|---|---|---|
| 400 | missing_idempotency_key |
The Idempotency-Key header was not sent. |
Send a per-intent key on every write. |
| 401 | invalid_key |
Token is unknown, expired, disabled, or deleted. | Check the token or issue a new one. |
| 402 | billing_expired |
Reseller subscription has expired. | Extend the reseller before creating lines. |
| 402 | trial_quota_exceeded |
Reseller hit the trial-creation cap for the current window. | Wait for the window to roll, or raise the group cap. |
| 402 | slot_limit_exceeded |
Reseller (users billing mode) would exceed its own user cap. | Delete unused lines or raise the cap. |
| 402 | ancestor_cap_reached |
A parent reseller's cap would be exceeded. | Raise the parent cap. |
| 402 | insufficient_credits |
Reseller (credits mode) lacks credits for the package. | Top up credits, or pick a cheaper package. |
| 403 | admin_only_field |
Reseller key sent one of member_id, exp_date, max_connections, is_restreamer, allowed_ips, allowed_ua, is_isplock. |
Remove the field(s). details.fields lists them. |
| 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. |
| 403 | isplock_not_allowed |
Reseller group has edit_isplock=0. |
Omit is_isplock. |
| 403 | insufficient_scope |
Token lacks lines:write. |
Issue a key with the scope. |
| 409 | idempotency_conflict |
Same key was reused with a different body. | Use a fresh key. |
| 409 | idempotency_in_flight |
Same key is still processing on another request. | Retry after a moment. |
| 422 | validation_error |
Missing / malformed field, unknown package_id, unknown member_id, username collision, bouquet not accessible, invalid exp_date. |
See details.field (or details.invalid_ids for bouquets) in the response. |
| 422 | trial_flag_requires_trial_package |
is_trial=true was sent for a non-trial package. |
Remove is_trial, or pick a trial package. |
| 429 | rate_limited |
The key hit its per-minute cap. | Back off and retry after Retry-After seconds. |