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:
notesis 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.bouquetsmust 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 returns422 validation_errorwithdetails.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_dateis three-state: omit the argument to leave the current expiry untouched, pass a UTC epoch to set a new one, or passnull/Noneto 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
- Create a line
- Renew a line. Extends the expiry, and is the only package change a reseller key can make.
- Enable a line
- Disable a line
- Panel API lines overview