---
title: "Create a customer line"
description: "Create a new subscriber line under a package. Returns the full line object with the (possibly autogenerated) username and password."
---

# 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](/docs/?page=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`.

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

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

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

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

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

## See also

- [Update a line](/docs/?page=xai-ref-lines-update)
- [Renew a line](/docs/?page=xai-ref-lines-renew)
- [Delete a line](/docs/?page=xai-ref-lines-delete)
- [Panel API lines overview](/docs/?page=xai-ref-lines-list)
- [Rate limits and idempotency](/docs/?page=panel-api-rate-limits-idempotency)
