---
title: "Authentication with the SDK"
description: "Construct a Panel API SDK client, read the caller's identity with me.get(), rotate the token at runtime, and set a custom User-Agent. PHP and Python side by side."
---

# Authentication with the SDK

Both SDKs authenticate with a single Bearer token you pass at construction time. Everything below is a thin ergonomic layer on top of the [HTTP authentication contract](/docs/?page=panel-api-authentication), which documents the token shape, scopes, IP allow-lists, and rotation.

## Constructing the client

The client is the entry point for every call. It holds the base URL, the token, and the transport settings (timeouts, retry count). Construct it once at startup and reuse it — see [Thread safety](#thread-safety) below.

```php
<?php
require __DIR__ . '/api-panel-php-sdk-1.0.0/autoload.php';

use XtreamAI\PanelApi\PanelApiClient;

$client = new PanelApiClient(
    baseUrl:    'https://<your-panel-domain>',
    token:      '<your-api-key>',
    timeout:    30.0,   // per-request seconds, default 30
    maxRetries: 3,      // transient-error retries, default 3
);
```

```python
from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(
    base_url="https://<your-panel-domain>",
    token="<your-api-key>",
    timeout=30.0,       # per-request seconds, default 30
    max_retries=3,      # transient-error retries, default 3
)
```

The base URL must be the panel domain without any path segment. The SDK appends `/panel-api/v1/*` internally — passing `https://panel.example.com/panel-api/v1` results in a double prefix and every request 404s.

## Where the token comes from

The token is a long random string issued from the panel UI. See [Authentication → Getting a token](/docs/?page=panel-api-authentication#getting-a-token) for the exact UI steps and scope selector. In practice you never hard-code it — load it from an environment variable or a secret manager.

```php
<?php
$client = new PanelApiClient(
    baseUrl: getenv('PANEL_BASE_URL')     ?: 'https://<your-panel-domain>',
    token:   getenv('PANEL_API_KEY')      ?: throw new RuntimeException('PANEL_API_KEY missing'),
);
```

```python
import os

client = PanelApiClient(
    base_url=os.environ["PANEL_BASE_URL"],
    token=os.environ["PANEL_API_KEY"],
)
```

Both languages will happily accept a wrong token — you only find out when the first call comes back `401`. Which is why the very first thing a boot script should do is call `me.get()`.

## Reading the caller's identity

`me.get()` returns an `Identity` object with the key's type (`admin` or `reseller`), member group, scope list, key prefix, and — for resellers — a billing snapshot. It costs one HTTP call and is the recommended smoke test after a base-URL swap.

```php
<?php
$me = $client->me->get();

echo "Type:    ", $me->type, PHP_EOL;
echo "Group:   ", $me->memberGroup ?? '(admin, no group)', PHP_EOL;
echo "Scopes:  ", implode(', ', $me->scopes), PHP_EOL;
echo "Prefix:  ", $me->keyPrefix, PHP_EOL;

if ($me->billing !== null) {
    echo "Balance: ", $me->billing->credits, " credits", PHP_EOL;
}
```

```python
me = client.me.get()

print(f"Type:    {me.type}")
print(f"Group:   {me.member_group or '(admin, no group)'}")
print(f"Scopes:  {', '.join(me.scopes)}")
print(f"Prefix:  {me.key_prefix}")

if me.billing is not None:
    print(f"Balance: {me.billing.credits} credits")
```

The `keyPrefix` (`key_prefix`) is a short human-readable hint like `pk_live_abc7…` — safe to log, useful in support tickets, never contains the full token.

## Rotating the token

The token is stored on the client as an immutable property. Rotating means constructing a NEW client with the fresh token — do not try to patch the field in place. This is deliberate: it keeps the client thread-safe (see below), and it makes the swap atomic from the point of view of every in-flight request.

```php
<?php
// Load the new token from your secret store...
$newToken = fetchTokenFromVault('panel_api_key_v2');

// ...and swap the client atomically.
$client = new PanelApiClient(
    baseUrl: $client->baseUrl,
    token:   $newToken,
);
```

```python
new_token = fetch_token_from_vault("panel_api_key_v2")

client = PanelApiClient(
    base_url=client.base_url,
    token=new_token,
)
```

For long-lived workers (a queue consumer, a background daemon), wrap the client in a getter that swaps to a fresh instance when your secret store signals a rotation. The [rotation flow on the panel side](/docs/?page=panel-api-authentication#rotating-a-key) supports zero-downtime by keeping the old key valid for a grace window while you deploy the new one.

## Custom User-Agent

Every SDK request sends a `User-Agent` header with the SDK version and the language. Overriding it is useful when you want your panel-side rate-limit metrics grouped per integration.

```php
<?php
$client = new PanelApiClient(
    baseUrl:   'https://<your-panel-domain>',
    token:     '<your-api-key>',
    userAgent: 'MyBillingApp/2.4 (+ops@example.com)',
);
```

```python
client = PanelApiClient(
    base_url="https://<your-panel-domain>",
    token="<your-api-key>",
    user_agent="MyBillingApp/2.4 (+ops@example.com)",
)
```

The panel accepts the header verbatim and never blocks a request based on it. Include the contact info of whoever owns the integration — support uses it when reaching out about anomalies.

## HTTPS and self-signed certificates

Both SDKs verify TLS by default. The panel ships with a valid certificate on every install, so this Just Works in production. During local development against a panel with a self-signed cert, you can disable verification — but never do this in production:

```php
<?php
$client = new PanelApiClient(
    baseUrl:      'https://panel.local',
    token:        '<your-api-key>',
    verifySsl:    false,   // local dev only
);
```

```python
client = PanelApiClient(
    base_url="https://panel.local",
    token="<your-api-key>",
    verify_ssl=False,   # local dev only
)
```

A better long-term approach: import the panel's certificate into your trust store, and leave verification on.

## Thread safety

The client is safe to share across threads. Once constructed, its internal configuration (base URL, token, timeout, retry count) is read-only. Each request creates its own cURL handle in PHP and its own connection in Python, so two threads calling the same client instance do not step on each other's transport state. You do not need a lock, and you do not need to build a pool of clients.

Under classic mod_php or PHP-FPM there is nothing to think about here — each request already runs in its own process. Under a long-lived worker (queue consumer, background daemon, Python service using threads), construct one client at startup and share it across your workers.

## See also

- [Authentication](/docs/?page=panel-api-authentication). Full HTTP-level auth contract: token shape, scopes, IP allow-lists, rotation flow.
- [Install](/docs/?page=panel-api-sdk-install). Requirements and package install.
- [Errors with the SDK](/docs/?page=panel-api-sdk-errors). What `AuthenticationException` vs `AuthorizationException` actually mean.
- [Retry and Idempotency](/docs/?page=panel-api-sdk-retry-idempotency). How transport-level failures are handled for you.
