Delete 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=delete_line removes a subscriber line permanently. The request is translated to the native POST /panel-api/v1/lines/{id}/delete and rewrapped in the classic {"status": "STATUS_SUCCESS", "data": {...}} envelope.
The delete cascade is the same one the CMS uses:
- Bouquet assignments (
user_bouquets) are removed. - Contact info rows (
admin_user_contact_info,reseller_user_contact_info) are removed. - The
usersrow is deleted.
Delete does not:
- Refund credits already paid to create or extend the line. This matches the classic panels' behavior; deletion is not a compensating transaction. In
creditsbilling mode, the reseller keeps the debit. - Preserve the row. There is no soft delete. Once the delete succeeds, the id will not resurface, and re-creating the line requires a fresh
create_line(with a new autogenerated id).
In users billing mode, the slot the deleted line occupied is freed. The slot count is derived from the row's existence, so removing the row automatically decrements the reseller's active-user count.
Reseller keys need their member group to have delete_users=1. Groups with delete_users=0 receive STATUS_NO_PERMISSIONS with delete_not_allowed; the panel's own UI hides the delete button for those groups, and the API enforces the same rule server-side so a fabricated POST cannot bypass it.
The endpoint verifies the row disappeared before reporting success. If the underlying delete transaction fails mid-way (deadlock, permissions, socket hiccup), the API returns STATUS_FAILURE with delete_failed instead of claiming success on a line that is still in the database.
Endpoint
POST https://<your-panel-domain>/panel-api/xc/{accesscode}/admin/index.php?action=delete_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 but recommended. Pass rid=<unique-per-operation> in the query string or POST body. Same rid with same body replays the original response instead of running a second delete. Same rid with a different body returns idempotency_conflict. Window is 24 hours. See the idempotency section.
The natural failure mode without rid is a retry after a network timeout: the first delete may have succeeded, and the retry sees a not_found and treats it as a bug. With rid the retry replays the original success.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
int | yes | The line to delete. | |
rid |
string | no | Idempotency identifier. |
Response
data is a small confirmation object.
| Field | Type | Description |
|---|---|---|
id |
int | The id of the deleted line. |
username |
string | The username the line had at the moment of deletion. Useful for reconciliation logs. |
deleted |
bool | Always true when the response is STATUS_SUCCESS. |
{
"status": "STATUS_SUCCESS",
"data": {
"id": 172511994,
"username": "u_a1b2c3d4",
"deleted": true
}
}
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=delete_line" \
-d "id=172511994" \
-d "rid=del-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' => 'delete_line']);
$body = http_build_query([
'id' => 172511994,
'rid' => 'del-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'] ?? 'delete_line failed');
}
// $resp['data']['deleted'] === true
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": "delete_line"},
data={
"id": 172511994,
"rid": f"del-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", "delete_line failed"))
assert body["data"]["deleted"] is True
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. data.details.field is "id". |
Send a numeric id. |
STATUS_FAILURE |
not_found |
The id does not exist (already deleted, or never existed), or a reseller key targeted a line owned by another reseller. | Verify the id and ownership. On idempotent retries, use rid to replay the original success instead of hitting not_found. |
STATUS_NO_PERMISSIONS |
delete_not_allowed |
Reseller's member group has delete_users=0. |
Ask the panel admin to enable line deletion for the group, or use an admin key. |
STATUS_NO_PERMISSIONS |
insufficient_scope |
The key does not have lines:write. |
Grant the scope. |
STATUS_FAILURE |
delete_failed |
The delete cascade started but the users row was still present when the API re-checked. The underlying UPDATE stream is not transactional, so a mid-way failure (permissions, deadlock, socket hiccup) leaves the row alive. |
Retry. If the failure persists, check MySQL error logs and the reseller-user-contact-info table for orphan rows. |
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. |
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. |