Create a line

If you are starting a new integration instead of migrating an existing one, prefer the native v1 API with the official SDKs. The XC dialect keeps compatibility with legacy tooling; the native dialect gives you typed models, header-based idempotency, and structured HTTP status codes.

action=create_line provisions a new subscriber line under a package and returns the full Line object. 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 you pick.

The compatibility layer maps the classic XC parameter names to the native handler's fields (package -> package_id, bouquets_selected[] -> bouquets, trial -> is_trial, reseller_notes/admin_notes -> notes), then delegates to POST /panel-api/v1/lines. Billing, quota checks, credit deduction and refund on failure all happen inside the native handler, unchanged.

Autogenerated username and password are returned in clear in the response, so a bot can hand them to the customer directly.

Endpoint

POST https://<your-panel-domain>/panel-api/xc/{accesscode}/admin/index.php?action=create_line

Both /admin/index.php and /reseller/index.php are accepted. The admin-versus-reseller decision comes from the key, not the path.

GET is also accepted for backward compatibility with classic XUI.one integrations, but write actions over GET leave api_key= in browser history, referrer headers, and any intermediary access log. Use POST for writes.

Authentication

Any one of these three forms:

  • ?api_key=<your-api-key> in the query string.
  • api_key=<your-api-key> in the POST body form field.
  • Authorization: Bearer <your-api-key> HTTP header.

See Authentication.

Required scope

lines:write.

Idempotency

Optional in the XC dialect (the classic panels had none). Pass rid=<unique-per-operation> in the query string or in the POST body to opt in.

  • Same rid, same body: the original response is replayed and nothing runs twice.
  • Same rid, different body: STATUS_FAILURE with error: "idempotency_conflict".
  • Windows for 24 hours.
  • Values up to 255 characters. Use a UUID, or a business id.
  • If you omit rid, the compat layer synthesizes an internal single-shot key per request. The request still goes through the same pipeline (audit, credit deduction with refund on failure), but a blind retry after a network timeout is no longer idempotent, and you may create the line twice.

Request body

The XC dialect posts form-encoded (application/x-www-form-urlencoded), and every value arrives as a string. See Boolean coercion for the exact rules on strings like "true", "false", "0", "1".

Field Type Required Default Description
package int yes Package the line inherits from. Must exist. Aliased to package_id internally; you may send package_id directly instead.
member_id int admin only, required Owner reseller's reg_user_id. Admin keys must supply this; the line will belong to that reseller. Reseller keys must NOT send this; the field is admin-only and returns STATUS_NO_PERMISSIONS (admin_only_field).
username string no autogenerated u_<8hex> (or trial_<8hex>) 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 STATUS_NO_PERMISSIONS with password_change_not_allowed.
bouquets_selected[] int[] no package default Aliased to bouquets[]. On reseller keys every id must be visible to the reseller's group.
trial bool no package default Aliased to is_trial. Only valid on a trial package; sending true on a non-trial package returns STATUS_INVALID_DATA with trial_flag_requires_trial_package.
reseller_notes string no Aliased to notes and stored on the contact-info row.
admin_notes string no Same field as reseller_notes. Whichever you send lands in the same slot.
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.
is_isplock bool admin only false Requires the reseller group to have edit_isplock=1 when set by a reseller (returns STATUS_NO_PERMISSIONS otherwise).
rid string no Idempotency identifier. See the section above.

Admin-only fields sent by a reseller key trigger STATUS_NO_PERMISSIONS with error: "admin_only_field" and data.details.fields listing the offending names.

Response

data is the full Line object. Same shape used by action=get_line.

{
  "status": "STATUS_SUCCESS",
  "data": {
    "id": 172511994,
    "username": "u_a1b2c3d4",
    "password": "4102fec6",
    "member_id": 100,
    "exp_date": 1817743138,
    "max_connections": 1,
    "is_trial": false,
    "is_restreamer": false,
    "enabled": true,
    "admin_enabled": true,
    "bouquets": [2, 4],
    "created_at": 1786207138
  }
}

HTTP status is always 200, even on failure. Branch on status in the envelope.

Examples

cURL

curl -X POST "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=create_line" \
  -d "package=42" \
  -d "member_id=100" \
  -d "username=johndoe" \
  -d "password=SuperSecret1" \
  -d "bouquets_selected[]=1" -d "bouquets_selected[]=4" -d "bouquets_selected[]=9" \
  -d "trial=false" \
  -d "rid=create-johndoe-2026-01-15"

PHP (raw HTTP)

$url = 'https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php'
     . '?' . http_build_query(['api_key' => '<your-api-key>', 'action' => 'create_line']);
$body = http_build_query([
    'package'           => 42,
    'member_id'         => 100,
    'username'          => 'johndoe',
    'password'          => 'SuperSecret1',
    'bouquets_selected' => [1, 4, 9],
    'trial'             => 'false',
    'rid'               => 'create-johndoe-' . bin2hex(random_bytes(8)),
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);
if (($resp['status'] ?? '') !== 'STATUS_SUCCESS') {
    throw new RuntimeException($resp['data']['message'] ?? 'create_line failed');
}
$lineId = $resp['data']['id'];

Python (raw HTTP)

import requests, secrets

r = requests.post(
    "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php",
    params={"api_key": "<your-api-key>", "action": "create_line"},
    data={
        "package":              42,
        "member_id":            100,
        "username":             "johndoe",
        "password":             "SuperSecret1",
        "bouquets_selected[]":  [1, 4, 9],
        "trial":                "false",
        "rid":                  f"create-johndoe-{secrets.token_hex(8)}",
    },
    timeout=30,
)
r.raise_for_status()
body = r.json()
if body.get("status") != "STATUS_SUCCESS":
    raise RuntimeError(body["data"].get("message", "create_line failed"))
line_id = body["data"]["id"]

Errors

Response is always HTTP 200. Branch on status, then data.error.

status Error slug When it happens How to fix
STATUS_INVALID_DATA validation_error Missing or malformed field: package not sent (data.details.field is package_id); admin key without member_id; unknown member_id; unknown package; username collision (Username already exist); bouquet id not accessible; exp_date outside the 5-year window. Read data.details.field (or data.details.invalid_ids for bouquets) and fix the input.
STATUS_INVALID_DATA trial_flag_requires_trial_package trial=true was sent for a non-trial package. Remove trial, or pick a trial package.
STATUS_NO_PERMISSIONS admin_only_field A reseller key sent one of member_id, exp_date, max_connections, is_restreamer, allowed_ips, allowed_ua, or is_isplock. data.details.fields lists which ones tripped the guard. Remove those fields on reseller keys, or issue an admin key for this integration.
STATUS_NO_PERMISSIONS insufficient_scope The key does not have lines:write. Grant the scope, or issue a new key.
STATUS_NO_PERMISSIONS password_change_not_allowed Reseller group has allow_change_pass=0 and the request set password. Omit password; the panel will autogenerate one.
STATUS_NO_PERMISSIONS isplock_not_allowed Reseller group has edit_isplock=0 and the request set is_isplock=true. Omit is_isplock.
STATUS_INVALID_PACKAGE package_not_accessible The reseller cannot sell from that package. Use a package inside the reseller's member group.
STATUS_FAILURE billing_expired Reseller subscription has expired. Extend the reseller subscription before creating lines.
STATUS_INSUFFICIENT_CREDITS insufficient_credits Reseller in credits mode lacks credits for the package cost. Top up credits, or pick a cheaper package.
STATUS_INSUFFICIENT_CREDITS slot_limit_exceeded Reseller in users mode has reached its slot cap. Delete unused lines, or raise the cap.
STATUS_INSUFFICIENT_CREDITS trial_quota_exceeded Reseller hit the trial-creation cap for the current window (per group config). Wait for the window to roll, or raise the group cap.
STATUS_FAILURE idempotency_conflict The same rid was reused with a different body. Pick a new rid, or send the original body.
STATUS_FAILURE idempotency_in_flight The same rid is still processing on another request. Retry after a moment.
STATUS_FAILURE invalid_key The token is missing, unknown, disabled, expired, or the source IP is not in the key's allow-list. Verify the token and the IP allow-list.
STATUS_FAILURE rate_limited Per-minute cap or per-IP cap exceeded. Back off.
STATUS_FAILURE api_disabled An admin has switched the Panel API off for this panel. Contact the panel admin.

See also