Docs
Status
OverviewQuickstartSetup promptsThe core loopAuthenticationOverviewModelsThe waterfallPlansAdding modelsData controlsOpenAI compatibilityEmbeddingsAnthropic APIErrorsIntegrate the gatewayCost APIAccount APICoding agentsCredits & billingSpend & intelligenceSpend APITelemetryBecome a providerProvider guideAPI reference

Get started

  • Overview
  • Quickstart
  • Setup prompts
  • The core loop
  • Authentication

Guides

  • Overview
  • Models
  • The waterfall
  • Plans
  • Adding models
  • Data controls
  • OpenAI compatibility
  • Embeddings
  • Anthropic API
  • Errors

Integrations

  • Integrate the gateway
  • Cost API
  • Account API
  • Coding agents

Billing & usage

  • Credits & billing
  • Spend & intelligence
  • Spend API
  • Telemetry

Providers

  • Become a provider
  • Provider guide

Reference

  • API reference

Integrations

Account API

Authenticate with one xpl_ key, inspect what a key can spend, and provision keys programmatically: mint one per end-customer, rotate on a schedule, or cap a key's daily spend, all over the API.

Base URL

The account and key-management routes answer under one host at https://api-pr-74.preview.experientiallabs.ai/api/v1. New to the surface? Start with Integrate the gateway.

Inference key vs provisioning key

There are two kinds of key: an inference key and a provisioning key. Both are ordinary xpl_keys; “provisioning” is a capability flag on the key (is_provisioning), not a different prefix or a separate account. You set it when you mint the key ("provisioning": true on POST /api/v1/keys); a key without it is a normal inference key.

  • An inference key runs inference and makes the read-only account/cost calls (/credits, /key, /generation, /activity, /usage, GET /models, GET /providers; see the Cost API).
  • A provisioning key does all of that plus the whole key-management family (GET/POST/PATCH/DELETE /keys) and the identity-setup routes (identities, budgets, per-identity usage). Every one of those routes, including the GET reads, is gated behind it; a normal inference key gets a 403 (A provisioning key is required for this action) on all of them. Only an org admin can create a provisioning key, and the keys list marks each one with a Provisioning badge.

For the everyday xpl_ key mechanics (how it looks, the Bearer header, and what one key can and cannot do), see Authentication.

The first provisioning key comes from the dashboard. Key creation is a provisioning-key action, so there is a chicken-and-egg: mint the first one from the dashboard (a signed-in org admin satisfies the same gate a provisioning key does), then that key can mint further keys, provisioning or inference, through POST /api/v1/keys. Keep the provisioning key server-side; do not use it as your day-to-day inference key.

Organization reports require a provisioning key

The Spend API at /api/orgs/{org_id}/spend uses the same provisioning-key gate for every report and handle read. An inference key receives 403; a foreign organization receives 404. Personal scope requires a signed-in human. Use analytics to acquire a frozen report, then reuse its query and snapshot for pagination, charts and exports.

GET /api/orgs/{org_id}/usage/by-key and /usage/by-identity remain lightweight provisioning-key reads with report_kind: "live_usage_summary". They retain existing USD display fields and add exact decimal-string paid_nano_usd, byok_nano_usd, free_nano_usd and spend_nano_usd. They are not certified snapshots; unknown usage is not reported, not zero. Reads have a 10,000 key/model cell limit and a 32 MiB source-cell JSON budget; final response formatting has separate overhead. Exceeding either source-read limit returns 422 usage_summary_too_large, directing you to Spend.

GET /api/v1/key still reports the presented key only. Do not generalize that rule to all inference reads: the existing Cost API activity and usage export remain org-scoped except for their separate plan-bound identity restrictions.

Inspect the presented key

GET /api/v1/key returns usage/limit metadata for the key in the Authorization header. usage, limit, and limit_remaining are real; the cap is a daily one, so limit_remainingis measured against today’s spend. This read requires an xpl_ key credential (it reports on the presented key itself).

Manage keys (provisioning key required)

The {hash} path segment is the key’s hash field (the row’s uuid id, as returned by list/create), never the secret.

RouteNotes
GET /api/v1/keysList keys ({data:[...]}); revoked hidden unless ?include_disabled=true.
POST /api/v1/keysCreate a key (200); returns {data, key:"xpl_..."}; the plaintext is returned once. Body: name (required), limit (daily USD cap), provisioning (bool), identity_id (an active identity of your org; 404 unknown, 409 disabled). Every key resource reports its identity_id.
GET /api/v1/keys/{hash}Read one key.
PATCH /api/v1/keys/{hash}Update name / disabled / limit. disabled:true revokes (terminal).
DELETE /api/v1/keys/{hash}Revoke one key; returns {data:{success:true}}.
POST /api/v1/keys
# Requires a provisioning key as the bearer.
curl https://api-pr-74.preview.experientiallabs.ai/api/v1/keys \
-H "Authorization: Bearer xpl_...provisioning..." \
-H "Content-Type: application/json" \
-d '{
"name": "cust_8842",
"limit": 25,
"provisioning": false
}'

On the key list/CRUD object, usage and limit_remaining are intentionally null today (there is no cheap per-key lifetime-spend reader for a list; a fabricated 0 would mislead). Real per-key spend lives on GET /api/v1/key (the presented key) and GET /api/v1/usage (filter by api_key_id, on the Cost API). This is a documented follow-up, not a permanent gap.

Customer plans and allowances

Native customer plans ship inactive. Ordinary assignment returns 409 customer_plans_inactive until an operator enables enforcement after every serving worker supports it. Automated checks do not establish production availability or live provider parity.

Plans belong to your organization and apply to authenticated identities, separately from the platform's Free/Pro subscription. All keys under one identity share the policy, including rotated keys. Request labels such as safety_identifier, user and metadata.plan never authorize access.

RouteNotes
GET/POST /api/orgs/{org_id}/model-groupsAdmin list/create immutable group versions. POST: {group_key, name, expected_version, model_ids:[canonical_model_uuid]}. Start expected_version at 0.
GET/POST /api/orgs/{org_id}/customer-plansAdmin list/create immutable plan versions. POST: {plan_key, name, expected_version, aggregate_limit_nano_usd, groups:[{group_key, group_version, limit_nano_usd, on_exhausted}]}. Each model belongs to exactly one group; overlap is rejected.
GET/PUT /api/orgs/{org_id}/identities/{identity_id}/customer-planPUT: {plan_key, plan_version, expected_revision, effective:next_month|immediate, budget_period?}. Default next_month, initial revision 0. Explicit source periods require immediate. GET returns current and pending assignments and the period. Null plan_key and plan_version removes the assignment, admin only.
PUT /api/orgs/{org_id}/customer-plan-delegations/{api_key_id}Admin only: {plan_keys:[approved-plan]}. Explicitly lets this provisioning key assign those plans, not author plans or remove assignments. Empty list revokes delegation.
GET /api/orgs/{org_id}/identities/{identity_id}/allowancesSelf or management read of the assigned period's group and aggregate limit, settled, reserved, pending and remaining integer nano-USD counters.

Authoring and delegation require an org-admin session through the trusted control-plane transport, not an ordinary provisioning key. On the web host, the same model-groups, customer-plans and delegation paths accept the signed-in administrator's browser session and forward its verified actor. They are not cookie-free machine-key endpoints. A delegated subscription handler calls the API host's assignment PUT with its provisioning bearer credential. All writes are audited. Model-group and plan versions are immutable. An unknown source plan must block migration, never map to an unassigned identity or an invented default.

aggregate_limit_nano_usdis a shared cap across one identity's groups, including exposure from removed groups. Null or omitted means no aggregate cap; zero denies. Group limits also treat zero as a hard zero. Both limits must admit the request. Group on_exhausted defaults to deny. Explicit next_model permits only group exhaustion to advance a certified server-authored model chain. It does not create a chain, permit arbitrary client fallback, or bypass the aggregate cap, model access or other refusal classes. The root must pass its existing funding checks before the skipped attempt is rolled back; only a committed request-scoped proof permits the child to run its own checks. Free-only :free, caller-selected, reasoning-pinned and unproven derived BYOK successor paths stay terminal.

The default budget_period is {"kind":"calendar_month_utc"}. A source interval uses {kind:"explicit", source_period_id, start, end}, with timezone-aware bounds for a nonempty half-open interval [start,end). Explicit periods expire without inventing a renewal. Omission on a plan change preserves the period. Rolling windows and automatic anniversary or carryover translation are unsupported. A used source interval cannot be renamed, overlapped or backdated, or changed back to calendar mode. Immediate policy changes keep settled history and pending exposure; models used in a period cannot move to a fresh group in that period.

Existing provisioning keys gain no plan authority automatically. Keep the explicitly delegated subscription-event credential server-side, on a separate management identity. For onboarding, create a fresh customer identity, assign its plan, then issue its inference keys. Do not hand out an unrestricted management credential and later repurpose it: operations authenticated before the first assignment are not canceled retroactively. Plan-bound inference keys cannot provision identities or keys, change plans, read the org wallet, or change BYOK, provider policy or prompt capture. Their usage reads stay scoped to their identity. Active or scheduled assignments prevent provisioning keys and key identity reassignment. Revoke every identity key before removing its plan, so customer credentials never become unrestricted. A first scheduled assignment refuses inference until its effective date. A scheduled removal becomes deny-all until an administrator explicitly clears it after revoking any replacement keys, never unrestricted at rollover. Unknown or foreign targets return 404; revision conflicts return 409.

Inspect and assign existing versions in API keys > Identities, inside the identity detail. Your application still owns subscription events and invoicing. The allowance read uses frozen attributed list value, including BYOK estimates, not provider invoices. Missing custom-provider protocol, pricing, authentication, usage or privacy facts remain cutover blockers.

Current-period handoff

On the web host, use the signed-in org administrator's browser session. The session proxy forwards its verified actor to the API host's typed control-plane route. Ordinary inference and provisioning keys cannot read or mutate handoff state; plan delegation does not grant cutover authority. These are not cookie-free customer-key endpoints and there is no dedicated cutover dashboard panel. Never supply a caller-chosen actor header to bypass that gate.

Prefix every suffix below with /api/orgs/{org_id}/identities/{identity_id}/allowance-cutover. All amounts are integer nano-USD; evidence digests are 64 lowercase hexadecimal SHA-256 characters, never credentials or prompt content.

RouteNotes
GET (base path)Returns identity_id, cutover, allowances, external_receipts, native_outstanding, settled_periods, export_overflow and export_sha256. Content-free accounting evidence.
PUT (base path)Prepare: {cutover_id, expected_assignment_revision, plan_key, plan_version, budget_period, source, source_watermark, covered_from, covered_until, evidence_sha256, receipts}. UUID cutover_id; timezone-aware coverage inside the exact period.
POST /activate{cutover_id, expected_revision, source_frozen:true, source_fence_evidence_sha256}. Requires global fleet activation, current period and unchanged prepared assignment.
POST /freeze{cutover_id, expected_revision}. Closes destination admission before draining and exporting.
POST /acknowledge-rollback{cutover_id, expected_revision, export_sha256, source_resume_evidence_sha256}. Requires a complete current frozen export and all native and imported reserved/pending exposure resolved.
POST /resolve{cutover_id, expected_revision, external_ref, settled_nano_usd, evidence_sha256}. Replaces an imported pending leg once from independent billing evidence; retains its original settled leg.

Consume and produce handoff JSON losslessly. Totals can exceed JavaScript's2^53 - 1 safe-integer bound even when individual receipts fit. The session proxy preserves raw request and response JSON without parsing or reserializing money. Keep the original export and use Python integers or exact decimals, or a JavaScript lossless JSON parser/serializer with BigInt support. OrdinaryJSON.parse, response.json() and Number can round rollback amounts; converting a rounded number to BigInt cannot recover them. Emit exact JSON integers, not numeric strings, and return the server-issued export_sha256 unchanged rather than recomputing it from a client document.

A prepare accepts at most 500 distinct, exclusively source-owned request receipts:{external_ref, group_key, group_version, settled_nano_usd, pending_nano_usd}. Do not import cumulative snapshots, destination-owned requests or overlapping exports. Identical prepare retries are idempotent; changed replays and reused references are refused. This is not a paginated bulk importer. A larger source export needs a separately supported migration path, not truncation or compressed totals.

Preparation records immutable receipts and closes destination inference for this identity even while global customer-plan enforcement is inactive. It does not stop Cloudflare or apply imported exposure to active counters. Stop source dispatch, account for in-flight requests and verify export completeness before activation. Activation applies the imported settled and pending amounts once and opens destination admission. Its transaction is local to the destination; the source-stop digest records your evidence, not proof that Cloudflare stopped.

Rollback means freeze, drain native reservations and resolve native pending usage plus all imported pending while active or frozen, read a complete fresh export, account for it at the source, then acknowledge. Any unresolved native or imported exposure blocks acknowledgment. Late settlement changes the export digest and requires another read. settled_periods retains historical periods, not just the current one. Each row contains period_start, period_end, nullable source_period_id, group_key and settled_nano_usd. More than 500 rows of either receipt class or 1,000 settled-period rows sets export_overflow and cannot be acknowledged. Overflow never certifies a partial export as complete. Rolled-back destination admission stays closed and destination resolution stops. Pending imports retain their evidence and transfer no authority automatically. Any unresolved prior exposure blocks another prepare, as does destination-owned usage in the same period. No operation resets history or silently grants a fresh allowance.

The implementation does not activate production or move traffic automatically. Obtain the actual source rule export, subscription mapping, credentials and provider proof, verify the deployed contract, and obtain separate approval before each mutation or cutover.

Set up identities with a provisioning key

An identityis a named spend bucket inside your org (“coding”, “product”, “research”): every key hangs off exactly one, so usage rolls up per identity and a monthly budget can cap an identity or a single key. A provisioning key drives the whole setup an org admin would otherwise click through on the API keys page (create the identities, mint one inference key per identity, set budgets, and read spend per identity), so a coding agent can do it from one prompt.

RouteNotes
GET /api/whoami{org_id, org_slug, org_name, identity_id, customer_plan_identity_id, is_provisioning} for the presented key. Restricted customer-plan keys may use this self-read too; it grants no management authority.
GET /api/orgs/{org_id}/identitiesList identities ({identities:[{identity_id, display_name, description, active, is_default, ...}]}). The is_default one is the org's own identity.
POST /api/orgs/{org_id}/identitiesCreate one: {display_name (required), identity_id?, description?}. identity_id starts with a letter, then lowercase letters or digits, groups joined by '.', '-' or '_' (^[a-z][a-z0-9]*([._-][a-z0-9]+)*$), never 'org-…', and is unique PLATFORM-wide. 201 created; 409 code identity_id_unavailable = the id already exists (yours or any org's; the response never says which): reuse it if it is in your own list, else retry with details.suggested_id; 422 bad id.
PATCH /api/orgs/{org_id}/identities/{identity_id}Rename / redescribe: {display_name?, description?}. A provisioning key may NOT send active (403): enabling or disabling an identity is the admin's, on the dashboard. 404 = that identity id does not exist in the org.
POST /api/v1/keysMint one inference key per identity (200): {name, identity_id, limit? (daily USD cap)}. Leave provisioning off.
GET /api/orgs/{org_id}/budgets?period=YYYY-MMThe month's budgets with limit / reserved / settled / remaining (nano-USD). Read-back takes the calendar month, never "*".
PUT /api/orgs/{org_id}/budgetsSet a monthly cap: {period: "*" (recurring) | "YYYY-MM", scope_kind: "identity" | "key", identity_id | api_key_id, limit_nano_usd} ($1 = 1,000,000,000). limit_nano_usd 0 is a HARD $0 cap, never "no cap". A provisioning key may set only identity and key scopes (422 otherwise), never the default identity's (403), and may REPLACE only a budget its own key created; an admin's row answers 403 "set by an administrator; change it in the dashboard". Every row reports created_by_kind (session | api_key) and created_by_api_key_id.
DELETE /api/orgs/{org_id}/budgets/{budget_id}Remove a cap (back to unlimited): {deleted:true}. A provisioning key may delete only identity- and key-scope budgets its own key created (403 on a team/model cap, on the default identity's, or on an admin's or another key's row); 404 = no such budget in the org.
GET /api/orgs/{org_id}/usage/by-identity?window=24h|7d|30dSpend per identity: {identities:[{identity_id, display_name, active, keys:[{api_key_id, key_label, request_count}], totals:{request_count, error_count, input_tokens, output_tokens, cost_usd, estimated_cost_usd, free_usd}, last_used_at}]}. Only keys that carried traffic in the window are listed, each with its own request_count. The identity_id null row is traffic whose key was deleted; a key minted without a group folds under the identity the mint assigned it (the Default identity on most orgs).
The prompt is on the API keys page. Creating or viewing a provisioning key offers “Copy prompt to set up identities”: paste it into your coding agent together with the key (the prompt carries a placeholder, never the key), and the agent proposes identities, creates them, mints a key per identity with optional limits, tells you where each key goes (Claude Code, the OpenAI SDK, OpenCode, a server), and verifies each with one request. The same text is under Setup prompts.
  • The path org_id must be the key’s own org; any other org answers a 404.
  • Disabling an identity, the grant matrix, org-wide (team) budgets, the default identity’s budget, and any budget an administrator set stay the admin’s: the PATCH refuses active from a key (403); the budget routes accept only identity and key scopes from a key (422 on set, 403 on delete), refuse the default identity (403), and let a key replace or delete only the rows its own key created (created_by_api_key_id; 403 otherwise); identity DELETE plus the grant routes are not admitted to keys at all (401 with a valid key: the edge refusing the route, not a key problem). A leaked provisioning key cannot reshape model access, freeze the org, or lift a freeze.
  • Error shapes: a business refusal carries {error}; a request-shape failure (missing display_name, a non-numeric limit, a budgets read without period) carries FastAPI’s {detail:[{loc, msg, type}]} and detail[0].loc[-1] names the field.
  • Every write is audited under the key (actor_kind = api_key), exactly like the dashboard path is audited under the admin. The API does not itself stop a provisioning key from serving inference or minting another provisioning key; the prompt keeps those as the admin’s rules.

Key-per-customer

A key per end-customer is an alternative to safety_identifierattribution: mint a distinct inference key per end-customer, and each customer’s usage is naturally isolated to their key with independent revocation and per-key daily limits. safety_identifier is simpler (one key, per-request label, on the Cost API); you can also combine them.

  • A normal inference key gets a 403 on every /api/v1/keys* route.
  • Keep the provisioning key server-side; never ship it to a client or use it to serve inference traffic.
PreviousCost APINextCoding agents