---
title: "Errors with the SDK"
description: "The Panel API SDK exception hierarchy. One base class for catch-all handling, one subclass per documented failure mode for precise reactions. Every exception carries request_id, slug, status_code, and details."
---

# 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](/docs/?page=panel-api-errors). Every slug has a remediation note.

> [!IMPORTANT]
> The panel emits `insufficient_slots` with HTTP 422, not 402. Semantically it is the users-billing-mode analog of `insufficient_credits`. The SDK special-cases it and raises `InsufficientCreditsException` in both cases, so a single `catch` handles 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](/docs/?page=panel-api-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
<?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}");
}
```

```python
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](/docs/?page=panel-api-sdk-retry-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
<?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}");
    },
);
```

```python
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](/docs/?page=panel-api-errors). The full slug catalogue, one row per documented failure mode.
- [Retry and Idempotency](/docs/?page=panel-api-sdk-retry-idempotency). What the SDK retries automatically before raising.
- [Rate limits and Idempotency](/docs/?page=panel-api-rate-limits-idempotency). Server-side contract for 429 and 409.
- [Authentication with the SDK](/docs/?page=panel-api-sdk-auth). Constructing the client, rotating tokens.
