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-2012.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 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}}. |
# Requires a provisioning key as the bearer.curl https://api-pr-2012.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. That org_id is what 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.