Errors with the SDK
Every failing API call raises a typed exception. There is one base class (PanelApiException) and one concrete subclass per documented failure mode. That gives you two natural ways to handle failures:
- Broad:
catch (PanelApiException $e)in one place, log$e->requestId, surface a generic error to the user. - Narrow: catch a specific subclass (say
InsufficientCreditsException) to react to exactly that condition — top up credits, suspend a downstream trigger, show a targeted message.
Both patterns compose. A try block that catches two specific classes and then the base class handles the important cases specifically and every other failure uniformly.
Exception hierarchy
Every SDK error inherits from PanelApiException. Everything else extends it. There are no other unrelated exception classes to worry about — network timeouts and bad TLS also surface as PanelApiException subclasses.
| Class | HTTP status | What it means |
|---|---|---|
PanelApiException |
any | Base class for every SDK error. All others extend it. |
BadRequestException |
400 | The request body is malformed or a required header is missing. |
AuthenticationException |
401 | The API key is invalid, disabled, expired, or the caller IP is not allowed. |
AuthorizationException |
403 | The key is valid but does not have the scope needed for this action, or the panel does not allow the action for this caller (for example sub_reseller_debit_disabled or debit_direct_only on a reseller credit transfer). |
InsufficientCreditsException |
402 or 422 | The reseller is out of credits, out of user slots, or their billing has lapsed. |
NotFoundException |
404 | The URL is right but the row does not exist (or is not visible to the caller). |
ConflictException |
409 | A concurrent write already used the same idempotency key with a different payload, or the same reseller credit transfer was sent twice within 5 seconds (duplicate_transfer). |
ValidationException |
422 | The request was well formed but a field failed a business rule (see .field). |
RateLimitException |
429 | You exceeded the per-key or per-IP rate limit. Read .retryAfter and back off. |
ServerException |
5xx or 0 | The panel encountered an internal error, or it could not be reached at all (slug network_error, status code 0). The SDK already retried before raising this. |
ServiceUnavailableException |
503 | The API is disabled or temporarily unavailable at the panel level. |
UnknownApiException |
any other | Fallback for status codes the SDK does not recognise. |
The full catalogue of error slugs (the machine-readable name each response carries) is in Errors. Every slug has a remediation note.
The panel emits
insufficient_slotswith HTTP 422, not 402. Semantically it is the users-billing-mode analog ofinsufficient_credits. The SDK special-cases it and raisesInsufficientCreditsExceptionin both cases, so a singlecatchhandles credits mode and users mode uniformly.
Common fields on every exception
Every exception carries the same set of properties so you can log and react without any extra plumbing:
- PHP:
$e->slug,$e->getMessage(),$e->requestId,$e->details,$e->statusCode. - Python:
.slug,str(e),.request_id,.details,.status_code.
requestId (request_id) is the value the panel puts in the X-Request-Id response header. Quote it whenever you contact support — it lets us jump straight to the failing request in our logs.
slug is the machine-readable failure name (validation_error, insufficient_credits, line_has_no_expiry, …). Log this instead of the human-facing message when you care about grouping errors by cause.
details is a small object with per-error context — for ValidationException it holds {"field": "username"}; for RateLimitException it holds {"retry_after": 30}. Details are documented per slug in Errors.
Extra fields on specific subclasses
Some subclasses expose their most-used detail as a first-class property so you do not have to reach into .details:
ValidationException—->field()(PHP) /.field(Python) returns the offending field name.RateLimitException—->retryAfter(PHP) /.retry_after(Python) returns the seconds you should wait before retrying.ConflictException—->existing()(PHP) /.existing(Python) returns the payload the panel remembers from the first request that used the same idempotency key.
Typical handling pattern
Group the specific reactions first, then a base-class fall-through for everything else. Order matters — catch more specific classes before the base.
<?php
require __DIR__ . '/api-panel-php-sdk-1.3.0/autoload.php';
use XtreamAI\PanelApi\PanelApiClient;
use XtreamAI\PanelApi\Exceptions\{
InsufficientCreditsException,
RateLimitException,
ValidationException,
PanelApiException,
};
$client = new PanelApiClient(
baseUrl: 'https://<your-panel-domain>',
token: '<your-api-key>',
);
try {
$line = $client->lines->create(
packageId: 42, memberId: 260595, username: 'johndoe',
);
} catch (InsufficientCreditsException $e) {
// Reseller is out of credits or user slots. Tell them to top up.
notifyBillingOps($e->requestId);
} catch (RateLimitException $e) {
// Slow down. The panel told us how long to wait.
sleep($e->retryAfter);
} catch (ValidationException $e) {
// A field is invalid. $e->field() names the culprit.
log_error("bad field: {$e->field()} req={$e->requestId}");
} catch (PanelApiException $e) {
// Anything else. Log and surface a generic error.
log_error("unexpected api error: slug={$e->slug} req={$e->requestId}");
}
from xtream_ai_panel_api import PanelApiClient
from xtream_ai_panel_api.exceptions import (
InsufficientCreditsException,
RateLimitException,
ValidationException,
PanelApiException,
)
import time
client = PanelApiClient(
base_url="https://<your-panel-domain>",
token="<your-api-key>",
)
try:
line = client.lines.create(
package_id=42, member_id=260595, username="johndoe",
)
except InsufficientCreditsException as e:
notify_billing_ops(e.request_id)
except RateLimitException as e:
time.sleep(e.retry_after)
except ValidationException as e:
log_error(f"bad field: {e.field} req={e.request_id}")
except PanelApiException as e:
log_error(f"unexpected api error: slug={e.slug} req={e.request_id}")
401 vs 403 — a common source of confusion
Both come with the word "not allowed", but they mean different things:
AuthenticationException(401) — the panel could not identify the caller. The token is wrong, expired, revoked, or the source IP is not in the key's allow-list. Fix: verify the token loads correctly from your secret store, and check the panel UI for the key's status and IP restrictions.AuthorizationException(403) — the panel identified the caller but the key does not have permission for this specific action. Fix: check the key's scope list (me.get().scopes) — the operation you attempted probably needs a scope your key does not have.
The exception names the missing scope in details['required_scope'] (Python: details["required_scope"]), and the message reads This API key lacks the required scope: lines:write. Add that scope in the panel UI, then rotate to a new key.
Distinguishing network errors from API errors
Failures that happen BEFORE the panel handles the request (connect timeout, DNS failure, TLS handshake failure, dropped connection mid-request) raise ServerException with slug network_error, status code 0 and no request ID, because there is no response. There is no separate network exception class: check the slug.
Every other exception covers a response the panel actually sent. Status code is a real HTTP status, the slug is populated, and the request ID identifies the call in the server logs.
The SDK's retry policy differs between the two (see Retry and Idempotency): network errors always retry (transient by nature); API errors retry only on 5xx and 429.
Getting the raw response
Every exception already carries slug, statusCode (status_code), requestId (request_id) and details, which is usually enough to log. To log every round trip, including the successful ones, hand the client your own transport.
In PHP the constructor accepts httpClient, a callable that receives (method, url, headers, body) and returns [status, headers, body]. This one is plain cURL plus one log line per request:
<?php
use XtreamAI\PanelApi\PanelApiClient;
use XtreamAI\PanelApi\Exceptions\ServerException;
$client = new PanelApiClient(
baseUrl: 'https://<your-panel-domain>',
token: '<your-api-key>',
httpClient: function (string $method, string $url, array $headers, ?string $body): array {
$ch = curl_init($url);
$respHeaders = [];
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HEADERFUNCTION => function ($ch, string $line) use (&$respHeaders): int {
if (str_contains($line, ':')) {
[$name, $value] = explode(':', $line, 2);
$respHeaders[strtolower(trim($name))] = trim($value);
}
return strlen($line);
},
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$respBody = curl_exec($ch);
if ($respBody === false) {
$error = curl_error($ch);
curl_close($ch);
throw new ServerException('network_error', $error, null, [], 0);
}
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
error_log("HTTP {$status} {$method} {$url} request_id=" . ($respHeaders['x-request-id'] ?? '-'));
return [$status, $respHeaders, $respBody];
},
);
Throwing ServerException with slug network_error on a cURL failure keeps the SDK's automatic retry working.
In Python the constructor accepts session, a requests.Session, so a standard response hook does the job:
import requests
from xtream_ai_panel_api import PanelApiClient
def log_response(response, *args, **kwargs):
print(f"HTTP {response.status_code} {response.request.method} {response.request.path_url} "
f"request_id={response.headers.get('X-Request-Id', '-')}")
session = requests.Session()
session.hooks["response"].append(log_response)
client = PanelApiClient(
base_url="https://<your-panel-domain>",
token="<your-api-key>",
session=session,
)
The request_id in these lines is the same one the exception carries. Quote it whenever you contact Xtream AI support: it is the fastest way to find your call in the server logs.
See also
- Errors. The full slug catalogue, one row per documented failure mode.
- Retry and Idempotency. What the SDK retries automatically before raising.
- Rate limits and Idempotency. Server-side contract for 429 and 409.
- Authentication with the SDK. Constructing the client, rotating tokens.