Integrations
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.
The account and key-management routes answer under one host at https://api-pr-2007.preview.experientiallabs.ai/api/v1. New to the surface? Start with Integrate the gateway.
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.
/credits, /key, /generation, /activity, /usage, GET /models, GET /providers — see the Cost API).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.
POST /api/v1/keys. Keep the provisioning key server-side; do not use it as your day-to-day inference 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).
The {hash} path segment is the key’s hash field (the row’s uuid id, as returned by list/create), never the secret.
| Route | Notes |
|---|---|
| GET /api/v1/keys | List keys ({data:[...]}); revoked hidden unless ?include_disabled=true. |
| POST /api/v1/keys | Create a key (200); returns {data, key:"xpl_..."} — the plaintext 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}}. |
# Requires a provisioning key as the bearer.curl https://api-pr-2007.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.
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.
| Route | Notes |
|---|---|
| GET /api/whoami | {org_id, org_slug, org_name} for the key's org — the org_id every route below takes. |
| GET /api/orgs/{org_id}/identities | List 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}/identities | Create 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/keys | Mint one inference key per identity (200): {name, identity_id, limit? (daily USD cap)}. Leave provisioning off. |
| GET /api/orgs/{org_id}/budgets?period=YYYY-MM | The month's budgets with limit / reserved / settled / remaining (nano-USD). Read-back takes the calendar month, never "*". |
| PUT /api/orgs/{org_id}/budgets | Set 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|30d | Spend 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). |
org_id must be the key’s own org; any other org answers a 404.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}; 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.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.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.
403 on every /api/v1/keys* route.