---
title: "Panel API OneStream Compatibility"
description: "How existing OneStream integrations point at Xtream AI without a rewrite: the full /ext/* endpoint mapping, opaque line UUIDs, rid-based idempotency, and the handful of known differences."
---

## The promise

If you already have code that talks to OneStream, this is how it works with Xtream AI. Changing the base URL is the only mandatory change: every documented OneStream `/ext/*` endpoint is supported by the Panel API with the same request body, the same response shape, the same `line_id` UUID contract, and the same `rid` idempotency semantics. Your SDKs and internal tooling keep working. This page lists every mapped endpoint, the fields that carry over, and the handful of intentional differences you should know about before you migrate.

## Base URL and authentication

Change the base URL. Keep your header.

**Old (OneStream):**

```
https://onestream.example.com/ext/*
X-Api-Key: <token>
```

**New (Xtream AI):**

```
https://<your-xtream-ai-panel-domain>/panel-api/onestream/ext/*
X-Api-Key: <token>
```

`X-Api-Key` is honored exactly as OneStream does. If your SDK has since standardized on `Authorization: Bearer <token>`, that works too. The legacy `X-Auth-User` header from earlier OneStream releases is also accepted as the token carrier, so you don't have to touch client code that still sends it.

Get an API key from **Settings, Panel API Keys** in the Xtream AI panel. See [Authentication](/docs/?page=panel-api-authentication) for the full contract.

```bash
curl -H "X-Api-Key: pk_live_x2fakekey423.Fk3xAmpLe..." \
     https://<your-panel-domain>/panel-api/onestream/ext/profile
```

## The full endpoint mapping

Every OneStream `/ext/*` endpoint the dialect supports, side by side with what it does internally. All request bodies are JSON, all responses are JSON without an envelope (no `{status,data:...}` wrapper unless a specific endpoint explicitly returns one).

| OneStream endpoint | Purpose | Notes |
|---|---|---|
| `GET /ext/profile` | Identity of the caller. | Returns `id`, `name`, `username`, `is_admin`, `credits`. |
| `GET /ext/packages` | List packages the caller can sell. | Returns a plain JSON array. |
| `GET /ext/bouquets` | List bouquets the caller can attach to lines. | Returns a plain JSON array. |
| `GET /ext/lines` | List lines, plain array. | Accepts `?username=`, `?per_page=`, `?page=`. See the pagination note in the gotchas section. |
| `GET /ext/lines/index` | List lines with a pagination envelope. | Returns `{status, data:{pagination, items}}`. |
| `GET /ext/line/find` | Look up a line by username (and optional password). | Returns `{line_id}` or HTTP 404. |
| `POST /ext/line/create` | Create a line from a package. | Body carries `package`, `username`, `password`, `bouquets`, `reseller_notes`, `max_connections`, `rid`. |
| `POST /ext/line/create-advanced` | Create a line with advanced fields. | Adds `expire_at` (ISO 8601) and `can_watch_adult` on top of `create`. |
| `POST /ext/line/{uuid}/renew` | Extend a line by another package cycle. | Body carries the target `package` and `rid`. |
| `POST /ext/line/{uuid}/enable` | Re-enable a disabled line. | Body may carry only `rid`. |
| `POST /ext/line/{uuid}/disable` | Disable an active line without deleting it. | Body may carry only `rid`. |
| `POST /ext/line/{uuid}/terminate` | Delete a line. | Irreversible. Body may carry only `rid`. |
| `POST /ext/line/{uuid}/update-advanced` | Update `password`, `expire_at`, `is_enabled`, `is_restreamer`, or `max_connections`. | Fields OneStream sends that this API doesn't accept (`username`, `bouquets`, `reseller_notes`, `is_trial`) are rejected explicitly rather than accepted silently. |
| `GET /ext/user/find` | Look up a sub-reseller. | Accepts `?name=` (translated to username lookup). |
| `POST /ext/user/create` | Create a sub-reseller under the caller. | Same field names as OneStream, including `password_confirmation`. |
| `POST /ext/user/{id}/update` | Update a sub-reseller. | Same shape as create. |
| `POST /ext/user/{id}/credit` | Adjust a sub-reseller's credits. | `credits` becomes the delta, `note` becomes the audit reason. |
| `GET /ext/transaction/{rid}` | Look up a previously idempotency-keyed transaction. | Returns the original response so you can safely reconcile after a network failure. |

Every write endpoint (POST) supports the `rid` body field for idempotency. See [Idempotency via rid](#idempotency-via-rid) below.

## Endpoint reference with examples

### GET /ext/profile

Identity of the caller. Use it to verify the key is working and to read the current credits balance for a reseller.

**Request:**

```bash
curl -H "X-Api-Key: $TOKEN" \
     https://<your-panel-domain>/panel-api/onestream/ext/profile
```

```php
$ch = curl_init('https://<your-panel-domain>/panel-api/onestream/ext/profile');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-Key: ' . $token]);
$body = json_decode(curl_exec($ch), true);
```

```python
import requests

r = requests.get(
    "https://<your-panel-domain>/panel-api/onestream/ext/profile",
    headers={"X-Api-Key": token},
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

**Response:**

```json
{
  "id": 7,
  "name": "alice",
  "username": "alice",
  "is_admin": false,
  "credits": 250.5
}
```

For admin keys, `id`, `name`, and `username` may be empty strings or zero. Admin keys are panel-scoped rather than tied to a specific user record.

### GET /ext/packages

Every package the caller can sell.

**Request:**

```bash
curl -H "X-Api-Key: $TOKEN" \
     https://<your-panel-domain>/panel-api/onestream/ext/packages
```

```php
$ch = curl_init('https://<your-panel-domain>/panel-api/onestream/ext/packages');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-Key: ' . $token]);
$packages = json_decode(curl_exec($ch), true);
```

```python
import requests

packages = requests.get(
    "https://<your-panel-domain>/panel-api/onestream/ext/packages",
    headers={"X-Api-Key": token},
    timeout=30,
).json()
```

**Response** (plain array, no envelope):

```json
[
  {"id": 1, "name": "Basic", "official_credits": 100, "official_duration": 30},
  {"id": 2, "name": "Pro",   "official_credits": 200, "official_duration": 30}
]
```

### GET /ext/bouquets

**Request:**

```bash
curl -H "X-Api-Key: $TOKEN" \
     https://<your-panel-domain>/panel-api/onestream/ext/bouquets
```

**Response:**

```json
[
  {"id": 5, "name": "Sports"},
  {"id": 6, "name": "Movies"}
]
```

### GET /ext/lines

List lines. Accepts `username`, `per_page`, and `page` as query parameters.

**Request:**

```bash
curl -H "X-Api-Key: $TOKEN" \
     "https://<your-panel-domain>/panel-api/onestream/ext/lines?per_page=50"
```

**Response** (plain array of line objects):

```json
[
  {
    "line_id": "3b91c74a-2d6e-4218-8f5c-1234deadbeef",
    "username": "foo",
    "password": "p1",
    "expire_at": "2025-02-18T00:00:00+00:00",
    "is_enabled": true,
    "is_restreamer": false,
    "is_trial": false,
    "package_id": null,
    "bouquets": [1, 2],
    "max_connections": 2,
    "reseller_notes": "",
    "mac_addr": null,
    "owner": "billing",
    "type": "regular"
  },
  {
    "line_id": "5f14e08b-a731-4c31-8bcd-2345feedface",
    "username": "bar",
    "password": "p2",
    "expire_at": null,
    "is_enabled": false,
    "is_restreamer": true,
    "is_trial": true,
    "..." : "..."
  }
]
```

Notes:

- `expire_at: null` means the line has no expiration; nothing else.
- `package_id` is currently null. If your integration needs the source package for a line, store it locally when you create the line.
- `mac_addr` is always null (the product doesn't provision physical devices).

### GET /ext/lines/index

Same data as `/ext/lines`, wrapped in an envelope with pagination metadata.

**Request:**

```bash
curl -H "X-Api-Key: $TOKEN" \
     "https://<your-panel-domain>/panel-api/onestream/ext/lines/index?per_page=50&page=1"
```

**Response:**

```json
{
  "status": "success",
  "data": {
    "pagination": {
      "current_page": 1,
      "per_page": 50,
      "total": 50,
      "has_more": true
    },
    "items": [
      {"line_id": "3b91c74a-2d6e-4218-8f5c-1234deadbeef", "...": "..."}
    ]
  }
}
```

`has_more: true` means there are more pages. See the pagination note in the gotchas section for the correct way to walk them.

### GET /ext/line/find

Look up a line by username. The response shape is specifically `{line_id}`, not a full line object. Use it as a cheap check for "does this line exist and what is its opaque ID?"

**Request:**

```bash
curl -H "X-Api-Key: $TOKEN" \
     "https://<your-panel-domain>/panel-api/onestream/ext/line/find?username=foo"
```

**Response** (200):

```json
{"line_id": "3b91c74a-2d6e-4218-8f5c-1234deadbeef"}
```

**Response** (404 when no line matches):

```json
{"error": "Not Found"}
```

### POST /ext/line/create

Create a line from a package. The classic OneStream field names are honored.

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "package": 3,
       "username": "newuser",
       "password": "12345",
       "bouquets": [2],
       "reseller_notes": "test",
       "rid": "onestream-idem-1"
     }' \
     https://<your-panel-domain>/panel-api/onestream/ext/line/create
```

```php
$ch = curl_init('https://<your-panel-domain>/panel-api/onestream/ext/line/create');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: ' . $token,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'package'         => 3,
        'username'        => 'newuser',
        'password'        => '12345',
        'bouquets'        => [2],
        'reseller_notes'  => 'test',
        'rid'             => bin2hex(random_bytes(16)),
    ]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
```

```python
import requests, uuid

r = requests.post(
    "https://<your-panel-domain>/panel-api/onestream/ext/line/create",
    headers={"X-Api-Key": token, "Content-Type": "application/json"},
    json={
        "package": 3,
        "username": "newuser",
        "password": "12345",
        "bouquets": [2],
        "reseller_notes": "test",
        "rid": uuid.uuid4().hex,
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

**Response:**

```json
{
  "line_id": "b32c1a04-11ea-4c67-8fd1-0001000000640",
  "expire_at": "2025-02-18T00:00:00+00:00",
  "transaction_amount": 100,
  "rid": "onestream-idem-1"
}
```

Field translations under the hood:

| OneStream body field | Internal field |
|---|---|
| `package` or `package_id` | `package_id` |
| `username` | `username` |
| `password` | `password` |
| `member_id` (admin keys only) | `member_id` |
| `reseller_notes` | `notes` |
| `bouquets` | `bouquets` |
| `max_connections` | `max_connections` |
| `mac_addr` | ignored (see gotchas) |

### POST /ext/line/create-advanced

Same as `create`, plus `expire_at` (ISO 8601) and `can_watch_adult` (boolean).

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "package": 3,
       "username": "vip1",
       "password": "s3cret",
       "expire_at": "2026-06-01T00:00:00+00:00",
       "can_watch_adult": true,
       "rid": "adv-1"
     }' \
     https://<your-panel-domain>/panel-api/onestream/ext/line/create-advanced
```

**Response:** identical to `create`, with `expire_at` reflecting the explicit value you passed.

If `expire_at` is not a valid ISO 8601 timestamp or a positive unix integer, the API rejects the request with `422` rather than silently falling back to the package's default duration.

### POST /ext/line/{uuid}/renew

Extend a line by another package cycle. `{uuid}` is the opaque UUID returned when the line was created (or by `/ext/lines`, `/ext/line/find`).

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"package": 3, "rid": "renew-2026-02-01-user-foo"}' \
     https://<your-panel-domain>/panel-api/onestream/ext/line/b32c1a04-11ea-4c67-8fd1-0001000000640/renew
```

```php
$uuid = 'b32c1a04-11ea-4c67-8fd1-0001000000640';
$ch = curl_init("https://<your-panel-domain>/panel-api/onestream/ext/line/{$uuid}/renew");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: ' . $token,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'package' => 3,
        'rid'     => 'renew-' . date('Y-m-d') . '-user-foo',
    ]),
]);
$body = json_decode(curl_exec($ch), true);
```

```python
import requests

uuid_ = "b32c1a04-11ea-4c67-8fd1-0001000000640"
r = requests.post(
    f"https://<your-panel-domain>/panel-api/onestream/ext/line/{uuid_}/renew",
    headers={"X-Api-Key": token, "Content-Type": "application/json"},
    json={"package": 3, "rid": "renew-2026-02-01-user-foo"},
    timeout=30,
)
r.raise_for_status()
```

**Response:**

```json
{
  "line_id": "b32c1a04-11ea-4c67-8fd1-0001000000640",
  "expire_at": "2025-03-04T00:00:00+00:00",
  "transaction_amount": 100,
  "rid": "renew-2026-02-01-user-foo"
}
```

### POST /ext/line/{uuid}/enable, /disable

Re-enable a disabled line, or disable it without deleting.

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"rid": "enable-1"}' \
     https://<your-panel-domain>/panel-api/onestream/ext/line/b32c1a04-.../enable
```

**Response:**

```json
{"line_id": "b32c1a04-11ea-4c67-8fd1-0001000000640"}
```

### POST /ext/line/{uuid}/terminate

Delete the line. Irreversible.

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"rid": "terminate-user-foo"}' \
     https://<your-panel-domain>/panel-api/onestream/ext/line/b32c1a04-.../terminate
```

```php
$uuid = 'b32c1a04-11ea-4c67-8fd1-0001000000640';
$ch = curl_init("https://<your-panel-domain>/panel-api/onestream/ext/line/{$uuid}/terminate");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: ' . $token,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['rid' => 'terminate-user-foo']),
]);
$body = json_decode(curl_exec($ch), true);
```

```python
import requests

uuid_ = "b32c1a04-11ea-4c67-8fd1-0001000000640"
r = requests.post(
    f"https://<your-panel-domain>/panel-api/onestream/ext/line/{uuid_}/terminate",
    headers={"X-Api-Key": token, "Content-Type": "application/json"},
    json={"rid": "terminate-user-foo"},
    timeout=30,
)
r.raise_for_status()
```

**Response:**

```json
{"line_id": "b32c1a04-11ea-4c67-8fd1-0001000000640"}
```

> [!IMPORTANT]
> These endpoints only accept `POST`. A `GET` request to `/ext/line/{uuid}/terminate` (or `/renew`, `/enable`, `/disable`, `/update-advanced`) returns `501 not_implemented`. This is deliberate: it stops browser prefetch, link scanners, and misbehaving retry loops from destroying lines by walking URLs.

### POST /ext/line/{uuid}/update-advanced

Change `password`, `expire_at`, `is_enabled`, `is_restreamer`, or `max_connections` on an existing line.

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "is_enabled": false,
       "max_connections": 3,
       "expire_at": "2026-12-31T00:00:00+00:00",
       "rid": "upd-1"
     }' \
     https://<your-panel-domain>/panel-api/onestream/ext/line/b32c1a04-.../update-advanced
```

**Response:** the full line object (same shape as a `/ext/lines` element).

Fields you may pass that this API does not currently update are rejected up front with `422 validation_error` rather than accepted and silently ignored: `username`, `bouquets`, `reseller_notes`, `is_trial`.

To turn off a line's expiration without touching anything else, pass `expire_at: null` explicitly.

> [!NOTE]
> Passing `expire_at: 0` (the unix epoch) is treated as a validation error rather than as "no expiration". The engine considers `exp_date = 0` an expired line, which was not the intent of most OneStream callers; the correct way to remove an expiration is `expire_at: null`.

### GET /ext/user/find

Look up a sub-reseller by username. Passed as `name` in OneStream, translated to `username` internally.

**Request:**

```bash
curl -H "X-Api-Key: $TOKEN" \
     "https://<your-panel-domain>/panel-api/onestream/ext/user/find?name=reseller1"
```

**Response** (plain array, may be empty):

```json
[
  {"id": 50, "username": "reseller1"}
]
```

### POST /ext/user/create

Create a sub-reseller under the caller.

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "newres",
       "email": "newres@example.com",
       "password": "topsecret",
       "password_confirmation": "topsecret",
       "credits": 100,
       "notes": "created by billing",
       "rid": "user-create-1"
     }' \
     https://<your-panel-domain>/panel-api/onestream/ext/user/create
```

**Response:**

```json
{"id": 5555, "rid": "user-create-1"}
```

If `password` and `password_confirmation` do not match, the API returns `422 validation_error` before it hits the reseller creation path.

Sub-resellers created through this endpoint go into the panel's default member group. To place them in a specific group, pass `member_group_id` explicitly, or use the native `/panel-api/v1/resellers` endpoint.

### POST /ext/user/{id}/update

Update a sub-reseller's fields. Same body shape as `create`.

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"notes": "reactivated after payment", "rid": "upd-res-1"}' \
     https://<your-panel-domain>/panel-api/onestream/ext/user/50/update
```

### POST /ext/user/{id}/credit

Adjust a sub-reseller's credits. `credits` is the delta (positive to add, negative to subtract). `note` is stored as the audit reason.

**Request:**

```bash
curl -X POST \
     -H "X-Api-Key: $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"credits": 25.5, "note": "topup by billing", "rid": "credit-1"}' \
     https://<your-panel-domain>/panel-api/onestream/ext/user/50/credit
```

**Response:**

```json
{"id": 50, "rid": "credit-1"}
```

## Line UUIDs (opaque)

Every line ID in the OneStream dialect is a UUID, not an integer. Xtream AI preserves this contract exactly: line IDs are UUIDs in every response, and every write endpoint accepts them in the URL path.

Under the hood, the UUID is a signed opaque identifier: it encodes the internal integer line ID together with an HMAC signature scoped to the panel's license. The important consequences:

- **Stable across time.** The same line always maps to the same UUID as long as the panel's opaque secret does not rotate. Store the UUID next to your own records; do not re-derive it.
- **Not the same shape as any internal ID.** Do not try to decode it. It is not a bijection with any user-visible number.
- **HMAC-signed, so forgery is rejected.** A hand-crafted UUID (even a valid-looking one) that was not issued by this panel is rejected with `422 line_id not found or invalid`. The same error is returned for a malformed UUID, an unknown UUID, and a UUID that was issued by a different panel, so the response cannot be used as an oracle to discover which lines exist.
- **Scoped to the issuing panel.** A UUID issued by panel A does not resolve on panel B, even for the same underlying line number. You cannot transport UUIDs across tenants.

If your integration needs to cross-reference UUIDs with your own internal IDs, save the UUID at line-creation time alongside your record. There is no batch decode endpoint.

## Idempotency via `rid`

OneStream uses a `rid` (request or transaction identifier) in the body of every write. Xtream AI honors this exactly.

**The contract:**

- Pass a unique `rid` per operation. UUIDs, ULIDs, or any string up to 255 characters work.
- If the same `rid` is retried with the same body, you receive the original response verbatim, including the original HTTP status and body. Side effects run at most once.
- If the same `rid` is retried with a different body, you receive `409 idempotency_conflict` (`Transaction already processed`). The original transaction stays untouched.
- Idempotency records persist for at least the retention window configured on the panel (defaults to several days), which is far longer than any realistic retry loop.

**Recovering a lost response:**

If a POST times out, the network drops, or you never see the response for any other reason, do not blindly retry. First look the transaction up:

```bash
curl -H "X-Api-Key: $TOKEN" \
     https://<your-panel-domain>/panel-api/onestream/ext/transaction/onestream-idem-1
```

**Response when the transaction exists:**

```json
{
  "rid": "onestream-idem-1",
  "transaction_status": "success",
  "http_status": 201,
  "response": {
    "line_id": "b32c1a04-11ea-4c67-8fd1-0001000000640",
    "expire_at": "2025-02-18T00:00:00+00:00",
    "..." : "..."
  }
}
```

**Response when it does not exist:**

```json
{"error": "Not Found"}
```

A 404 from this endpoint is authoritative: the original write never reached the panel. You can safely retry it with the same `rid`.

> [!IMPORTANT]
> Transaction lookups are scoped to the caller. An `rid` belonging to a different API key is indistinguishable from a non-existent one: both return 404 with `{"error":"Not Found"}`. Requests with an unknown or missing key receive the same 404. This is intentional so that an attacker cannot use the endpoint to enumerate other integrators' transactions.

If your OneStream client library sends `rid` for every POST, you are already covered. For more on how idempotency and retries work, see [Rate limits & Idempotency](/docs/?page=panel-api-rate-limits-idempotency).

## Errors

OneStream error responses are a plain object `{"error": "<message>"}` on the appropriate HTTP status code. Xtream AI matches this shape and maps its internal error slugs to the messages your OneStream clients already recognize.

| Xtream AI slug | OneStream message | HTTP status |
|---|---|---|
| `insufficient_credits`, `insufficient_slots` | `Insufficient credits balance` | 402 |
| `idempotency_conflict` | `Transaction already processed` | 409 |
| `not_found` | `Not Found` | 404 |
| `rate_limited` | `Rate limit exceeded` | 429 |
| `api_disabled` | `API disabled` | 403 |
| `validation_error` | the field-level message | 422 |

Errors that carried an `rid` in the request also carry it in the response body under the `rid` key, so you can correlate a failed retry with the original attempt.

For the full error slug catalog and per-endpoint semantics, see [Errors](/docs/?page=panel-api-errors).

## Gotchas and known differences

Read this section before you migrate.

### Pagination beyond page 1

OneStream uses `?page=N&per_page=M` for pagination. Internally, Xtream AI uses cursor-based (keyset) pagination for lines. The dialect translates page-based pagination by computing an offset (`(page - 1) * per_page`), which works well when line IDs are dense (a freshly seeded panel), but **can return the same items as page 1 when line IDs are sparse**, which is the norm for mature panels where lines have been deleted over time.

Two safe patterns:

1. Use `/ext/lines/index` and honor the `has_more` field in the pagination envelope. Iterate until `has_more` is false.
2. Migrate your line-listing flow to the native `GET /panel-api/v1/lines?cursor=<value>` endpoint, which returns a `next_cursor` in the response you pass into the next request. See [Lines](/docs/?page=xai-ref-lines-list).

### `/ext/profile` shape depends on the key

- **Reseller keys** return a fully populated `{id, name, username, is_admin, credits}` object.
- **Admin keys** return the same shape, but `id`, `name`, and `username` may be zero or empty and `credits` is 0. Admin keys are panel-scoped, not tied to a specific user record.

If your integration relies on the profile for identity, use a reseller key for that flow, or move to `GET /panel-api/v1/me` which exposes the caller type explicitly.

### `member_id` on `/ext/line/create` (admin keys)

Admin keys can pass `member_id` in the create-line body to assign the resulting line to a specific reseller. Reseller keys that try to pass `member_id` receive `403 admin_only_field`; the line is always assigned to themselves.

### `expire_at`

- ISO 8601 timestamps (`2026-06-01T00:00:00+00:00`) are the canonical format.
- Unix integer timestamps (as string or number) are accepted for compatibility with clients that never migrated to ISO 8601.
- `null` means "no expiration". The engine distinguishes this from `0` (which it treats as expired). If you want a line with no expiration, pass `null`, never `0`.
- Any other value returns `422 validation_error` at translation time, rather than silently falling back to the package's default duration.

### The `is_enabled` field

OneStream request bodies use `is_enabled`; the internal API uses `enabled`. The dialect translates automatically. Both truthy string values (`"true"`, `"1"`, `"yes"`, `"on"`) and JSON booleans are accepted.

### Bouquets on update

`update-advanced` cannot change a line's bouquets in the current release. Bouquets pass through only on create. Fields that OneStream allows but this endpoint cannot update (`username`, `bouquets`, `reseller_notes`, `is_trial`) are rejected explicitly with `422` rather than accepted silently. If you need to change a line's bouquets after creation, use the native `POST /panel-api/v1/lines/{id}/update` and consult [Lines](/docs/?page=xai-ref-lines-list).

### `/ext/live_connections/*` is not exposed

Requests to `/ext/live_connections/index` (or any path under `/ext/live_connections/`) return `501 not_implemented` with:

```json
{"error": "Global live-connections listing/kill is not exposed by this API. Use per-line connections instead."}
```

This is deliberate. Per-line connection listing is available in the native API at `GET /panel-api/v1/lines/{id}/connections`. There is no global list or global kill.

### MAG and Enigma devices

Xtream AI does not provision physical devices. `mac_addr` on `/ext/line/create` is accepted for backwards compatibility with clients that send it by default and is silently ignored (it is not stored, not indexed, and not returned in any response). If your workflow revolves around MAG or Enigma provisioning, this API is not the right tool.

### `password_confirmation`

`/ext/user/create` accepts `password_confirmation` and verifies it matches `password` when both are supplied. If they mismatch, the request is rejected with `422`. If your client omits `password_confirmation`, the check is skipped.

### GET requests to write endpoints

`GET` requests to `/ext/line/{uuid}/{renew|enable|disable|terminate|update-advanced}` and `/ext/user/{id}/{update|credit}` return `501 not_implemented`. Only `POST` is accepted. This protects against browser prefetch, link scanners, and retry loops that walk URLs.

## Migration checklist

1. Change your OneStream client's base URL to `https://<your-xtream-ai-panel-domain>/panel-api/onestream/`. The `/ext/*` path segment is unchanged.
2. Get an API key from your Xtream AI panel (**Settings, Panel API Keys**). Pick the scopes matching what your integration does.
3. Replace your existing OneStream `X-Api-Key` value with the new token.
4. Run your test suite. Everything except `/ext/live_connections/*` should pass without code changes.
5. If your integration paginates line lists, switch that specific flow to `/ext/lines/index` and honor `has_more`, or migrate it to the native `/panel-api/v1/lines?cursor=<value>` endpoint.
6. Continue to send a unique `rid` for every write. Add a `GET /ext/transaction/{rid}` lookup to your retry code if you don't already have one.

## See also

- [Panel API Overview](/docs/?page=panel-api-overview) — the three dialects and how to pick one.
- [Authentication](/docs/?page=panel-api-authentication) — token shape, scopes, IP allow-lists, key rotation.
- [Rate limits & Idempotency](/docs/?page=panel-api-rate-limits-idempotency) — retry contract, headers, backoff.
- [Errors](/docs/?page=panel-api-errors) — full error slug catalog.
- [Xtream Codes / XUI.one compatibility](/docs/?page=panel-api-xtream-codes-compatibility) — the other compatibility dialect.
- [Common Tasks](/docs/?page=panel-api-migration-recipes) — copy-pasteable examples for the most common flows.
