Self-serve access

Workspace setup, agent account tokens, and runtime API keys.

LoreOS has two auth planes:

  • Account plane - workspace, team, app, API key, external-user cap, usage, and billing management.
  • Runtime plane - character, session, message, import, image, delivery, and metering calls for your product.

Keep them separate. A coding agent may use a short-lived account access token to set up your workspace, issue an app key, and manage users. Your server then uses the issued runtime API key for character runtime calls such as POST /v1/characters and POST /v1/sessions/{session_id}/messages.

The account-plane API is in preview. The backend device-flow/token path is implemented and live-tested, and the browser approval page is available at /console/device. Account routes are not yet part of the generated API Reference or public SDK surface. Treat this page and GET /v1/llms.txt as the preview contract until the account-plane graduation gates pass.

When to use each key

CredentialPrefixUsed byCan call
Demo sandbox keyreturned by POST /v1/demo/sandbox-keylocal tests and docs dogfoodcapped runtime /v1/* routes
Account access tokenlat_...coding agents and console automation/v1/account/* only
Runtime API keyck_...your backend servicepublic runtime /v1/* routes

Clerk Account API keys start with ak_.... They are issued by Clerk’s own Account/API Keys UI and are never accepted by LoreOS. For LoreOS runtime calls, issue a ck_... Runtime API Key from the LoreOS console or POST /v1/account/api-keys.

Never put a runtime API key in a browser or mobile app. Never give an agent your Google, Clerk, or session-cookie credentials.

Agent-safe workspace setup

Use this flow when account-device approval is enabled and a coding agent needs to create a workspace app and issue a runtime API key without handling a human login.

  1. The agent starts device authorization:
$curl -X POST https://api.loreos.app/v1/account/device/start \
> -H "Content-Type: application/json" \
> -d '{
> "client_label": "codex-local-setup",
> "client_fingerprint": "opaque-agent-installation-id",
> "scopes": [
> "account:workspace:read",
> "account:apps:write",
> "account:api_keys:write",
> "account:external_users:write",
> "account:caps:write",
> "account:usage:read"
> ]
> }'
  1. The response includes:
1{
2 "schema_version": "v0",
3 "data": {
4 "device_code": "ldc_...",
5 "user_code": "ABCD-2345",
6 "verification_uri": "https://<loreos-console>/console/device?user_code=ABCD-2345",
7 "expires_at": "2026-06-19T12:00:00Z",
8 "interval_seconds": 5,
9 "requested_scopes": ["account:workspace:read", "account:apps:write"]
10 },
11 "next_actions": [
12 {
13 "command": "POST /v1/account/device/token",
14 "description": "Poll with device_code after the human approves the user_code."
15 }
16 ]
17}
  1. A human opens verification_uri, signs in with Clerk/Google, chooses or creates the workspace, reviews the scopes, and approves the code. The human identity check stays in the browser; the agent never receives browser cookies or OAuth credentials. The console approval page uses GET /v1/account/device/{user_code} behind the signed browser proxy to show the client label, requested scopes, status, and expiry before approval.

  2. The agent polls for the account token:

$curl -X POST https://api.loreos.app/v1/account/device/token \
> -H "Content-Type: application/json" \
> -d '{ "device_code": "ldc_..." }'

If the human has not approved yet, the response is data.status = "pending" (keep polling at interval_seconds). After the human approves at /console/device, the next device/token call returns data.status = "authorized" (NOT "approved") together with the short-lived data.account_access_token. Also handle the terminal "denied" and "expired" states.

  1. The agent confirms the token:
$curl https://api.loreos.app/v1/account/me \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN"
  1. The agent creates a sandbox or staging app:
$curl -X POST https://api.loreos.app/v1/account/apps \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{ "name": "Local staging", "environment": "sandbox" }'
  1. The agent issues a runtime API key for that app:
$curl -X POST https://api.loreos.app/v1/account/api-keys \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{ "app_id": "00000000-0000-0000-0000-000000000000" }'

Use the returned runtime API key as LOREOS_KEY on your server for normal runtime calls:

$curl https://api.loreos.app/v1/me \
> -H "Authorization: Bearer $LOREOS_KEY"

If the app was created with "environment": "production", the key is intentionally blocked until production activation is complete. Before activation, runtime calls return 403 with detail.code = "production_not_active". After GET /v1/account/apps/$APP_ID/production-readiness passes and POST /v1/account/apps/$APP_ID/production-activation succeeds, verify the same runtime key with GET /v1/me. A ready production key returns:

1{
2 "data": {
3 "environment": "production",
4 "is_sandbox": false,
5 "production_state": "production_active"
6 }
7}

If GET /v1/me returns the plain string detail "api key invalid or revoked", the key is not an active LoreOS runtime key. Check that it starts with ck_... and was issued by the LoreOS console or account API.

The full runtime key is shown only once. To inspect or rotate keys later, use prefix-only account-plane routes:

$curl https://api.loreos.app/v1/account/apps/$APP_ID/api-keys \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN"
$
$curl -X POST https://api.loreos.app/v1/account/apps/$APP_ID/api-keys/ck_abcd1234/rotate \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{ "revoke_old": true }'
$
$curl -X POST https://api.loreos.app/v1/account/apps/$APP_ID/api-keys/ck_abcd1234/revoke \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN"

GET /api-keys returns safe metadata only: prefix, status, auth_status, app id, environment, production state, scopes, created time, last-used time, expiry, quota/rate-limit hints, and whether revoke/rotate are supported. It never returns the full secret. Rotate returns the replacement ck_... secret once.

For zero-downtime rotation, do not revoke the old key first:

  1. Call rotate with { "revoke_old": false }.
  2. Store the new ck_... secret in your server secret manager.
  3. Deploy or reload your app with the new secret.
  4. Verify the new key with GET /v1/me. For production apps, confirm data.production_state = "production_active".
  5. Revoke the old prefix with POST /v1/account/apps/$APP_ID/api-keys/$OLD_PREFIX/revoke.
  6. Confirm the old key now fails runtime auth.

If the old key may already be exposed, rotate with { "revoke_old": true } and accept the brief cutover while you update your server secret.

Managing end-user caps

Account tokens can also prepare end-user records and limits for the app they just created. Use this when your product wants per-user metering caps before sending live messages.

$curl -X PUT https://api.loreos.app/v1/account/apps/$APP_ID/external-users/user_123 \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{ "display_name": "Test user", "credit_limit": 5.0 }'
$
$curl -X POST https://api.loreos.app/v1/account/apps/$APP_ID/external-users/user_123/budget-policy \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{ "limit_credits": 5.0, "resource_type": "all", "enforcement": "hard" }'

Runtime message calls still use the runtime API key. LoreOS resolves external_user_ref inside the app and enforces caps before expensive model work starts.

To test a 402 upsell wall without touching real billing, create a sandbox zero-credit fixture. This lets you test 402 without manually zeroing real credits.

Step 1 — create the fixture with the lat_ account token. $APP_ID is the data.app_id from POST /v1/account/apps (the same value the runtime GET /v1/me returns under a ck_ key):

$curl -X POST https://api.loreos.app/v1/account/apps/$APP_ID/test-fixtures/insufficient-credit \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{ "external_user_ref": "fixture_zero_credit" }'

The route refuses active production apps (sandbox or pending-production only). The response returns the zero-credit user at data.external_user.external_user_ref — that is the value you feed the runtime calls. (If you omit external_user_ref in the request, LoreOS generates one; read it back from the response.)

Step 2 — switch to the ck_ runtime key and exercise the wall:

$# create a session for the fixture user (ck_ runtime key)
$curl -X POST https://api.loreos.app/v1/sessions \
> -H "Authorization: Bearer $LOREOS_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "character": "luna", "external_user_ref": "fixture_zero_credit" }'
$
$# send one message → expect 402 BEFORE any provider work (ck_ runtime key)
$curl -X POST https://api.loreos.app/v1/sessions/$SESSION_ID/messages \
> -H "Authorization: Bearer $LOREOS_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "text": "hi" }'

The message call should fail with 402 budget_exceeded before any model or image work begins. The error detail includes scope, limit_usd, used_or_reserved_usd, estimated_hold_usd, remaining_usd, would_exceed_by_usd, and top_up_url. It should not emit a message.created event for the blocked send, and it should not change GET /v1/account/apps/{app_id}/cost-summary settled totals because the provider call never started.

After users arrive, a server or coding agent can use the runtime key to list them:

$curl "https://api.loreos.app/v1/external-users?limit=50" \
> -H "Authorization: Bearer $LOREOS_KEY"

GET /v1/external-users returns the app’s end-user dashboard rows: external_user_ref, display name, metadata, status, hard limit, used/reserved/remaining credits, metered cost, session count, last session, last activity, and next_cursor. To inspect one user’s conversation, list their sessions with GET /v1/external-users/{external_user_ref}/sessions and then read GET /v1/sessions/{session_id}/events?since=0.

Reading cost for pricing

Use the account token when your console or coding agent needs app-level unit economics:

$curl https://api.loreos.app/v1/account/apps/$APP_ID/cost-summary \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN"

The response separates user_attributed_cost_usd, character_infrastructure_cost_usd, app_setup_cost_usd, active reservations, top end users, and top characters. Use it to decide whether customer pricing should include only user-driven work or also allocate shared character infrastructure such as Story Room, offscreen scenes, proactive candidate planning, and ambient-life planning.

For raw grouped usage, GET /v1/account/apps/{app_id}/usage?group_by=character returns character ids with slugs and display names. group_by=session returns session ids with the resolved character slug plus the end-user ref, so a console can attribute cost to exact user-character pairs.

$APP_ID here is the data.app_id returned by POST /v1/account/apps (the runtime GET /v1/me also returns it under a ck_ key). The account-plane lat_ routes always need it. For cost rolled up across every app in the workspace, call GET /v1/account/cost-summary (no app_id). For the full “which usage/cost read maps to which credential” matrix — including the app-scoped GET /v1/usage?group_by=… and GET /v1/rates runtime reads — see Usage & cost endpoints by credential.

The hosted developer console shows the same projection after an app is selected: Cost overview and Per-user metering appear near the top, while Cost, Usage And Caps shows usage classes, top end users, top characters, and the app hard cap editor. The External Users And Limits panel shows each managed end user’s spent cost, hard limit, reserved in-flight cost, remaining budget after holds, last activity, and cap editor.

Telegram delivery setup

For companion products that want one Telegram contact per user-character relationship, use official Managed Bots from the console.

  1. Issue a runtime API key for the selected app, or paste an existing key into the Telegram Managed Bots panel.
  2. In Telegram, open @BotFather, create or select the manager bot, open BotFather’s Mini App, and enable Bot Management Mode in the bot settings.
  3. Paste the manager bot token into the console and connect it.
  4. Click Verify BotFather status. The console shows management_mode_status, can_manage_bots, webhook status, command-menu status, and last checked time.
  5. Use Test child contact from the console, or call POST /v1/sessions/{session_id}/telegram/managed-child-bot from your app.

If management_mode_status is disabled, return to BotFather, enable Bot Management Mode, then verify again. Bot tokens are stored in Vault and are never displayed after connection.

Scopes

Request the smallest scope set the agent needs:

  • account:workspace:read - inspect workspace, apps, role, and billing status.
  • account:team:write - invite, update, or remove workspace members.
  • account:apps:write - create apps and manage production activation state.
  • account:api_keys:write - issue runtime API keys.
  • account:external_users:write - create, list, update, or delete app end-users.
  • account:caps:write - set app-level or user-level budget policies.
  • account:usage:read - read usage by app and external user.
  • account:billing:write - start prepaid top-up checkout or portal flows.

Prepaid billing

LoreOS billing is workspace-scoped prepaid credit — a single shared pool that every app in the workspace draws from. A workspace owner/admin calls POST /v1/account/billing/checkout, completes a Stripe One-off Checkout package, and the Stripe checkout.session.completed webhook increments the workspace prepaid balance. Runtime usage across all apps burns that shared balance through metering; the top-up itself is 1:1 USD, while the billing margin is applied when usage is settled.

The hosted console shows the same projection in Billing:

  • billing status and Stripe customer connection
  • prepaid package / plan mapping
  • topped-up, used, reserved, and remaining workspace balance
  • Top up credits, which opens Stripe Checkout for the workspace
  • Portal, which opens Stripe’s customer portal when a customer exists

Use account routes when a coding agent needs to drive the same flow:

$curl -X POST https://api.loreos.app/v1/account/billing/checkout \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{}'

A top-up always funds the whole workspace pool, so it can never credit the wrong app — an app_id field is accepted for backward compatibility but ignored. The response includes checkout_session_id, url, price_id, scope: "workspace", mode: "payment", and any configured top_up_credits override. Follow url to complete the test or live checkout.

Revocation

Account access tokens expire (default 30 days; see the device-flow response’s expires_at) and are revocable. When an agent finishes setup, revoke the active token so it cannot be reused:

$curl -X POST https://api.loreos.app/v1/account/tokens/current/revoke \
> -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN"

Revoking an account token does not revoke runtime API keys that were already issued. Rotate or revoke runtime API keys separately from the account plane.

Current limitations

  • The account routes are preview routes, so generated SDKs may not include them yet.
  • Device authorization start is public but rate-limited. 429 account_device_rate_limited means the code issuance window was exceeded; reuse an existing account token or wait.
  • 503 account_device_rate_limit_secret_missing means public self-serve device issuance is not configured on that deployment. Retry after the deployment is configured; demo sandbox keys remain available only for short-lived first tests.
  • Account-token automation can create Stripe Checkout sessions and inspect balance/readiness. Completing hosted Checkout still happens on Stripe’s hosted payment page, after which the webhook updates LoreOS balance.
  • The public demo sandbox route is still the fastest no-login path for a first character reply. Use account tokens when you need persistent apps, API key issuance, team state, or per-user caps.