---
title: "Create a line, action=create_line"
description: "Xtream Codes compatibility create for a subscriber line. Translates the classic parameter set (package, bouquets_selected[], trial, ...) to the native handler and rewraps the response in the STATUS_SUCCESS envelope."
---

# Create a line

> [!NOTE]
> If you are starting a new integration instead of migrating an existing one, prefer the [native v1 API](/docs/?page=xai-ref-lines-create) with the [official SDKs](/docs/?page=panel-api-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`](/docs/?page=xai-ref-lines-create). 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](/docs/?page=panel-api-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](/docs/?page=panel-api-xtream-codes-compatibility#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`](/docs/?page=xc-ref-lines-get).

```json
{
  "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

```bash
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)

```php
$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)

```python
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

- [Edit a line, action=edit_line](/docs/?page=xc-ref-lines-edit)
- [Extend a line, action=extend_line](/docs/?page=xc-ref-lines-extend)
- [Delete a line, action=delete_line](/docs/?page=xc-ref-lines-delete)
- [Xtream Codes / XUI.one / OTT Panel compatibility](/docs/?page=panel-api-xtream-codes-compatibility)
- [Native create endpoint, POST /lines](/docs/?page=xai-ref-lines-create)
