Renew a line
Push a line's exp_date forward by the official duration of a package. 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.
Renewing also reactivates the line: admin_enabled and enabled are both set to true in the same statement. That way a renew brings back a previously disabled or admin-blocked line without a second call.
Renewing also applies the package to the line, the same way the panel's own Extend does. max_connections becomes the package's value (clamped to [1, 100]) and is_trial becomes false, so a trial line renewed onto an official package leaves the trial state and gets the connections the customer paid for. is_restreamer, allowed_ips, allowed_ua and the other per-line settings are left as they were. If you gave a line more connections than its package and want to keep them across renewals, follow the renew with update and an explicit max_connections (admin keys only).
Billing behavior:
- Reseller in credits mode deducts
package.official_creditsbefore the update. Failure to deduct returns402 insufficient_credits. If the update fails after the deduction, the credits are refunded. - Reseller in users mode renews for free (a slot is already accounted for as long as the line exists).
- Admin keys skip the billing checks entirely.
Renew is also where a line changes what it can watch. Pass bouquets with a subset of the package's bouquets and the line's assignment is replaced; leave it out and the line keeps the bouquets it already has. For a reseller key this is the only way to add bouquets to a line or move it to another package, since update can only remove them, and it charges a full period in credits mode.
Renew vs. changing the package. The two calls do different jobs and an admin key has both. Renew is the billing operation: it pushes exp_date forward by the package's official duration, reactivates the line, applies the package's connections and clears the trial flag, and in credits mode charges official_credits. Update with package_id is the configuration operation: it applies the new package's connections, restreamer flag and bouquets to the line and leaves the expiry, the flags and the credits exactly where they were. Use renew when the customer is paying for another cycle, and update with package_id when the customer switches plan mid-cycle and the billing already happened on your side. package_id on update is admin-only; with a reseller key it returns 403 admin_only_field, and renew stays the only path.
Perpetual lines (exp_date == null) cannot be renewed. Renewing would set an expiry and effectively degrade the line, so the endpoint returns 422 line_has_no_expiry. Use update with a chosen exp_date if you really want to convert a perpetual line into a timed one.
Endpoint
POST https://<your-panel-domain>/panel-api/v1/lines/{id}/renew
Authentication
Bearer token in the Authorization header. See Panel API authentication.
Required scope
lines:write.
Idempotency
Required. Renewals are the classic double-charge risk: sending the same Idempotency-Key on a retry returns the original response instead of charging twice. A missing header returns 400 missing_idempotency_key.
Path parameters
| Name | Type | Description |
|---|---|---|
id |
int | The line id to renew. |
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
package_id |
int | yes | Package to renew with. Must exist, must not be a trial package. Reseller keys must have access to the package. | |
bouquets |
int[] | no | line keeps its current bouquets | Non-empty subset of the bouquets of package_id, at most 512 ids, and every id must still exist in the bouquet catalog. Replaces the line's assignment. Same rule for admin and reseller keys. |
{
"package_id": 42
}
Renewing onto a package but keeping only part of it:
{
"package_id": 42,
"bouquets": [1, 4]
}
bouquets is validated before any credit is deducted, so a rejected id never costs a reseller a period: ids that are not in the package come back as 422 validation_error with details.invalid_ids, and nothing is charged or changed. A package definition can still list a bouquet that was deleted afterwards; those ids are rejected the same way.
Response
200 OK with the full line object. exp_date reflects the new expiry, enabled and admin_enabled are both true, and bouquets is re-read from the panel, so it shows what was actually stored.
{
"id": 172504295,
"username": "u_a1b2c3d4",
"password": "c30b22e4",
"member_id": 100,
"exp_date": 1791477054,
"max_connections": 1,
"is_trial": false,
"is_restreamer": false,
"enabled": true,
"admin_enabled": true,
"bouquets": [],
"created_at": 1786206654
}
Examples
cURL
curl -X POST https://<your-panel-domain>/panel-api/v1/lines/172504295/renew \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-INV-2026-00915" \
-d '{"package_id": 42}'
With a bouquet subset:
curl -X POST https://<your-panel-domain>/panel-api/v1/lines/172504295/renew \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-INV-2026-00915" \
-d '{"package_id": 42, "bouquets": [1, 4]}'
PHP SDK
require __DIR__ . '/api-panel-php-sdk-1.3.0/autoload.php';
use XtreamAI\PanelApi\PanelApiClient;
$client = new PanelApiClient(baseUrl: 'https://<your-panel-domain>', token: '<your-api-key>');
$line = $client->lines->renew(
id: 172504295,
packageId: 42,
bouquets: [1, 4], // optional: omit to keep the line's current bouquets
idempotencyKey: 'invoice-INV-2026-00915',
);
echo 'new expiry ', $line->expDate?->format(DATE_ATOM), PHP_EOL;
Python SDK
from xtream_ai_panel_api import PanelApiClient
client = PanelApiClient(base_url="https://<your-panel-domain>", token="<your-api-key>")
line = client.lines.renew(
id=172504295,
package_id=42,
bouquets=[1, 4], # optional: omit to keep the line's current bouquets
idempotency_key="invoice-INV-2026-00915",
)
print("new expiry", line.exp_date.isoformat() if line.exp_date else None)
Errors
| HTTP | Error slug | When it happens | How to fix |
|---|---|---|---|
| 400 | missing_idempotency_key |
The Idempotency-Key header was not sent. |
Send a per-intent key on every write. |
| 401 | invalid_key |
Token is unknown, expired, disabled, or deleted. | Check the token or issue a new one. |
| 402 | billing_expired |
Reseller subscription has expired. | Extend the reseller subscription. |
| 402 | insufficient_credits |
Reseller (credits mode) lacks credits for the package. | Top up credits, or pick a cheaper package. |
| 403 | package_not_accessible |
Reseller cannot sell from that package. | Use a package inside the reseller's member group. |
| 403 | insufficient_scope |
Token lacks lines:write. |
Issue a key with the scope. |
| 404 | not_found |
The line id does not exist, or a reseller key targeted a line it does not own. | Verify the id and ownership. |
| 409 | idempotency_conflict |
Same key was reused with a different body. | Use a fresh key. |
| 409 | idempotency_in_flight |
Same key is still processing. | Retry after a moment. |
| 422 | validation_error |
Missing package_id, the package id does not exist, or bouquets is empty, malformed, or holds ids that are not in the package or no longer exist in the bouquet catalog. |
See details.field and details.invalid_ids in the response. Nothing was charged. |
| 422 | renew_with_trial_package_not_allowed |
package_id points to a trial package. |
Pick a non-trial package. |
| 422 | line_has_no_expiry |
The line is perpetual (exp_date == null) and renewing would set an expiry. |
Use update with an explicit exp_date instead. |
| 429 | rate_limited |
The key hit its per-minute cap. | Back off and retry after Retry-After seconds. |
See also
- Create a line
- Update a line
- Changing the package. The admin-key way to switch plan without renewing.
- Panel API lines overview
- Rate limits and idempotency