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 only writes a fixed set of fields: password, exp_date, max_connections, is_restreamer, enabled, admin_enabled (admin key only), allowed_ips, allowed_ua. Classic XC panels accepted many more fields (bouquets_selected[], username, notes, is_trial, ...) and silently dropped anything the underlying handler did not process. The compat layer keeps that fidelity: it forwards only the supported fields to the native handler. Fields the native handler does not accept are ignored, with one important protection.
If every field in the request is unsupported (for example, only bouquets_selected[]), 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, username, notes, and is_trial cannot be changed through this endpoint (or through the native update endpoint). The current workaround for changing bouquets is delete + recreate.
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 | no | New stream password. Reseller keys whose member group has allow_change_pass=0 cannot set this. |
|
exp_date |
int (UTC epoch) | no | 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 | no | Concurrent-connection cap. Clamped to [1, 100]. |
|
is_restreamer |
bool | no | Whether the line may restream through the panel. | |
enabled |
bool | no | Reseller-visible toggle. The dedicated actions enable_line and disable_line are equivalent shortcuts. |
|
admin_enabled |
bool | admin only | Admin override. If set to false, the line is blocked no matter what enabled says. |
|
allowed_ips[] |
string[] | no | IPv4 allow-list, up to 50 entries. Invalid entries drop silently. | |
allowed_ua[] |
string[] | no | User-Agent allow-list, up to 50 entries capped at 500 chars each. | |
rid |
string | no | Idempotency identifier. |
At least one editable field is required. A request that sends only fields the native handler ignores (for example, bouquets_selected[], username, notes, is_trial) 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, allowed_ips, allowed_ua.
Response
data is the full Line object, re-read from the database after the update. Same shape used by action=get_line.
{
"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"
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_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 admin_enabled. |
Remove admin_enabled, 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. |
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. |