Using the official SDKs? Every endpoint below is a one-line typed method call — the HTTP details on this page are handled for you.
What a line is
A line is a subscriber account on your panel. It has a username and password that end customers plug into an IPTV app, an expiry date, a set of bouquets (channel groups) it is allowed to see, a maximum number of concurrent connections, and a few flags (is_trial, is_restreamer). Every line belongs to a specific owner: an admin or a reseller. Resellers only ever see, mutate, or bill for their own lines.
This page documents every endpoint under /panel-api/v1/lines/*. Requests need either the lines:read or lines:write scope depending on the operation. See Authentication for how to obtain a token.
Every write operation (create, update, enable, disable, renew, reset password, delete) requires an
Idempotency-Keyheader. See Rate limits & Idempotency for the safe-retry contract.
The line lifecycle
A line moves through a small number of well-defined states over its life:
- Create with
POST /lines. You pick the package (which decides duration, credit cost, default bouquets, default connections, and the trial flag) and, if you are an admin, themember_idof the owner reseller. The line is created withenabled=trueandadmin_enabled=true. On a reseller key, credits are deducted atomically before the row is written, and refunded automatically if the create fails. - List and get with
GET /linesandGET /lines/{id}. Reseller keys are auto-scoped to their ownmember_id. - Enable and disable with
POST /lines/{id}/enableandPOST /lines/{id}/disable.disablenever charges.enablere-checks slot capacity on reseller keys inusersbilling mode, so a reseller cannot use disable-then-enable to sneak past the cap. - Renew with
POST /lines/{id}/renewto pushexp_dateforward by the package's official duration. Increditsmode this deductsofficial_credits; inusersmode renewals are free. - Reset the password at any time with
POST /lines/{id}/reset-password(returns the new password in the response). - Update operator-level fields (admin only) with
POST /lines/{id}/update. This is a partial update: only the fields you send are written. - Inspect live connections with
GET /lines/{id}/connectionsto see who is streaming what, from where. - Delete with
POST /lines/{id}/delete. Bouquet assignments and contact-info rows are cleaned up in the same transaction. Deletion does not refund credits, matching the panel UI's behavior.
The two flags enabled and admin_enabled are separate on purpose: enabled is the reseller-visible toggle, admin_enabled is a hard override the admin can flip to block a line regardless of what the reseller does. The Panel API surfaces both, but only POST /lines/{id}/update (admin-only) can touch admin_enabled; the enable/disable endpoints only move enabled.
The line object
Every endpoint that returns a line uses this shape:
{
"id": 12345,
"username": "u_ab12cd34",
"password": "9f3c1a77",
"member_id": 42,
"exp_date": 1793520000,
"max_connections": 2,
"is_trial": false,
"is_restreamer": false,
"enabled": true,
"admin_enabled": true,
"bouquets": [1, 4, 9],
"created_at": 1785984000
}
Field notes:
| Field | Type | Notes |
|---|---|---|
id |
int | Panel-wide numeric id. Stable for the life of the line. |
username |
string | Autogenerated as u_<8hex> (or trial_<8hex> for trials) if you don't send one. |
password |
string | Autogenerated (8 hex chars) if omitted. Returned in clear in every line response. |
member_id |
int | Numeric id of the owner (admin or reseller). Reseller keys always see their own id here. |
exp_date |
int or null | Expiry as a UTC Unix epoch. null means the line never expires (perpetual). |
max_connections |
int | Concurrent-connection cap, in the range [1, 100]. |
is_trial |
bool | True if the line was created from a trial package. |
is_restreamer |
bool | True if this line is allowed to restream through the panel. |
enabled |
bool | Reseller-visible toggle. |
admin_enabled |
bool | Admin override. If false, the line is blocked no matter what enabled says. |
bouquets |
int[] | Bouquet ids assigned to the line. |
created_at |
int or null | Creation timestamp (UTC Unix epoch). |
Endpoints
The PHP and Python examples below assume a
$client/clientalready constructed as shown on the SDKs page (base URL plus API key), so each endpoint shows only the call itself. The SDK generates theIdempotency-Keyheader for every write automatically; passidempotencyKey/idempotency_keyyourself only when you want to derive it from a business ID. All line methods live under$client->lines/client.lines.
POST /lines — create a line
Create a new subscriber line.
Scope: lines:write. Idempotency: required.
Body fields
| Field | Type | Required | Notes |
|---|---|---|---|
package_id |
int | yes | Must exist. Determines default duration, credit cost, default bouquets, default max_connections, and whether the line is a trial. |
member_id |
int | admin only, required | The reseller (or admin) that will own the line. Reseller keys must NOT send this: the owner is forced to the key's own reseller id, and sending this field returns 403 admin_only_field. |
username |
string | no | Autogenerated if omitted. Must be unique panel-wide. |
password |
string | no | Autogenerated (8 hex chars) if omitted. Reseller keys whose member group has allow_change_pass=0 cannot pick a password (returns 403 password_change_not_allowed). |
bouquets |
int[] | no | Falls back to the package's default bouquets if omitted or empty. On reseller keys, every bouquet id must be visible to the reseller's member group; unknown ids return 422 validation_error with details.invalid_ids. |
is_trial |
bool | no | Only valid if the package itself is a trial package. Sending true on a non-trial package returns 422 trial_flag_requires_trial_package. A trial package always forces is_trial=true even if you don't send it. |
email, notes |
string | no | Stored on the line's contact-info row. Best-effort: a write failure here logs but does not fail the create. Reseller keys write to reseller_user_contact_info, admin keys to admin_user_contact_info. |
exp_date |
int (UTC epoch) | admin only | Custom expiry. Must be in the future and within 5 years. If omitted, expiry comes from the package's duration. |
max_connections |
int | admin only | Override the package's default. Clamped to [1, 100]. |
is_restreamer |
bool | admin only | Override the package's default. |
allowed_ips |
string[] | admin only | IPv4 allow-list. Up to 50 entries. Invalid entries are dropped silently. |
allowed_ua |
string[] | admin only | User-Agent allow-list. Up to 50 entries, each capped at 500 characters. |
is_isplock |
bool | admin only | Lock the line to the first ISP that connects. |
Admin-only fields: member_id, exp_date, max_connections, is_restreamer, allowed_ips, allowed_ua, is_isplock. If a reseller key sends any of these, the request is rejected with 403 admin_only_field and the offending field names come back in details.fields.
Order of validation (all before any write):
- Package exists.
- Reseller only: the package is in a member group the reseller can sell from.
is_trialmatches the package.- Reseller only: subscription is still active (
billing_expiresnot in the past). Admin keys are exempt here, so the admin can rescue an expired reseller's customer manually. - Reseller + trial: the reseller's trial quota for the current window is not exceeded.
- Reseller +
usersbilling mode + non-trial: slot capacity (both the reseller's own cap and any ancestor cap) is not exceeded. - Reseller +
creditsbilling mode: enough credits to cover the package cost. - Member-group permission checks for
passwordandis_isplock. - Bouquet ids are all visible to the reseller.
Credit deduction is atomic. In credits mode the deduction happens before the row insert, using a conditional UPDATE. If the deduction fails you get 402 insufficient_credits. If the insert fails afterwards (for example, a username collision) the credits are refunded automatically.
curl:
curl -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"package_id": 1,
"member_id": 42,
"username": "johndoe",
"password": "s3cret!",
"bouquets": [1, 4, 9],
"email": "johndoe@example.com",
"notes": "created from WHMCS order #4712"
}' \
https://<your-panel-domain>/panel-api/v1/lines
$line = $client->lines->create(
packageId: 1,
memberId: 42,
username: 'johndoe',
password: 's3cret!',
bouquets: [1, 4, 9],
email: 'johndoe@example.com',
notes: 'created from WHMCS order #4712',
);
echo $line->id, ' ', $line->username, PHP_EOL;
line = client.lines.create(
package_id=1,
member_id=42,
username="johndoe",
password="s3cret!",
bouquets=[1, 4, 9],
email="johndoe@example.com",
notes="created from WHMCS order #4712",
)
print(line.id, line.username)
Response (201): a full line object.
Errors:
| HTTP | Slug | Meaning |
|---|---|---|
| 400 | missing_idempotency_key |
Header not sent. |
| 402 | billing_expired |
Reseller's subscription has expired. |
| 402 | trial_quota_exceeded |
Reseller hit their trial-creation cap for the current window. |
| 402 | slot_limit_exceeded |
Reseller's own user cap would be exceeded. |
| 402 | ancestor_cap_reached |
A parent reseller's cap would be exceeded. |
| 402 | insufficient_credits |
Not enough credits to pay for the package. |
| 403 | admin_only_field |
Reseller sent a field reserved to admin keys. |
| 403 | package_not_accessible |
Reseller cannot sell from that package. |
| 403 | password_change_not_allowed |
Reseller's member group forbids picking a password. |
| 403 | isplock_not_allowed |
Reseller's member group forbids ISP-lock. |
| 409 | idempotency_conflict / idempotency_in_flight |
Retry with a different key, or wait for the in-flight request to finish. |
| 422 | validation_error |
Missing / malformed field, unknown package, unknown member_id, username collision, bouquet not accessible. |
| 422 | trial_flag_requires_trial_package |
is_trial=true on a non-trial package. |
GET /lines — list lines
List lines with cursor-based pagination.
Scope: lines:read.
Query parameters
| Param | Type | Notes |
|---|---|---|
limit |
int | Page size. Clamped to [1, 100], default 50. |
cursor |
int | Keyset cursor: the API returns lines with id > cursor, ascending. Use next_cursor from the previous response to walk the whole list. |
is_trial |
bool | Filter by trial flag. Accepts true / false / 1 / 0 / yes / no / on / off, case-insensitive. |
enabled |
bool | Same boolean parser as is_trial. |
username |
string | Exact match. |
password |
string | Exact match. Combined with username, this gives you the "find by credentials" query billing systems use to reconcile. |
Reseller keys are automatically scoped to their own member_id: they cannot see lines that belong to another owner, regardless of any filter they send.
When you filter by
password, we recommend sending the value in the request body (as an alternative supported form) or, better, callingGET /lines/{id}once you already know the id. Apasswordin a query string can leak into intermediate access logs (your load balancer, a corporate proxy). Our nginx logs strip the query string for the Panel API location, but you should not assume the whole path from client to server does the same.
curl:
curl -H "Authorization: Bearer $TOKEN" \
"https://<your-panel-domain>/panel-api/v1/lines?limit=50&enabled=true&cursor=0"
$page = $client->lines->list(limit: 50, enabled: true);
foreach ($page->items as $line) {
echo $line->id, ' ', $line->username, ' ', $line->enabled ? 'on' : 'off', PHP_EOL;
}
if ($page->nextCursor !== null) {
echo 'next page cursor: ', $page->nextCursor, PHP_EOL;
}
page = client.lines.list(limit=50, enabled=True)
for line in page.items:
print(line.id, line.username, line.enabled)
if page.next_cursor is not None:
print("next page cursor:", page.next_cursor)
Response (200):
{
"items": [
{ "id": 12345, "username": "u_ab12cd34", "...": "..." },
{ "id": 12346, "username": "u_ff2233aa", "...": "..." }
],
"next_cursor": 12346
}
next_cursor is null on the last page. Pass it back as the cursor query parameter to fetch the next slice.
GET /lines/{id} — fetch one line
Return the full line object for a single id.
Scope: lines:read.
curl:
curl -H "Authorization: Bearer $TOKEN" \
https://<your-panel-domain>/panel-api/v1/lines/12345
$line = $client->lines->get(12345);
echo $line->id, ' ', $line->username, ' expires ',
$line->expDate?->format(DATE_ATOM) ?? 'never', PHP_EOL;
line = client.lines.get(12345)
print(line.id, line.username, "expires",
line.exp_date.isoformat() if line.exp_date else "never")
Response (200): a full line object.
Errors: 404 not_found. Cross-tenant reads also return 404, not 403, so a reseller cannot use this endpoint to check whether an id exists in another reseller's book of business.
POST /lines/{id}/update — partial update (admin only)
Patch a subset of fields on an existing line. Every field is optional; only the fields you send are written.
Scope: lines:write. Admin key required, reseller keys get 403 admin_only_endpoint. Idempotency: required.
Accepted fields
| Field | Type | Notes |
|---|---|---|
password |
string | Set a new password (no random generation, use /reset-password for that). |
exp_date |
int, or null |
UTC epoch. Sending null explicitly makes the line perpetual (no expiry). Omitting the field leaves the current value alone. |
max_connections |
int | Clamped to [1, 100]. |
is_restreamer |
bool | Flip the restreamer flag. |
enabled |
bool | Reseller-visible toggle. |
admin_enabled |
bool | Hard admin override. Only settable through this endpoint. |
allowed_ips |
string[] | IPv4 allow-list, up to 50 entries. |
allowed_ua |
string[] | User-Agent allow-list, up to 50 entries. |
Unlike the create endpoint,
exp_datehere is not range-validated. You can move a line arbitrarily far into the future or back in time. Use with care.
curl:
curl -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"max_connections": 4, "is_restreamer": true}' \
https://<your-panel-domain>/panel-api/v1/lines/12345/update
$line = $client->lines->update(
id: 12345,
maxConnections: 4,
isRestreamer: true,
);
echo $line->maxConnections, PHP_EOL;
line = client.lines.update(
id=12345,
max_connections=4,
is_restreamer=True,
)
print(line.max_connections)
exp_date is three-state: omit the argument to leave the current expiry untouched, pass a UTC epoch to set a new one, or pass null / None to make the line perpetual — e.g. $client->lines->update(id: 12345, expDate: null) / client.lines.update(id=12345, exp_date=None).
Response (200): a full line object with the updated fields.
Errors: 400 missing_idempotency_key · 403 admin_only_endpoint · 404 not_found · 409 idempotency codes · 500 internal_error.
POST /lines/{id}/enable
Flip the enabled flag to true.
Scope: lines:write. Idempotency: required.
For reseller keys in users billing mode with a non-trial line, this endpoint re-checks slot capacity before enabling. This closes the loophole where a reseller could disable an old line and enable a new one to sneak past their user cap.
curl:
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
https://<your-panel-domain>/panel-api/v1/lines/12345/enable
$line = $client->lines->enable(12345);
echo $line->enabled ? 'enabled' : 'still disabled', PHP_EOL;
line = client.lines.enable(12345)
print(line.enabled)
Response (200): a full line object with "enabled": true.
Errors: 400 missing_idempotency_key · 402 slot_limit_exceeded / ancestor_cap_reached (reseller, users mode) · 404 not_found · 409 idempotency codes · 500 internal_error.
POST /lines/{id}/disable
Flip the enabled flag to false. Never charges, never rejects on billing.
Scope: lines:write. Idempotency: required.
curl:
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
https://<your-panel-domain>/panel-api/v1/lines/12345/disable
$line = $client->lines->disable(12345);
echo $line->enabled ? 'still enabled' : 'disabled', PHP_EOL;
line = client.lines.disable(12345)
print(line.enabled)
Response (200): a full line object with "enabled": false.
Errors: 400 missing_idempotency_key · 404 not_found · 409 idempotency codes · 500 internal_error.
POST /lines/{id}/renew
Extend the expiry of a line by the package's official duration.
Scope: lines:write. Idempotency: required.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
package_id |
int | yes | Must exist. Cannot be a trial package (returns 422 renew_with_trial_package_not_allowed). Reseller keys must have access to the package (403 package_not_accessible otherwise). |
Behavior:
- The new expiry is
max(current_exp_date, now) + package.official_duration. If the line already expired, the countdown starts from now. If it is still active, the extension is added on top of the current expiry (no lost days). admin_enabledandenabledare both set totruein the same statement, so a renew reactivates a previously disabled or admin-blocked line.- Reseller in
creditsmode: deductspackage.official_creditsbefore the update. Failure to deduct returns402 insufficient_credits. If the update fails after the deduction, the credits are refunded. - Reseller in
usersmode: renewals are free (a slot is already accounted for as long as the line exists). - Perpetual lines (
exp_date == null) cannot be renewed: renewing would set an expiry and effectively degrade the line. The endpoint returns422 line_has_no_expiry. UsePOST /lines/{id}/updatewith a chosenexp_dateif you really want to convert a perpetual line into a timed one.
curl:
curl -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"package_id": 1}' \
https://<your-panel-domain>/panel-api/v1/lines/12345/renew
$line = $client->lines->renew(id: 12345, packageId: 1);
echo 'new expiry ', $line->expDate?->format(DATE_ATOM), PHP_EOL;
line = client.lines.renew(id=12345, package_id=1)
print("new expiry", line.exp_date.isoformat() if line.exp_date else None)
Response (200): a full line object with the new exp_date.
Errors: 400 missing_idempotency_key · 402 billing_expired / insufficient_credits · 403 package_not_accessible · 404 not_found · 409 idempotency codes · 422 validation_error (unknown package) / renew_with_trial_package_not_allowed / line_has_no_expiry · 500 internal_error.
POST /lines/{id}/reset-password
Rotate a line's password.
Scope: lines:write. Idempotency: required.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
password |
string | no | If omitted, the API generates a random 8-hex-character password. |
Reseller keys whose member group has allow_change_pass=0 cannot use this endpoint (403 password_change_not_allowed).
curl:
curl -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{}' \
https://<your-panel-domain>/panel-api/v1/lines/12345/reset-password
// resetPassword returns the new password as a string, not a Line.
$newPassword = $client->lines->resetPassword(12345);
echo $newPassword, PHP_EOL; // e.g. "9f3c1a77"
# reset_password returns the new password as a str, not a Line.
new_password = client.lines.reset_password(12345)
print(new_password) # e.g. "9f3c1a77"
To set a specific password instead of a random one, pass it: $client->lines->resetPassword(12345, 's3cret!') / client.lines.reset_password(12345, "s3cret!").
Response (200): a compact shape, not the full line object:
{"id": 12345, "password": "9f3c1a77"}
If you need the full line back, call GET /lines/{id} afterwards.
Errors: 400 missing_idempotency_key · 403 password_change_not_allowed · 404 not_found · 409 idempotency codes · 500 internal_error.
POST /lines/{id}/delete
Delete a line and cascade-clean its related rows (bouquet assignments, contact info).
Scope: lines:write. Idempotency: required.
Reseller keys are subject to the member_groups.delete_users permission: if the reseller's group is not allowed to delete customers, the endpoint returns 403 delete_not_allowed. This mirrors the guard the reseller UI applies, so a POST from an API integration cannot bypass what the UI itself would block.
Deletion does not refund credits, matching the panel UI. In users billing mode the slot is released automatically (the active-users count reads live from the users table).
curl:
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
https://<your-panel-domain>/panel-api/v1/lines/12345/delete
// delete returns a bool: true once the panel confirms the row is gone.
$deleted = $client->lines->delete(12345);
echo $deleted ? 'gone' : 'still there', PHP_EOL;
# delete returns a bool: True once the panel confirms the row is gone.
deleted = client.lines.delete(12345)
print(deleted) # True
Response (200):
{"id": 12345, "username": "u_ab12cd34", "deleted": true}
The API verifies the row actually disappeared before reporting success. If the delete cascade fails partway through, the endpoint returns 500 delete_failed instead of a false 200, so your billing system does not mark a customer inactive while their line is still active on the panel.
Errors: 400 missing_idempotency_key · 403 delete_not_allowed (reseller only) · 404 not_found · 409 idempotency codes · 500 delete_failed / internal_error.
GET /lines/{id}/connections
List live streaming connections for a single line. Useful for kick-per-user diagnostics, showing "who is watching what" in a support UI, or feeding a fraud-detection pipeline.
Scope: lines:read.
Returns up to 200 connections, ordered by started_at descending. There is no pagination cursor: a single line rarely has more than a handful of active connections at once.
curl:
curl -H "Authorization: Bearer $TOKEN" \
https://<your-panel-domain>/panel-api/v1/lines/12345/connections
// connections returns a plain array of Connection objects (no cursor).
$connections = $client->lines->connections(12345);
foreach ($connections as $c) {
echo $c->clientIp, ' ', $c->clientCountry, ' ', $c->contentName, ' ', $c->elapsedSec, "s\n";
}
# connections returns a plain list of Connection objects (no cursor).
connections = client.lines.connections(12345)
for c in connections:
print(c.client_ip, c.client_country, c.content_name, c.elapsed_sec, "s")
Response (200):
{
"items": [
{
"connection_id": 987654,
"content_type": "live",
"content_id": 4211,
"content_name": "ESPN HD",
"started_at": 1785984000,
"elapsed_sec": 132,
"client_ip": "203.0.113.42",
"client_country": "US"
},
{
"connection_id": 987655,
"content_type": "movie",
"content_id": 88012,
"content_name": "Breaking Bad S01E01",
"started_at": 1785983900,
"elapsed_sec": 232,
"client_ip": "203.0.113.42",
"client_country": "US",
"is_serie": true
}
]
}
Field notes:
content_typeis normalized to"live"or"movie". Series episodes are reported as"movie"with an additionalis_serie: trueflag.content_idandcontent_namearenull/ empty if the content was deleted after the connection started (rare, but the join is a LEFT JOIN so the connection is still surfaced).elapsed_secis computed server-side, so successive calls give you a monotonically increasing value without clock drift concerns on your end.client_ipandclient_countrycome from the streaming server's own connection log, which uses a MaxMind GeoIP2 database for the country code.
Fields that we deliberately do NOT expose here: server ip, session id, user-agent. If you need those for a specific audit, tell us the use case and we will decide whether to add them behind an opt-in scope.
Errors: 404 not_found.
Cross-tenant isolation
Every endpoint on this page enforces the same rule: a reseller key can never see or mutate a line whose member_id is not the key's own reseller id. This includes:
GET /linesauto-filters bymember_id = <caller>.GET /lines/{id},POST /lines/{id}/*,GET /lines/{id}/connectionsall resolve the line, check ownership, and return404 not_foundon any mismatch.- We return
404, not403, on cross-tenant reads. A403would confirm that the requested id exists, which is enough for an attacker to enumerate the size of another reseller's book of business inside the same panel.
Admin keys have no such filter: they see and mutate every line, in every reseller's book. Give the admin scope only to integrations that genuinely need panel-wide reach (billing reconciliation, migration tooling, incident response). For everything else, issue a reseller key with the smallest scope that gets the job done.
Common errors
The full table of Panel API error slugs, HTTP codes, and remediation lives in Errors. The subset your integration is most likely to hit on this resource:
| HTTP | Slug | When |
|---|---|---|
| 400 | missing_idempotency_key |
Any write without the header. Send a fresh key on each new attempt; reuse only when retrying the same logical operation. |
| 402 | insufficient_credits |
Reseller ran out of credits mid-create or mid-renew. Top up and retry with a fresh idempotency key. |
| 402 | billing_expired |
Reseller's subscription lapsed. Renew the reseller first, then retry. |
| 402 | slot_limit_exceeded / ancestor_cap_reached |
Reseller (or an ancestor) hit their user cap. Raise the cap or delete inactive lines. |
| 402 | trial_quota_exceeded |
Reseller hit their trial-creation quota for the current window. Wait for the window to roll or raise the cap. |
| 403 | admin_only_endpoint |
Reseller called /lines/{id}/update. Use an admin key. |
| 403 | admin_only_field |
Reseller sent a field reserved to admin keys (member_id, exp_date, max_connections, is_restreamer, allowed_ips, allowed_ua, is_isplock). Drop the field or use an admin key. |
| 403 | package_not_accessible |
Reseller cannot sell from that package. Add the package to the reseller's member group, or pick another package. |
| 403 | password_change_not_allowed |
Reseller's member group forbids picking / resetting passwords. Change the group permission or let the API autogenerate. |
| 403 | delete_not_allowed |
Reseller's member group forbids deleting customers. |
| 404 | not_found |
The line does not exist, or (reseller only) it exists but belongs to someone else. |
| 409 | idempotency_in_flight |
An earlier request with the same key is still processing. Wait and retry. |
| 409 | idempotency_conflict |
An earlier request with the same key resolved with a different body. Generate a fresh key. |
| 422 | validation_error |
Missing / malformed field, unknown package or member id, username collision, bouquets not accessible. details usually points to the offending field. |
| 422 | trial_flag_requires_trial_package |
Sent is_trial=true on a non-trial package. |
| 422 | renew_with_trial_package_not_allowed |
Passed a trial package to /renew. |
| 422 | line_has_no_expiry |
Tried to renew a perpetual line. |
| 429 | rate_limited |
Per-key or per-IP budget exceeded. Honor the Retry-After header. |
See also
- Overview — the whole Panel API in one page.
- Authentication — token shape, scopes, IP allow-lists.
- Rate limits & Idempotency — safe-retry semantics, budgets.
- Catalog — packages, bouquets, streams, and VODs you can attach to a line.
- Resellers — the owners of the lines you create with
member_id. - Errors — the full slug table for every endpoint.