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 for the full contract.

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 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:

curl -H "X-Api-Key: $TOKEN" \
     https://<your-panel-domain>/panel-api/onestream/ext/profile
$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);
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:

{
  "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:

curl -H "X-Api-Key: $TOKEN" \
     https://<your-panel-domain>/panel-api/onestream/ext/packages
$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);
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):

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

GET /ext/bouquets

Request:

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

Response:

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

GET /ext/lines

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

Request:

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

Response (plain array of line objects):

[
  {
    "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:

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

Response:

{
  "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:

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

Response (200):

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

Response (404 when no line matches):

{"error": "Not Found"}

POST /ext/line/create

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

Request:

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
$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);
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:

{
  "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:

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:

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
$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);
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:

{
  "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:

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:

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

POST /ext/line/{uuid}/terminate

Delete the line. Irreversible.

Request:

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
$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);
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:

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

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:

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.

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:

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

Response (plain array, may be empty):

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

POST /ext/user/create

Create a sub-reseller under the caller.

Request:

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:

{"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.

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:

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:

{"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:

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

Response when the transaction exists:

{
  "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:

{"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.

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.

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.

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.

/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.

/ext/live_connections/* is not exposed

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

{"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