Panel API Authentication
Every request to the Panel API is authenticated with a single string of the form pk_live_<prefix>.<secret>. You create it once from the panel UI and hand it to your integration. The same key works across all three flavors of the API (native v1, Xtream Codes / XUI.one, and OneStream), so you never need to juggle one credential for the native endpoints and another for the compatibility ones.
This page covers what a key looks like, how to send it in each flavor, what scopes are available, and how to keep keys under control (rotation, revocation, IP allow-lists, per-key rate limits). If you just want to place your first authenticated call, jump to the Quickstart instead. It gets you from "no key" to "first successful response" in about five minutes.
The official SDKs take the token in the client constructor and add the correct header on every request for you. If you use them, most of this page is background information rather than something you have to implement.
How does it work?
An API key is a single string with a fixed shape. The panel stores only the hash of the secret half, so the full string is shown to you exactly once, at the moment of issuance. Every incoming request is checked against that stored hash with a constant-time comparison, so timing attacks cannot leak information about whether a given prefix exists on the panel.
Requests are then filtered by three independent checks. The IP allow-list decides whether the source address of the request is permitted. The scope list decides whether the requested endpoint is permitted. The per-key rate limit decides whether the caller is allowed to make another request in the current minute. Any one of the three can reject the request on its own.
Token shape
Every API key has the following layout.
pk_live_<12-char prefix>.<43-char secret>
Example.
pk_live_x2fakekey423.Fk3xAmpLeSecretNotARealKeyDoNotUse_9x2fake9
The pieces are:
pk_live_. Fixed prefix identifying a live (production) key.<12-char prefix>. Public identifier, safe to log. Used for lookup and troubleshooting. RFC 4648 base32 lowercase (lettersa-zand digits2-7)... Separator.<secret>. The actual secret. Thirty-two bytes of random data encoded as URL-safe base64 (roughly 43 characters). Shown only once at issuance and never again.
The secret is compared with a constant-time hash on the server (hash_equals), so an attacker cannot mount timing attacks against the byte-by-byte comparison.
If you lose the secret you cannot recover it. You must rotate the key (regenerate a new secret while keeping the same prefix) or issue a new key entirely. See Rotating a key below.
Authenticating in each flavor
The same API key works in all three flavors. Only the transport differs.
Native v1 (JSON with Bearer)
Send the token in the Authorization: Bearer <token> HTTP header.
curl -H "Authorization: Bearer pk_live_x2fakekey423.Fk3xAmpLe..." \
https://<your-panel-domain>/panel-api/v1/me
The header name and the scheme are case-insensitive. Bearer, bearer and BEARER all work, per RFC 7235.
The official SDKs take the Bearer token in the client constructor and set the header on every request for you.
use XtreamAI\PanelApi\PanelApiClient;
$client = new PanelApiClient(
baseUrl: 'https://<your-panel-domain>',
token: 'pk_live_x2fakekey423.Fk3xAmpLe...',
);
$identity = $client->me->get();
echo $identity->type, ' scopes: ', implode(', ', $identity->scopes), PHP_EOL;
from xtream_ai_panel_api import PanelApiClient
client = PanelApiClient(
base_url="https://<your-panel-domain>",
token="pk_live_x2fakekey423.Fk3xAmpLe...",
)
identity = client.me.get()
print(identity.type, identity.scopes)
Xtream Codes / XUI.one / OTT Panel (query parameter)
The classic panels expect the token as a query string parameter named api_key.
curl "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=pk_live_x2fakekey423.Fk3xAmpLe...&action=user_info"
The {accesscode} segment (here, panel_api) is decorative and can be any value. Both /admin/index.php and /reseller/index.php accept the same key. The API infers whether the caller is an admin or a reseller from the key itself, not from the path.
This flavor also accepts
Authorization: Bearer <token>as an alternative. If your XC-compatible SDK sends both?api_key=and a Bearer header, they are equivalent. This makes it easy to move a key to a shared secret store while keeping the classic URL structure.
OneStream (X-Api-Key header)
OneStream integrations send the token in the X-Api-Key header.
curl -H "X-Api-Key: pk_live_x2fakekey423.Fk3xAmpLe..." \
https://<your-panel-domain>/panel-api/onestream/ext/profile
Authorization: Bearer <token> also works in the OneStream flavor, in case your SDK has already been standardized on Bearer.
Scopes
Every key is issued with an explicit list of scopes. A request that hits an endpoint requiring a scope the key does not have receives 403 insufficient_scope.
| Scope | Grants |
|---|---|
lines:read |
List, get and inspect connections of subscriber lines. |
lines:write |
Create, update, enable, disable, renew, reset password and delete lines. |
packages:read |
List packages. |
bouquets:read |
List bouquets. |
streams:read |
List and get live streams. |
vods:read |
List and get VOD entries. |
resellers:read |
List and get resellers, view their billing. Admin-only. |
resellers:write |
Update resellers, adjust their credits or user quotas. Admin-only. |
subresellers:write |
Create a sub-reseller under the caller. Reseller keys may hold this scope. The admin still gates the actual permission via the reseller's member group (create_sub_resellers). |
The two admin-only scopes (resellers:read, resellers:write) are rejected at issuance time when you try to attach them to a reseller key.
Pick the minimum scopes your integration needs. A billing system that only creates lines and adjusts credits, for example, only needs lines:write, packages:read, bouquets:read, resellers:write. Fewer scopes means a smaller blast radius when a key is compromised.
IP allow-list
Every key can optionally be restricted to a list of source IPv4 addresses. Requests from any other IP are rejected with 401 invalid_key. That is the same slug you get for an unknown key. The uniform response is deliberate. An attacker cannot use the endpoint to discover whether a given key exists or which IPs are on its allow-list.
Set the IP allow-list when creating the key from the UI, or leave it empty to accept requests from any IP.
Per-key rate limit
Each key carries its own rate_limit_per_min value. The default is 60 requests per minute and it can be raised at issuance time (the UI accepts values between 1 and 600). When the budget is exhausted the endpoint returns 429 rate_limited with Retry-After: 60. Full details, response headers and safe retry recipes live in Rate limits and Idempotency.
Key expiration
Keys can carry an optional expires_at timestamp. After that moment, requests receive 401 invalid_key. Setting an expiration is recommended for short-lived integrations. Contractors, one-off migrations and sandbox testing all benefit from a key that turns itself off on a known date.
Managing keys from the panel UI
Go to Settings → Panel API Keys in your admin panel.
Issuing a new key
- Click Create key.
- Fill in a label (for example
"WHMCS-production-2026"). This is what you will see in logs and the audit trail. - Pick the scopes.
- Optional. Add an IP allow-list.
- Optional. Set a per-minute rate limit (default 60, higher values available on request).
- Optional. Set an expiration date.
- Copy the token immediately. The secret is shown only once and cannot be retrieved later.
Rotating a key
Rotation regenerates the secret without changing the key id or prefix. It is useful when you suspect a leak but do not want to update every external reference to the key.
- Go to Settings → Panel API Keys.
- Find the key by its prefix or label.
- Click Rotate.
- Copy the new token immediately. The old secret is invalidated the moment the rotation completes.
Revoking a key
- Find the key.
- Click Delete.
The key is soft-deleted (kept in the audit table for 90 days) and all further requests return 401 invalid_key immediately.
Emergency kill for a compromised key
If a key is actively being abused and you cannot wait for the UI to load, use the admin CLI on the panel host.
xai pg sql -q "UPDATE regusers_cms_public_api_keys
SET enabled = FALSE, deleted_at = NOW()
WHERE key_prefix = '<prefix>'"
The change takes effect immediately.
Resellers issuing their own keys
A reseller can issue Panel API keys only if the panel admin has enabled the permission on their member group (Reseller settings → Member groups → Can create API keys). By default this is off. The admin has to explicitly opt each group in.
Reseller keys:
- Cannot include admin-only scopes (
resellers:read,resellers:write). - Are capped at 10 keys per reseller.
- Only see the reseller's own lines and their sub-resellers' lines. Tenant isolation is enforced by the API.
Security recommendations
The recommendations below apply to every integration, regardless of scale.
- Store secrets in a proper secret manager. Not in Git. Not in
.envfiles committed to source control. - Use a distinct key per integration. If one leaks you rotate only that one, not everything.
- Set an IP allow-list whenever the calling system has a stable outbound IP. It eliminates the entire class of stolen-token replay from another network.
- Set a per-minute rate limit matching your integration's actual traffic pattern. It caps the blast radius of a leak.
- Rotate periodically. Every 6 to 12 months as a routine, or immediately if you suspect a leak. Reasons to rotate immediately include a source code repository accidentally made public, a stolen laptop or an employee leaving.
The /me endpoint
To verify a key is working and see what it can do, hit /me.
curl -H "Authorization: Bearer $TOKEN" \
https://<your-panel-domain>/panel-api/v1/me
Admin key response.
{
"type": "admin",
"reg_user_id": null,
"member_group_id": null,
"member_group_name": null,
"billing": null,
"permissions": null,
"key": {
"id": 42,
"prefix": "pk_live_x2fakekey423",
"scopes": ["lines:read", "lines:write", "packages:read", "bouquets:read", "streams:read", "vods:read", "resellers:read", "resellers:write"]
}
}
Reseller key response.
{
"type": "reseller",
"reg_user_id": 260595,
"member_group_id": 4,
"member_group_name": "RESELLER",
"billing": {
"mode": "credits",
"credits": 0.25,
"max_users": null,
"active_users": null,
"billing_expires": null
},
"permissions": {
"create_sub_resellers": true,
"delete_users": true
},
"key": {
"id": 43,
"prefix": "pk_live_fakereseller",
"scopes": ["lines:read", "lines:write", "packages:read", "bouquets:read", "streams:read", "vods:read", "subresellers:write"]
}
}
/me is the canonical way to check "am I authenticated correctly?" and to learn the caller's identity in code paths that need it.
When authentication fails
The API returns a single uniform slug for every failure of the credential itself.
401 invalid_key. TheAuthorizationheader is missing, the token is malformed, the prefix is unknown, the secret is wrong, the key is expired, the key is disabled, the key was deleted, or the source IP is not on the key's allow-list. The uniform response is deliberate. It prevents attackers from using the API to probe your key inventory.401 caller_disabled. The token is valid but the underlying reseller account has been banned, its member group has been banned, or the reseller record no longer exists. Only reseller keys can receive this slug. Admin keys are not tied to a reg_user.
If either fires and you cannot resolve it from the panel UI, contact your administrator. See the full slug catalog in Errors.
See also
- Quickstart. Two-minute walk-through to get your first key and place your first call.
- Errors. The full slug catalog, including
invalid_key,caller_disabledandinsufficient_scope. - Rate limits and Idempotency. Per-key and per-IP budgets, response headers and safe retry recipes.