---
title: "Delete a line, action=delete_line"
description: "Xtream Codes compatibility hard delete of a subscriber line. Removes bouquet assignments and contact-info rows in the same cascade as the CMS. Does not refund credits."
---

# Delete a line

> [!NOTE]
> If you are starting a new integration instead of migrating an existing one, prefer the [native v1 API](/docs/?page=xai-ref-lines-delete) with the [official SDKs](/docs/?page=panel-api-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`](/docs/?page=xai-ref-lines-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 `users` row 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 `credits` billing 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](/docs/?page=panel-api-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](/docs/?page=panel-api-xtream-codes-compatibility#idempotency-via-rid).

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`. |

```json
{
  "status": "STATUS_SUCCESS",
  "data": {
    "id": 172511994,
    "username": "u_a1b2c3d4",
    "deleted": true
  }
}
```

HTTP status is always `200`, even on failure.

## Examples

### cURL

```bash
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)

```php
$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)

```python
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. |

## See also

- [Disable a line, action=disable_line](/docs/?page=xc-ref-lines-disable)
- [Create a line, action=create_line](/docs/?page=xc-ref-lines-create)
- [Get a line, action=get_line](/docs/?page=xc-ref-lines-get)
- [Xtream Codes / XUI.one / OTT Panel compatibility](/docs/?page=panel-api-xtream-codes-compatibility)
- [Native delete endpoint, POST /lines/{id}/delete](/docs/?page=xai-ref-lines-delete)
