> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.loreos.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.loreos.app/_mcp/server.

# Self-serve access

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`.

> **Info**
>
> 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

| Credential           | Prefix                                  | Used by                              | Can call                      |
| -------------------- | --------------------------------------- | ------------------------------------ | ----------------------------- |
| Demo sandbox key     | returned by `POST /v1/demo/sandbox-key` | local tests and docs dogfood         | capped runtime `/v1/*` routes |
| Account access token | `lat_...`                               | coding agents and console automation | `/v1/account/*` only          |
| Runtime API key      | `ck_...`                                | your backend service                 | public runtime `/v1/*` routes |

> **Warning**
>
> 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:

```bash
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"
    ]
  }'
```

2. The response includes:

```json
{
  "schema_version": "v0",
  "data": {
    "device_code": "ldc_...",
    "user_code": "ABCD-2345",
    "verification_uri": "https://<loreos-console>/console/device?user_code=ABCD-2345",
    "expires_at": "2026-06-19T12:00:00Z",
    "interval_seconds": 5,
    "requested_scopes": ["account:workspace:read", "account:apps:write"]
  },
  "next_actions": [
    {
      "command": "POST /v1/account/device/token",
      "description": "Poll with device_code after the human approves the user_code."
    }
  ]
}
```

3. 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.

4. The agent polls for the account token:

```bash
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.

5. The agent confirms the token:

```bash
curl https://api.loreos.app/v1/account/me \
  -H "Authorization: Bearer $LOREOS_ACCOUNT_TOKEN"
```

6. The agent creates a sandbox or staging app:

```bash
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" }'
```

7. The agent issues a runtime API key for that app:

```bash
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:

```bash
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:

```json
{
  "data": {
    "environment": "production",
    "is_sandbox": false,
    "production_state": "production_active"
  }
}
```

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:

```bash
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.

```bash
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):

```bash
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:

```bash
# 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:

```bash
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:

```bash
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](./delivery-metering-observability#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:

```bash
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:

```bash
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.