Edit 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=edit_line updates a subset of a subscriber line's fields and returns the full re-read Line object. The request is translated to the native POST /panel-api/v1/lines/{id}/update and rewrapped in the classic {"status": "STATUS_SUCCESS", "data": {...}} envelope.
The native update handler writes a fixed set of fields: password, exp_date, max_connections, is_restreamer, enabled, admin_enabled, allowed_ips, allowed_ua (all of them admin-key only), plus the line's bouquets and its notes, which reseller keys may send too. The compat layer forwards only those. Classic XC panels also accepted username, trial, and member_id on edit_line and silently dropped whatever the underlying handler did not process; those three are still ignored here, with one important protection.
If every field in the request is unsupported (for example, only username and trial), the compat layer returns STATUS_INVALID_DATA up front with an explicit message listing the supported fields, so a call that "changes nothing" is loud instead of silent. But if the request mixes supported and unsupported fields, the supported ones are applied and the rest are dropped without warning. Send only fields you know are supported.
Bouquets and notes are applied by this action: bouquets_selected[] maps to the native bouquets, and reseller_notes / admin_notes map to the native notes. A line's username and its trial flag cannot be changed here, on any key.
Endpoint
POST https://<your-panel-domain>/panel-api/xc/{accesscode}/admin/index.php?action=edit_line
Both /admin/index.php and /reseller/index.php are accepted. The admin-versus-reseller decision comes from the key.
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. Pass rid=<unique-per-operation> to opt in. Same rid with the same body replays the original response; same rid with a different body returns idempotency_conflict. Window is 24 hours. See the idempotency section on the compatibility overview.
Request body
The XC dialect posts form-encoded (application/x-www-form-urlencoded). Booleans coerce loosely from strings (see Boolean coercion).
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
int | yes | The line to update. | |
password |
string | admin only | New stream password. | |
exp_date |
int (UTC epoch) | admin only | New expiry. On admin keys it may be null to make the line perpetual; the XC dialect cannot express null because everything is a string, so use the native update endpoint for that specific case. |
|
max_connections |
int | admin only | Concurrent-connection cap. Clamped to [1, 100]. |
|
is_restreamer |
bool | admin only | Whether the line may restream through the panel. | |
enabled |
bool | admin only | Reseller-visible toggle. On reseller keys use the dedicated actions enable_line and disable_line, which accept both key types; sending enabled here on a reseller key is rejected. |
|
admin_enabled |
bool | admin only | Admin override. If set to 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 capped at 500 chars each. | |
bouquets_selected[] |
int[] | no | Aliased to bouquets[]. Replaces the line's bouquet set, so send the full list you want the line to end up with, not just the delta. An empty selection is ignored: a form posted with nothing checked sends bouquets_selected[]=, and rather than failing the whole call the compat layer drops the field, applies the other fields you sent and leaves the line's bouquets untouched. If that empty selection is the only field in the request there is nothing to edit and the call returns STATUS_INVALID_DATA (see below). Admin keys may send any bouquet id that exists on the panel; reseller keys may only send a subset of the ids the line already has. At most 512 ids per call. |
|
reseller_notes / admin_notes |
string | no | Both alias the native notes. Stored verbatim, trimmed, up to 4000 characters. An empty string clears the note. If you send both, admin_notes wins. |
|
rid |
string | no | Idempotency identifier. |
At least one editable field is required. A request that sends only fields the native handler ignores (for example, username, trial, member_id), or only an empty bouquets_selected[], returns STATUS_INVALID_DATA with the message edit_line: none of the provided fields is editable by this API. Supported: password, exp_date, max_connections, is_restreamer, enabled, admin_enabled, allowed_ips, allowed_ua, bouquets_selected, reseller_notes, admin_notes.
Reseller keys
A reseller key may send only bouquets_selected[] and reseller_notes / admin_notes on this action. Any of password, exp_date, max_connections, is_restreamer, enabled, admin_enabled, allowed_ips, or allowed_ua in the body returns STATUS_NO_PERMISSIONS with error: "admin_only_field" and data.details.fields listing the offending names. Nothing at all is written in that case, so a mixed body never lands half-applied.
A reseller's bouquets_selected[] can only remove: every id must already be on the line, and the resulting list must not be empty. Ids outside the line's current set come back as STATUS_INVALID_DATA with data.details.invalid_ids. To widen a reseller line's bouquets, use the native POST /panel-api/v1/lines/{id}/renew with a bouquets list drawn from the package. Renew is a billing operation, not a bouquet editor: in credits mode it charges a full period at the package's price, and in every mode it moves the line's expiry date forward by the package's official duration.
Notes are per role: an admin key writes the admin note on the line, a reseller key its own reseller note. The two live side by side and never overwrite each other.
Response
data is the full Line object, re-read from the database after the update. Same shape used by action=get_line. Updated bouquets show up in the bouquets array; the note does not appear anywhere in the object, in this action or in get_line, so keep your own copy if your billing system needs to display it.
{
"status": "STATUS_SUCCESS",
"data": {
"id": 172511994,
"username": "u_a1b2c3d4",
"password": "Sup3rSecret1",
"member_id": 100,
"exp_date": 1817743138,
"max_connections": 3,
"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.
Examples
cURL
curl -X POST "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=edit_line" \
-d "id=172511994" \
-d "max_connections=3" \
-d "enabled=true" \
-d "rid=edit-172511994-2026-01-15"
Bouquets and notes travel on the same action, and both work on a reseller key:
curl -X POST "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=edit_line" \
-d "id=172511994" \
-d "bouquets_selected[]=2" -d "bouquets_selected[]=4" \
-d "reseller_notes=Trimmed to the sports bouquets, invoice INV-42" \
-d "rid=edit-172511994-bouquets-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' => 'edit_line']);
$body = http_build_query([
'id' => 172511994,
'max_connections' => 3,
'enabled' => 'true',
'rid' => 'edit-172511994-' . 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'] ?? 'edit_line failed');
}
$line = $resp['data'];
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": "edit_line"},
data={
"id": 172511994,
"max_connections": 3,
"enabled": "true",
"rid": f"edit-172511994-{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", "edit_line failed"))
line = body["data"]
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 |
id is missing or empty; or the request sent only fields the native handler ignores (message names the supported set). |
Send id plus at least one supported field. |
STATUS_INVALID_DATA |
validation_error |
bouquets_selected[] is not a list of ids, carries ids the caller may not set on this line (data.details.invalid_ids names them), or holds more than 512 ids. An empty selection never reaches the panel: the compat layer drops it, and the call only fails if there was nothing else to edit. |
Send ids the line may have. On a reseller key, restrict them to ids the line already has. |
STATUS_INVALID_DATA |
validation_error |
The note is longer than 4000 characters or is not a string (data.details.field is notes). |
Shorten the note. |
STATUS_FAILURE |
not_found |
The id does not exist, or a reseller key targeted a line owned by another reseller. | Verify the id and ownership. |
STATUS_NO_PERMISSIONS |
admin_only_field |
A reseller key sent any of password, exp_date, max_connections, is_restreamer, enabled, admin_enabled, allowed_ips, allowed_ua. data.details.fields lists them and nothing was written. |
Drop those fields (a reseller key may send only bouquets_selected[] and notes), 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_FAILURE |
idempotency_conflict |
Same rid reused with a different body. |
Pick a new rid, or send the original body. |
STATUS_FAILURE |
idempotency_in_flight |
Same rid is still processing on another request. |
Retry after a moment. |
STATUS_FAILURE |
invalid_key |
Token missing, unknown, disabled, expired, or IP not in 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 |
Panel API is switched off. | Contact the panel admin. |