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. |
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. |
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 | The panel encountered an internal error. The SDK already retried before raising this. |
ServiceUnavailableException |
503 | The API is disabled or temporarily unavailable at the panel level. |
NetworkException |
— | Connection refused, DNS failure, timeout. The panel was never reached. |
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.0.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 message on the exception tells you which scope was expected (missing scope: lines:write). Add that scope in the panel UI, then rotate to a new key.
Distinguishing network errors from API errors
NetworkException covers everything that happens BEFORE the panel handles the request — connect timeout, DNS failure, TLS handshake failure, dropped connection mid-request. The status code and slug are null because there is no response.
PanelApiException and its non-NetworkException subclasses cover responses the panel actually sent. Status code is a real HTTP status, slug is populated.
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
For debugging, both SDKs let you attach a callback that receives the raw request/response for every call:
<?php
$client = new PanelApiClient(
baseUrl: 'https://<your-panel-domain>',
token: '<your-api-key>',
onResponse: function ($request, $response) {
error_log("HTTP {$response->status} — {$request->method} {$request->path}");
},
);
def log_hook(request, response):
print(f"HTTP {response.status} — {request.method} {request.path}")
client = PanelApiClient(
base_url="https://<your-panel-domain>",
token="<your-api-key>",
on_response=log_hook,
)
The hook receives the same request/response pair that produced the exception, so you can log the full round-trip when an unexpected error surfaces.
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.