Extend 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=extend_line pushes a line's exp_date forward by the official duration of a package. The request is translated to the native POST /panel-api/v1/lines/{id}/renew and rewrapped in the classic envelope.
The new expiry is max(current_exp_date, now) + package.official_duration. If the line already expired, the countdown restarts from now. If it is still active, the extension is added on top of the current expiry so no days are lost.
Extending also reactivates the line: admin_enabled and enabled are both set to true in the same statement, so an extend brings back a previously disabled or admin-blocked line without a second call.
Billing:
- Reseller in credits mode deducts
package.official_creditsbefore the update. Failure to deduct returnsSTATUS_INSUFFICIENT_CREDITS. If the update fails after the deduction, the credits are refunded. - Reseller in users mode extends for free (the slot is already accounted for as long as the line exists).
- Admin keys skip the billing checks entirely.
Perpetual lines (exp_date == null) cannot be extended. Extending would set an expiry and effectively degrade the line, so the endpoint returns STATUS_INVALID_DATA with line_has_no_expiry.
Endpoint
POST https://<your-panel-domain>/panel-api/xc/{accesscode}/admin/index.php?action=extend_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 in the XC dialect, but strongly recommended for extensions because they are the classic double-charge risk on network retries. Pass rid=<unique-per-operation> in the query string or POST body. Same rid with 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
int | yes | The line to extend. | |
package |
int | yes | Package to extend with. Must exist, must not be a trial package. Aliased to package_id; you may send package_id directly. |
|
rid |
string | no | Idempotency identifier. |
The classic panels also accepted months=N, days=N, or a computed exp_date on this action. The native renew handler extends by the official duration attached to the package, so those alternative forms are not translated. If your integration currently computes the expiry itself, create the equivalent package in the Xtream AI panel and send its id instead. This keeps renewal duration and cost in one place (the package definition).
Response
data is the full Line object with the new exp_date, and with enabled and admin_enabled both true.
{
"status": "STATUS_SUCCESS",
"data": {
"id": 172511994,
"username": "u_a1b2c3d4",
"password": "Sup3rSecret1",
"member_id": 100,
"exp_date": 1849365538,
"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=extend_line" \
-d "id=172511994" \
-d "package=42" \
-d "rid=renew-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' => 'extend_line']);
$body = http_build_query([
'id' => 172511994,
'package' => 42,
'rid' => 'renew-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'] ?? 'extend_line failed');
}
$line = $resp['data'];
echo 'new expiry ', date(DATE_ATOM, $line['exp_date']), PHP_EOL;
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": "extend_line"},
data={
"id": 172511994,
"package": 42,
"rid": f"renew-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", "extend_line failed"))
line = body["data"]
print("new expiry", line["exp_date"])
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; package is missing (message says package_id); or the package id does not exist. |
Send both id and package. |
STATUS_INVALID_DATA |
line_has_no_expiry |
The line is perpetual (exp_date == null). Extending would set an expiry and degrade the line. |
Use action=edit_line (or the native update endpoint) if you actually want to convert a perpetual line into a timed one. |
STATUS_INVALID_PACKAGE |
renew_with_trial_package_not_allowed |
package points to a trial package. |
Pick a non-trial package. |
STATUS_INVALID_PACKAGE |
package_not_accessible |
The reseller cannot sell from that package. | Use a package inside the reseller's member group. |
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 |
insufficient_scope |
The key does not have lines:write. |
Grant the scope. |
STATUS_FAILURE |
billing_expired |
Reseller subscription has expired. | Extend the reseller subscription first. |
STATUS_INSUFFICIENT_CREDITS |
insufficient_credits |
Reseller in credits mode lacks credits for the package cost. |
Top up credits, or pick a cheaper package. |
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. |