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_credits before the update. Failure to deduct returns 402 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