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: nullmeans the line has no expiration; nothing else.package_idis currently null. If your integration needs the source package for a line, store it locally when you create the line.mac_addris 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. AGETrequest to/ext/line/{uuid}/terminate(or/renew,/enable,/disable,/update-advanced) returns501 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 considersexp_date = 0an expired line, which was not the intent of most OneStream callers; the correct way to remove an expiration isexpire_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
ridper operation. UUIDs, ULIDs, or any string up to 255 characters work. - If the same
ridis 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
ridis retried with a different body, you receive409 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
ridbelonging 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:
- Use
/ext/lines/indexand honor thehas_morefield in the pagination envelope. Iterate untilhas_moreis false. - Migrate your line-listing flow to the native
GET /panel-api/v1/lines?cursor=<value>endpoint, which returns anext_cursorin 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, andusernamemay be zero or empty andcreditsis 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.
nullmeans "no expiration". The engine distinguishes this from0(which it treats as expired). If you want a line with no expiration, passnull, never0.- Any other value returns
422 validation_errorat 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
- Change your OneStream client's base URL to
https://<your-xtream-ai-panel-domain>/panel-api/onestream/. The/ext/*path segment is unchanged. - Get an API key from your Xtream AI panel (Settings, Panel API Keys). Pick the scopes matching what your integration does.
- Replace your existing OneStream
X-Api-Keyvalue with the new token. - Run your test suite. Everything except
/ext/live_connections/*should pass without code changes. - If your integration paginates line lists, switch that specific flow to
/ext/lines/indexand honorhas_more, or migrate it to the native/panel-api/v1/lines?cursor=<value>endpoint. - Continue to send a unique
ridfor every write. Add aGET /ext/transaction/{rid}lookup to your retry code if you don't already have one.
See also
- Panel API Overview — the three dialects and how to pick one.
- Authentication — token shape, scopes, IP allow-lists, key rotation.
- Rate limits & Idempotency — retry contract, headers, backoff.
- Errors — full error slug catalog.
- Xtream Codes / XUI.one compatibility — the other compatibility dialect.
- Common Tasks — copy-pasteable examples for the most common flows.