What these SDKs are

The Panel API is a plain HTTP + JSON service. You can call it from any language that speaks HTTP, and the Quickstart shows exactly that with raw curl. But for teams building against the API in PHP or Python, we ship two official SDKs so you do not have to reinvent the plumbing every panel does the same way.

Each SDK is a thin wrapper around the same JSON API described in the Overview. It gives you a proper client object, typed request and response models, typed exceptions for every documented failure mode, automatic retry with exponential backoff on transient errors, and automatic idempotency for writes. Both are MIT licensed and hosted on GitHub. PHP lives at github.com/Xtream-AI/api-panel-php-sdk and Python at github.com/Xtream-AI/api-panel-python-sdk.

What the SDKs give you

Compared to hand-rolling curl calls, the SDKs remove a class of chores that are always the same and always easy to get wrong. Here is the concrete list, with why each one matters in practice.

Feature What it does What it saves you
Typed models Responses come back as objects with typed fields ($line->expDate, line.exp_date) instead of raw associative arrays. You get IDE autocompletion, static analysis, and a compile-time error the moment the API adds or renames a field. No more $data['exp_date'] ?? null scattered through your code.
Typed exceptions Every documented error slug maps to a concrete exception class (InsufficientCreditsException, RateLimitException, ValidationException, and so on). You catch exactly the failure you care about, and you do not have to read every response body looking for an error.slug key.
Automatic retry 5xx responses and network errors are retried with exponential backoff. 429 honors the server's Retry-After header. A one-second network hiccup does not become a failed invoice in your billing system. You do not implement retry loops in ten different places.
Automatic idempotency Every write generates a UUID Idempotency-Key and reuses it across internal retries. You can also pass your own business key. A retry that reaches the server twice never creates two lines or charges the reseller twice.
Cursor pagination list() returns a PaginatedResponse with the items plus the opaque cursor for the next page. You iterate pages with a clean while loop instead of parsing next_cursor out of raw JSON.
MIT licensed Both repositories are MIT. Fork them, vendor them, ship them inside your own product. No license review meeting.

If you like the shape of what the SDKs give you but you work in a different language, the Overview documents the wire format and every doc page ends with a curl example. You have all you need to write your own thin client.

Install

PHP

Requirements: PHP 8.1 or newer, the ext-curl extension, the ext-json extension. No other runtime dependencies. These apply to both install options below.

You can install the PHP SDK two ways. Pick based on how the rest of your project handles dependencies.

Option A. Manual install (no Composer required)

This is the simplest path if your codebase does not already use Composer. It is common in older projects, in one-off scripts, or in server-side integrations that just need to talk to the panel from a single file.

  1. Download the release ZIP: v1.0.0 archive.
  2. Extract it next to your script. You will get a folder named api-panel-php-sdk-1.0.0/.
  3. Require the bundled autoloader at the top of your file:
<?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>',
);

That is the whole install. No terminal, no package manager, no lockfile.

The __DIR__ in the require line matters. It resolves to the directory of the file being executed, so the require works no matter which working directory PHP is invoked from. If you drop the __DIR__ and write just require 'api-panel-php-sdk-1.0.0/autoload.php', a cron running from / will fail with a "file not found" error the first time it fires.

Option B. Composer

If your project already uses Composer, this is the natural choice. The package is distributed over its Git repository (it is not published on Packagist), so you register the VCS repository first, then require the package.

composer config repositories.xtream-ai vcs https://github.com/Xtream-AI/api-panel-php-sdk
composer require xtream-ai/api-panel-php-sdk

From then on your code uses the usual Composer autoloader:

<?php
require 'vendor/autoload.php';

use XtreamAI\PanelApi\PanelApiClient;

$client = new PanelApiClient(
    baseUrl: 'https://<your-panel-domain>',
    token:   '<your-api-key>',
);

Python

Requirements: Python 3.10 or newer, and the requests library (2.28 or newer). If you use a modern virtual environment, requests gets installed for you as a transitive dependency.

Install straight from GitHub. The package is not on PyPI yet, so pip pulls the tagged release from the repository:

pip install "git+https://github.com/Xtream-AI/api-panel-python-sdk.git@v1.0.0"

If you pin dependencies with requirements.txt, add this line:

xtream-ai-api-panel-sdk @ git+https://github.com/Xtream-AI/api-panel-python-sdk.git@v1.0.0

Then import the client the usual way:

from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(
    base_url="https://<your-panel-domain>",
    token="<your-api-key>",
)

Quickstart with the SDK

The smallest thing you can do with either SDK is ask the panel who you are. It is one HTTP call, it needs no data, and it tells you at a glance that your token works and that the panel is reachable.

If you installed with Composer, use require 'vendor/autoload.php'; instead of the autoload line below.

<?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>',
);

$me = $client->me->get();
echo "Authenticated as: ", $me->type, " (key ", $me->keyPrefix, ")", PHP_EOL;
echo "Scopes: ",           implode(', ', $me->scopes),           PHP_EOL;
from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(
    base_url="https://<your-panel-domain>",
    token="<your-api-key>",
)

me = client.me.get()
print(f"Authenticated as: {me.type} (key {me.key_prefix})")
print("Scopes:", ", ".join(me.scopes))

Run either script and you should see something like this. Your key prefix and scopes will differ.

Authenticated as: admin (key pk_live_abc7fake123)
Scopes: lines:read, lines:write, packages:read, bouquets:read, streams:read, vods:read, resellers:read, resellers:write, subresellers:write

If that call returns without an exception, your credentials are correct, your key has at least the me endpoint reachable, and the panel is up. From here you can move on to real work in the Quickstart guide, which walks through creating your first line end to end.

Resources

The client groups methods under one attribute per resource. PHP uses camelCase method names. Python uses snake_case. Everything else is identical. Below is the SDK-level reference. For the underlying HTTP shapes and error slugs, see the resource pages listed in See also.

Lines

Everything you can do to a customer line: create, list, read, update, enable, disable, renew, reset the password, delete, and inspect active connections.

Method Returns What it does
lines.create(...) Line Creates a new line and returns the persisted object.
lines.list(...) PaginatedResponse<Line> Lists lines. Filterable by username, password, enabled, is_trial. Cursor pagination.
lines.get(id) Line Returns one line by numeric ID.
lines.update(id, ...) Line Updates fields on an existing line. Only fields you pass are sent.
lines.enable(id) Line Enables a disabled line.
lines.disable(id) Line Disables an enabled line without deleting it.
lines.renew(id, packageId?) Line Extends the expiry, optionally switching package.
lines.resetPassword(id) / reset_password(id) string Rotates the password and returns the new one as a plain string. Note that this is the one method that does NOT return a Line. If you need the full object after, call lines.get(id).
lines.delete(id) bool Deletes the line. Returns true when the panel confirms the row is gone.
lines.connections(id) list[Connection] Returns currently active connections for the line. Plain list, no cursor.

Catalog

Read-only endpoints that describe what your panel sells and streams. Packages and bouquets are small enough that they come back as plain lists. Streams and VODs are paginated.

Method Returns What it does
catalog.packages() list[Package] All packages available to the caller.
catalog.bouquets() list[Bouquet] All bouquets.
catalog.streams(...) PaginatedResponse<Stream> Lists live streams. Filterable by category and search string.
catalog.stream(id) Stream Returns one stream by ID.
catalog.vods(...) PaginatedResponse<Vod> Lists VOD items. Filterable by category and search string.
catalog.vod(id) Vod Returns one VOD by ID.

Resellers

Manage the reseller tree and adjust reseller billing. Only admins can create resellers. A reseller with subresellers:write can create sub-resellers under themselves.

Method Returns What it does
resellers.list(...) PaginatedResponse<Reseller> Lists resellers you can see. Filterable by group, status, username.
resellers.create(...) Reseller Creates a reseller (admin) or a sub-reseller (reseller with the right scope).
resellers.get(id) Reseller Returns one reseller by ID.
resellers.update(id, fields) Reseller Updates the given fields on the reseller.
resellers.billing(id) BillingSnapshot Current credit balance, cap, active users, mode.
resellers.adjustBilling(id, delta, reason?) / adjust_billing(...) BillingSnapshot Adds or removes credits (negative delta deducts). Ideal for wiring your billing system into the panel.

Identity

One tiny resource that returns who the current API key belongs to. Useful for smoke tests and for building UIs that display "connected as X".

Method Returns What it does
me.get() Identity Returns the caller's type, group, scopes, key prefix, and (for resellers) a billing snapshot.

Health

Not a resource, just a top-level helper on the client. It calls the public probe with no authentication. Handy for monitoring and for the very first sanity check when a new panel comes online.

Method Returns What it does
client.health() dict Returns the panel's health payload. No auth required.

Pagination

The list endpoints use cursor pagination. Every list() call gives you a PaginatedResponse with two things: the items on the current page, and an opaque next_cursor string that fetches the next page. When there are no more results, the cursor comes back as null in PHP and None in Python.

The SDK does not loop for you. That is deliberate. Many real integrations do not want the whole set. They stop as soon as they find a match, or after they have collected enough rows to render one screen. You drive the loop, and you stop when it suits your use case.

Always pass the cursor back verbatim. It is opaque to your code, and the panel is free to change what it encodes in future releases.

<?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>',
);

$page  = $client->resellers->list(limit: 3);
$total = 0;
$pages = 0;

while (true) {
    $pages++;
    $total += count($page->items);
    echo "Page {$pages}: ", count($page->items), " resellers.", PHP_EOL;
    if ($page->nextCursor === null) {
        break;
    }
    $page = $client->resellers->list(limit: 3, cursor: $page->nextCursor);
}

echo "Total: {$total} resellers across {$pages} pages.", PHP_EOL;
from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(
    base_url="https://<your-panel-domain>",
    token="<your-api-key>",
)

page  = client.resellers.list(limit=3)
total = 0
pages = 0

while True:
    pages += 1
    total += len(page.items)
    print(f"Page {pages}: {len(page.items)} resellers.")
    if page.next_cursor is None:
        break
    page = client.resellers.list(limit=3, cursor=page.next_cursor)

print(f"Total: {total} resellers across {pages} pages.")

Running the PHP script against a test panel with a handful of resellers produced:

Page 1: got 3 resellers. next=yes
Page 2: got 3 resellers. next=yes
Page 3: got 3 resellers. next=yes
Page 4: got 3 resellers. next=yes
Page 5: got 3 resellers. next=yes
Total collected: 15 resellers over 5 pages

The Python version produced the same output. Small limit values are fine, and often desirable, because they keep memory bounded and let the caller react to results as they come.

Idempotency

Every mutating call (create, update, enable, disable, renew, reset, delete, adjust billing) generates a random UUID v4 and sends it as the Idempotency-Key header. If the SDK has to retry that call internally because of a 5xx, a 429, or a network blip, it reuses the same key. The panel remembers the result of the first successful attempt and replays it verbatim. You never create two lines from one call.

That default is enough for interactive uses. But if the call is being triggered by an event that can fire more than once (a payment webhook, a job queue with at-least-once delivery, a WHMCS invoice hook), you should override the key with a value derived from a stable business identifier. Then even a retry that comes from outside the SDK, hours later, resolves to a single side effect instead of two.

<?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>',
);

$first  = $client->lines->create(
    packageId: 76,
    memberId:  260595,
    username:  'newcustomer',
    idempotencyKey: 'invoice-INV-123',
);
$second = $client->lines->create(
    packageId: 76,
    memberId:  260595,
    username:  'newcustomer',
    idempotencyKey: 'invoice-INV-123',
);

echo "first  id=", $first->id,  PHP_EOL;
echo "second id=", $second->id, PHP_EOL;
echo "same result? ", ($first->id === $second->id ? "YES" : "NO"), PHP_EOL;
from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(
    base_url="https://<your-panel-domain>",
    token="<your-api-key>",
)

first = client.lines.create(
    package_id=76, member_id=260595, username="newcustomer",
    idempotency_key="invoice-INV-123",
)
second = client.lines.create(
    package_id=76, member_id=260595, username="newcustomer",
    idempotency_key="invoice-INV-123",
)

print(f"first  id={first.id}")
print(f"second id={second.id}")
print("same result?", "YES" if first.id == second.id else "NO")

Running the PHP version against a test panel produced:

first  id=172511984 username=docsdemo_706819
second id=172511984 username=docsdemo_706819
same result? YES

The second call did not create a new line. The panel recognised the key from the first call and replayed the exact same response, including the same numeric id. If you had used a fresh random UUID for the second call, you would have gotten a second line and a second credit deduction.

For the full server-side contract, including how the panel handles a genuinely conflicting retry (idempotency_conflict) and how it resolves a race between two concurrent requests using the same key (idempotency_in_flight), see Rate limits and Idempotency.

Retry policy

The SDK retries transient failures for you. A 5xx response, a network error, and a dropped connection are all considered transient. The SDK waits, then tries again, with exponential backoff between attempts. The default is up to three additional attempts on top of the original request. If none of them succeeds, the underlying exception surfaces to your code.

A 429 response is handled specially. The panel returns the standard Retry-After header telling you how many seconds to wait before trying again. The SDK reads that header (capped at 60 seconds so a hostile header cannot stall your process for hours) and sleeps for exactly that long before its next attempt.

A 4xx response other than 429 is never retried. Those errors mean the request itself is wrong (bad body, missing scope, unknown line ID). Retrying without changing the request would just produce the same failure. That is your bug to fix, not a transient condition to wait out.

Both the per-request timeout and the retry count are set once at construction. Sensible defaults are timeout=30.0 seconds and maxRetries=3, but you can raise them for slow networks or lower them for jobs that need to fail fast.

Exceptions

The SDK's exception hierarchy is designed so you can be as broad or as narrow as you want. Catch the base class PanelApiException and you handle every API failure in one place. Catch a specific subclass and you handle exactly one condition (for example "reseller ran out of credits") while letting everything else bubble up.

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.
UnknownApiException any other Fallback for status codes the SDK does not recognise.

Every exception carries the same context so you can log and act on it without any extra plumbing.

  • PHP: $e->slug, $e->getMessage(), $e->requestId, $e->details, $e->statusCode. ValidationException adds a ->field() helper. RateLimitException adds ->retryAfter (seconds).
  • Python: .slug, str(e), .request_id, .details, .status_code. ValidationException adds .field. RateLimitException adds .retry_after.

A typical try block groups the ones you have a specific response for, and lets the rest hit the base class handler:

<?php
use XtreamAI\PanelApi\Exceptions\{
    InsufficientCreditsException,
    RateLimitException,
    ValidationException,
    PanelApiException,
};

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.
} 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.
} catch (PanelApiException $e) {
    // Anything else. Log $e->requestId and surface a generic error.
}
from xtream_ai_panel_api.exceptions import (
    InsufficientCreditsException,
    RateLimitException,
    ValidationException,
    PanelApiException,
)
import time

try:
    line = client.lines.create(
        package_id=42, member_id=260595, username="johndoe",
    )
except InsufficientCreditsException:
    pass  # Reseller ran out. Tell them to top up.
except RateLimitException as e:
    time.sleep(e.retry_after)
except ValidationException as e:
    print("invalid field:", e.field)
except PanelApiException as e:
    print("unexpected api error:", e.request_id, e.slug)

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.

The complete slug catalogue, with a remediation note for each one, lives in Errors.

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, because each request already runs in its own process. Under a long-lived worker (a queue consumer, a background daemon, a Python service using threads), constructing one client at startup and sharing it across your workers is the recommended pattern.

Migrating from Xtream Codes or OneStream

These SDKs speak only the native /panel-api/v1/* dialect. If your integration was written for the Xtream Codes / XUI.one player API, or for the OneStream ext API, you do not need to adopt these SDKs to point your integration at an Xtream AI panel. The panel speaks both wire formats natively. In most cases you only need to change the base URL and the API key.

Reach for the PHP or Python SDK when you are building something new against the native dialect, or when you want the typed models and typed exceptions without writing that plumbing yourself.

See also

  • Quickstart. Your first line, end to end, with either raw curl or the SDK.
  • Common tasks. Recipes for the operations you will run most often.
  • Authentication. Token shape, scopes, and IP allow-lists.