Update a line

Patch fields on an existing line. This endpoint is a partial update: only the fields you send are written, everything else stays as it was.

Both key types can call it, with different reach. Admin keys get every field in the table below: this is the only endpoint that can flip admin_enabled, set allowed_ips / allowed_ua, or move exp_date freely (including making a line perpetual by sending exp_date: null). It is also where an admin key moves a line onto a different package with package_id, without renewing it and without spending credits. Reseller keys get notes and bouquets and nothing else; any admin-only field in the body returns 403 admin_only_field and nothing is written.

Endpoint

POST https://<your-panel-domain>/panel-api/v1/lines/{id}/update

Authentication

Bearer token in the Authorization header. Admin and reseller keys are both accepted; what each one may write is listed under Request body. See Panel API authentication.

Required scope

lines:write.

Idempotency

Required. Every write must send an Idempotency-Key header. Retrying the same key with the same body returns the original response; a different body returns 409 idempotency_conflict; a missing header returns 400 missing_idempotency_key.

Path parameters

Name Type Description
id int The line id to update.

Request body

Every field is optional; only the fields present in the body are written.

Field Type Required Default Description
notes string no Free-text note on the line, up to 4000 characters. Stored as sent, trimmed of surrounding whitespace, with no prefix added. An empty string clears it. Admin keys write the admin note, reseller keys write their own reseller note, and the two are stored separately. The note is not returned in the line object: this endpoint writes it, no endpoint reads it back.
bouquets int[] no Replaces the line's bouquet assignment. Never empty, and at most 512 ids per call (more returns 422 validation_error with details.field: "bouquets"). Admin keys can send any id that exists in the bouquet catalog; reseller keys can only send a subset of the ids the line already has.
package_id int admin only Move the line onto another package. The package's template is applied in place: max_connections, is_restreamer and, unless you send bouquets yourself, the package's bouquets. Billing is untouched: no renewal, no credits, and exp_date stays where it was. The package must exist and must not be a trial package. Full rules in Changing the package.
password string admin only Set a new password. To generate a random one instead, use reset-password.
exp_date int or null admin only UTC epoch. Sending null explicitly makes the line perpetual. Omitting the field leaves the current expiry alone. Not range-validated here (unlike create).
max_connections int admin only Clamped to [1, 100].
is_restreamer bool admin only Flip the restreamer flag.
enabled bool admin only Reseller-visible toggle.
admin_enabled bool admin only Hard admin override. If false, the line is blocked no matter what enabled says.
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, each capped at 500 chars.

A reseller key that sends any admin-only field gets 403 admin_only_field with the offending names in details.fields, and the whole request is discarded: no partial write happens.

{
  "max_connections": 4,
  "is_restreamer": true,
  "allowed_ips": ["203.0.113.7", "198.51.100.42"],
  "allowed_ua": ["ExampleTV/1.0"]
}

What a reseller key can change

A reseller key sends notes, bouquets, or both:

  • notes is the reseller's own note on the customer. It never crosses over to the admin note and the other way around, and it is not echoed back in the line object.
  • bouquets must be a non-empty subset of the bouquets the line already has. Through this endpoint a reseller can only remove bouquets, for example to downgrade a customer mid-cycle. Any id outside the line's current set returns 422 validation_error with details.invalid_ids.

To add bouquets, or to move the line to a different package, a reseller key calls renew with the new package_id and, optionally, a bouquets subset of that package. Renew is a billing operation: in credits mode it charges a full period at the package's price. There is no free path for a reseller key to widen a line's bouquets.

{
  "notes": "downgraded to the sports-only plan",
  "bouquets": [1, 4]
}

Changing the package

package_id is the mid-cycle upgrade and downgrade path for an admin key. The panel applies the new package's template to the line in place, so the line keeps its id, its username, its password and its expiry. Nothing is renewed and no credits are spent.

What the new package writes:

  • max_connections, clamped to [1, 100].
  • is_restreamer.
  • bouquets, unless you send your own set (see below).

What a package change never touches: exp_date, enabled, admin_enabled, is_trial, member_id, the owner reseller's credit balance, and every other billing field. It is a configuration change, not a renewal. If you also want to move the expiry, send exp_date in the same body, or call renew instead.

Explicit fields win. The package fills in only what you left out of the body, so anything you send yourself takes precedence. {"package_id": 7, "max_connections": 9} gives the line 9 connections plus the is_restreamer flag of package 7. The value you send is the value the line ends up with: there is no arithmetic against the package's own number. Send package_id on its own and every one of those fields comes from the package.

Bouquets follow the new package. Send bouquets and it must be a non-empty subset of the new package's bouquets, with every id still present in the bouquet catalog; ids outside the package come back as 422 validation_error with details.invalid_ids. Leave bouquets out and the line inherits the new package's bouquets in full, which is what makes a downgrade actually remove content. A package that has no bouquets of its own leaves the line with none, so check the package definition before you point a live line at it.

The panel does not store a line's package. A package is a template that gets applied, on create and on update alike; there is no package column on a line. The response does not report which package was applied and neither does retrieve. If your system needs to know which package a line is on, record it on your side once the call returns 200.

{
  "package_id": 7,
  "max_connections": 9,
  "notes": "upgraded to the 4K plan on invoice INV-2026-00915"
}

A reseller key cannot send package_id: it comes back as 403 admin_only_field with package_id in details.fields, and nothing is written. The gate is deliberate. Renewing onto a richer package charges a reseller a full period in credits mode, so an update that did the same thing for free would be a way around that charge. The reseller path to another package stays renew.

Response

200 OK with the full line object reflecting the new field values. bouquets is re-read from the panel after the write, so it shows what was actually stored.

{
  "id": 172504291,
  "username": "u_a1b2c3d4",
  "password": "8ebc79cf",
  "member_id": 100,
  "exp_date": 1788885052,
  "max_connections": 4,
  "is_trial": false,
  "is_restreamer": true,
  "enabled": true,
  "admin_enabled": true,
  "bouquets": [],
  "created_at": 1786206652
}

Examples

cURL

Admin key, operator fields:

curl -X POST https://<your-panel-domain>/panel-api/v1/lines/172504291/update \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: line-172504291-bump-caps-2026-08-08" \
  -d '{
    "max_connections": 4,
    "is_restreamer": true,
    "allowed_ips": ["203.0.113.7", "198.51.100.42"],
    "allowed_ua": ["ExampleTV/1.0"]
  }'

Reseller key, a note plus a narrower bouquet set:

curl -X POST https://<your-panel-domain>/panel-api/v1/lines/172504291/update \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: line-172504291-downgrade-2026-09-09" \
  -d '{
    "notes": "downgraded to the sports-only plan",
    "bouquets": [1, 4]
  }'

Admin key, moving the line onto package 7 and capping it at 9 connections instead of the package's own number:

curl -X POST https://<your-panel-domain>/panel-api/v1/lines/172504291/update \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: line-172504291-upgrade-INV-2026-00915" \
  -d '{
    "package_id": 7,
    "max_connections": 9
  }'

PHP SDK

packageId needs the PHP SDK v1.2.0 or newer. Everything else on this page works from v1.0.0 on.

require __DIR__ . '/api-panel-php-sdk-1.3.0/autoload.php';
use XtreamAI\PanelApi\PanelApiClient;

$client = new PanelApiClient(baseUrl: 'https://<your-panel-domain>', token: '<your-api-key>');
$line = $client->lines->update(
    id:             172504291,
    maxConnections: 4,
    isRestreamer:   true,
    allowedIps:     ['203.0.113.7', '198.51.100.42'],
    allowedUa:      ['ExampleTV/1.0'],
    idempotencyKey: 'line-172504291-bump-caps-2026-08-08',
);
echo $line->maxConnections, PHP_EOL;

// Reseller key: the note and a narrower bouquet set.
$line = $client->lines->update(
    id:             172504291,
    bouquets:       [1, 4],
    notes:          'downgraded to the sports-only plan',
    idempotencyKey: 'line-172504291-downgrade-2026-09-09',
);

// Admin key: move the line onto package 7, overriding its connection count.
$line = $client->lines->update(
    id:             172504291,
    maxConnections: 9,
    idempotencyKey: 'line-172504291-upgrade-INV-2026-00915',
    packageId:      7,
);
echo $line->maxConnections, PHP_EOL;

Python SDK

package_id needs the Python SDK v1.2.0 or newer. Everything else on this page works from v1.0.0 on.

from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(base_url="https://<your-panel-domain>", token="<your-api-key>")
line = client.lines.update(
    id=172504291,
    max_connections=4,
    is_restreamer=True,
    allowed_ips=["203.0.113.7", "198.51.100.42"],
    allowed_ua=["ExampleTV/1.0"],
    idempotency_key="line-172504291-bump-caps-2026-08-08",
)
print(line.max_connections)

# Reseller key: the note and a narrower bouquet set.
line = client.lines.update(
    id=172504291,
    bouquets=[1, 4],
    notes="downgraded to the sports-only plan",
    idempotency_key="line-172504291-downgrade-2026-09-09",
)

# Admin key: move the line onto package 7, overriding its connection count.
line = client.lines.update(
    id=172504291,
    max_connections=9,
    idempotency_key="line-172504291-upgrade-INV-2026-00915",
    package_id=7,
)
print(line.max_connections)

exp_date is three-state: omit the argument to leave the current expiry untouched, pass a UTC epoch to set a new one, or pass null / None to make the line perpetual ($client->lines->update(id: 172504291, expDate: null) / client.lines.update(id=172504291, exp_date=None)).

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.
403 admin_only_field A reseller key sent an admin-only field (package_id, password, exp_date, max_connections, is_restreamer, enabled, admin_enabled, allowed_ips, allowed_ua). details.fields lists them. Nothing was written. Drop those fields, or call from an admin key.
403 insufficient_scope Token lacks lines:write. Issue a key with the scope.
404 not_found The line id does not exist, or a reseller key targeted a line it does not own. Verify the id and ownership (GET /lines/{id}).
409 idempotency_conflict Same key was reused with a different body. Use a fresh key.
409 idempotency_in_flight Same key is still processing. Retry after a moment.
422 validation_error bouquets empty or not an array of ids, bouquet ids the caller may not set (details.invalid_ids), notes longer than 4000 characters, or package_id that is not a positive integer or points at a package that does not exist (details.field: "package_id"). Read details.field and details.invalid_ids. A reseller can only send bouquets the line already has. With package_id, the bouquets you send must be a subset of the new package.
422 trial_package_not_allowed package_id points at a trial package. A line cannot be moved onto a trial package. Pick a non-trial package. Nothing was written.
429 rate_limited The key hit its per-minute cap. Back off and retry after Retry-After seconds.

See also