<!-- OpenSmartRoute: Platform guide. https://opensmartroute.ai/docs/PLATFORM -->
# Platform guide

The hosted platform runs OpenSmartRoute as a service. A workspace has an API key and a plan; each
request to the API is routed to a target (model, agent, skill, persona or tool) and answered with the
decision, its trace and, on request, the target's output. An OpenAI-compatible endpoint accepts
existing chat-completion clients.

This page covers calling the platform: authentication, routing, execution, the OpenAI-compatible
endpoint, feedback, plans, organizations, tenants, governance (workspace policy and budgets),
observability (probes, metrics, per-request traces and the event stream), caching, the dashboard and
the MCP server. The
[marketplace](https://opensmartroute.ai/docs/MARKETPLACE.md) - shared agents, skills, personas, prompts and stack templates - has
its own page. To run the router on your own infrastructure see
[Deploy with Docker and Helm](https://opensmartroute.ai/docs/deploy.md); to use it as a Python library see the
[user guide](https://opensmartroute.ai/docs/GUIDE.md) and the [SDK guide](https://opensmartroute.ai/docs/SDK.md). Every endpoint below is listed in the REST API
reference (`/docs/api`) and in `/openapi.json`.

## 1. Authentication

1. Sign up at `/platform/signup` with GitHub, Google or an email address - with a password if you want to sign
   in by email later (`OSR_PLATFORM_PASSWORD_LOGIN`, on by default). Signup creates a personal workspace
   on the **free** plan and issues an API key.
2. The key is shown once. Store it in a secret manager. Keys are created, renamed, rotated and revoked in
   the dashboard (`/platform/dashboard/keys`) or with `POST /api/v1/keys`, `PATCH /api/v1/keys/{key_id}`
   `{name}`, `POST /api/v1/keys/{key_id}/rotate` (a new secret for the same key, shown once; the old one
   stops working immediately) and `DELETE /api/v1/keys/{key_id}`.
3. Send the key with every metered request in either header:

```http
Authorization: Bearer osr_...
X-API-Key: osr_...
```

Public endpoints (`/api/v1/info`, `/api/v1/models`, `/api/v1/llms`, `/api/v1/rankings`,
`/api/v1/catalogue`, `/api/v1/estimate`, `/api/v1/stats/public`, `/api/v1/registry`) need no key.

### Email and password

`POST /api/v1/auth/password/login` `{email, password}` returns the same browser session as a
single sign-on callback (`session_token`, `user`, `account`, `plan`); the dashboard stores it and
sends it as a bearer token. Passwords are at least 8 characters (no composition rules) and are stored
as salted scrypt hashes. A signed-in user sets or changes theirs with `POST /api/v1/auth/password`
`{password, current_password}` - the current one is required once set and every other browser is
signed out - and removes it with `DELETE /api/v1/auth/password` as long as a provider identity remains.
Sign-in attempts are limited per address and per email; `has_password` on the user object says which
users have one. `/platform/dashboard/account` has the form.

### Account lifecycle: confirmation, recovery, deletion

Anyone can create an account with any email address. What happens around it:

| Step | How | Endpoints |
|---|---|---|
| Email confirmation | Signup sends a link to `/platform/verify-email?token=...` (valid three days). Signing in through a provider that vouches for the address, accepting an invitation sent to it or completing a password reset also confirms it. `email_verified` on the user object; `/platform/dashboard/account` shows a banner with *Send again* until it is done. | `POST /api/v1/auth/verify` `{token}`, `POST /api/v1/auth/verify/send` (signed in, once a minute) |
| Forgot password | `/platform/forgot-password` emails a single-use link to `/platform/reset-password?token=...` (one hour). The answer is always `202` so it never reveals whether an address has an account; limited per address and per email. Resetting revokes every session, confirms the address, signs the person in and sends a notice. | `POST /api/v1/auth/password/forgot` `{email}`, `POST /api/v1/auth/password/reset` `{token, password}` |
| Invitations | `POST /api/v1/workspace/invites` emails the invitee a link to `/platform/invite/<token>` (fourteen days) and returns the same link once to the inviter. | see [Organizations](#7-organizations) |
| Account history | The person's own sign-ins, password and email changes and memberships, newest first. | `GET /api/v1/me/audit` |
| Download my data | One JSON document with everything held about the person and the current workspace: profile, identities, sessions, memberships and history; the workspace with plan, members, key prefixes, tenants, policy, SSO, usage, the request log and reported outcomes of up to a year. Prompt text is never stored, so none is included. *Download my data* on `/platform/dashboard/account`. | `GET /api/v1/me/export?days=` |
| Delete account | *Delete my account* on `/platform/dashboard/account`: type the email (and the password when one is set). Sessions, linked identities and memberships go, workspaces only this user belonged to are disabled and their keys revoked; an organization with other members needs another owner first. A notice is emailed. | `POST /api/v1/me/delete` `{confirm, password}` |
| Getting started | The checklist of a workspace: e-mail confirmed, API key created, first request routed, first outcome reported, a model provider connected and - for organizations - a teammate invited; each step carries the dashboard link and the API call that completes it, `next` is the first open one and `complete` the share done. The dashboard overview shows it until every step is done. | `GET /api/v1/onboarding` |

Signup, confirmation, password recovery, invitation links and this checklist form the `onboarding`
domain, which a deployment can run as its own service (see [Services](#services)).

Every one of these events - plus sign-ins, failed sign-ins, role changes, removals and operator actions
- is written to the account audit log. Workspace admins read their workspace's entries with
`GET /api/v1/workspace/audit`; operators see everything at `/platform/admin/audit` (`GET /api/v1/admin/audit-log`)
and per user on `/platform/admin/users/<id>`, where they can also issue a reset link, re-send or force the email
confirmation, disable, sign out everywhere or delete the user.

### Email: transports, templates, welcome, newsletter and reports

Every message the platform sends is composed once in `platform/api/osr_platform/email_templates.py` as a
branded HTML card with a plain-text alternative: the welcome message after signup (dashboard and guide links),
address confirmation, password reset, invitation, account deleted, password changed, the operator test message,
usage reports and newsletter campaigns. Each is written to the outbox first and then delivered by whichever
transport is configured:

| Transport | Setting | Notes |
|---|---|---|
| Azure Communication Services Email | `OSR_PLATFORM_ACS_CONNECTION_STRING` | `endpoint=https://<name>.<region>.communication.azure.com/;accesskey=...`; the platform signs requests itself (HMAC-SHA256, no SDK). `OSR_PLATFORM_MAIL_FROM` must be a sender of a linked domain, e.g. `DoNotReply@<guid>.azurecomm.net` for an Azure-managed domain or `no-reply@yourdomain` once a custom domain is verified. Wins when both transports are set. |
| SMTP | `OSR_PLATFORM_SMTP_URL` | `smtp://user:pass@host:587` (STARTTLS) or `smtps://user:pass@host:465`. |
| Outbox only | neither | Nothing is lost: operators read the links at `/platform/admin/mail` (`GET /api/v1/admin/mail`) and hand them over; `mail` in `GET /api/v1/info` tells the web app which case applies. |

**Newsletter.** `POST /api/v1/newsletter/subscribe` (the form on `/newsletter` and in the site footer; off with
`OSR_PLATFORM_NEWSLETTER=false`) stores the address, sends a confirmation and always answers `202`. Every
newsletter message carries a one-click unsubscribe link (`POST /api/v1/newsletter/unsubscribe` with a per-send
token). Operators manage subscribers and campaigns at `/platform/admin/newsletter`: add an address by hand,
export CSV, write a campaign in plain text (blank lines separate paragraphs, `## ` starts a heading), preview the
rendered email, send a test to one address, then send once to every active subscriber.

**Scheduled reports.** `reports.py` runs inside the API: every `OSR_PLATFORM_REPORTS_INTERVAL_S` (default hourly,
`0` = on demand only) it checks whether a weekly (Monday) or monthly (1st, UTC) window has completed and sends
each workspace's owners a digest - requests, cost, savings, error rate, top models - and the operators in
`OSR_PLATFORM_REPORT_RECIPIENTS` the deployment-wide digest. A window is recorded in `report_runs` and never sent
twice, so several API replicas stay idempotent. `/platform/admin/reports` shows the scheduler, past digests and
sends one now (`POST /api/v1/admin/reports/send`, `POST /api/v1/admin/reports/tick`).

### Command line sign-in

Install the CLI (`curl -LsSf https://opensmartroute.ai/install.sh | sh`, or
`irm https://opensmartroute.ai/install.ps1 | iex` on Windows) and run `osr login`. The CLI shows an
eight-character code and opens `/platform/cli/authorize`; sign in there, check that the code and computer
name match, choose the key name and approve. The platform mints a new workspace API key for that
machine and the CLI stores it in `~/.config/opensmartroute/credentials.json`. This is the OAuth 2.0
device authorization grant (RFC 8628) and works the same for the community and enterprise editions:

| Step | Endpoint | Notes |
|---|---|---|
| CLI asks for a code | `POST /api/v1/auth/device/code` `{client_name}` | anonymous, rate limited per IP; returns `device_code`, `user_code`, `verification_uri[_complete]`, `expires_in` (900 s), `interval` (5 s) |
| Browser looks it up | `GET /api/v1/auth/device/{user_code}` | signed in; shows client name, status and expiry |
| Browser decides | `POST /api/v1/auth/device/approve` / `deny` `{user_code, name}` | approving needs the `admin` role and a free key slot in the plan |
| CLI polls | `POST /api/v1/auth/device/token` `{device_code}` | `authorization_pending`, `slow_down`, `access_denied`, `expired_token` as RFC 8628 errors; success returns `access_token` (the `osr_live_` key), `account`, `plan`, `edition` |

Only approve codes from a terminal you started yourself. Keys minted this way appear on
`/platform/dashboard/keys` with the chosen name and can be revoked like any other. Pasting a key works too
(`osr login --token osr_live_...`, or `--with-token` from stdin in CI); `osr whoami` shows the
workspace, plan and edition behind the stored credential, `osr token create|list|revoke` manage keys
and `osr mcp --remote` bridges an IDE to this workspace's MCP server without copying the key around.

## 2. Route a request

`POST /api/v1/route` returns a decision: the chosen target, the confidence, ranked alternatives and
the signals extracted from the request. The target is not called unless `execute` is set.

```bash
curl -s "$OSR_URL/api/v1/route" \
  -H "Authorization: Bearer $OSR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Prove that sqrt(2) is irrational.", "top_k": 3}'
```

```json
{
  "request_id": "7f3c...",
  "target": {"id": "llm-frontier", "kind": "llm", "name": "Frontier model"},
  "confidence": 0.81,
  "alternatives": [{"id": "llm-mid", "kind": "llm", "utility": 0.62}],
  "signals": {"complexity": 0.74, "domains": ["math"], "reasoning_need": 0.9, "contains_pii": false, "...": "..."},
  "ranked": [{"id": "llm-frontier", "utility": 0.79, "quality_estimate": 0.91, "breakdown": {"...": "..."}}],
  "policy_rejections": {},
  "explanation": "llm-frontier: high reasoning need, math domain, quality weight dominates ...",
  "executable": true,
  "elapsed_ms": 1.9
}
```

Request fields:

| Field | Type | Meaning |
|---|---|---|
| `text` | string, required | The user request. |
| `history` | list of `{role, content}` | Prior turns; the router reads them for intent, task type and session affinity. |
| `context` | object | Free-form facts (`app`, `session_id`, `user_locale`, ...). Keys starting with `_` are dropped. |
| `objective` | object | Relative weights: `quality` (default 1.0), `cost` (0.15), `latency` (0.05), plus `quality_floor`, `energy`, `carbon`. `cost_weight` is accepted as a synonym of `cost`. |
| `constraints` | object | Hard constraints, never traded off: see below. |
| `profile` | object | Opaque user/tenant features (locale, expertise, preferences). Hashed into the learners; never logged raw. |
| `kinds` | list | Restrict to target kinds: `llm`, `agent`, `skill`, `persona`, `tool`, `human`. |
| `top_k` | int, default 3 | How many alternatives to return. |
| `plan` | bool | Build a persona → skill → model plan instead of choosing a single target (**pro** plan and above). |
| `execute` | bool | Route and run the chosen target or plan; the output is returned in `result` (**pro** and above). |

Constraints (`constraints` object):

| Key | Effect |
|---|---|
| `max_cost_per_1k` | Drop targets whose price per 1k tokens is above this. |
| `max_latency_ms` | Drop targets slower than this. |
| `preferred_max_latency_ms` | Soft target: targets whose observed p90 latency is above it are penalised, not dropped. |
| `region` | Only targets served from this region. |
| `data_boundary` | Only targets at least this strict (`public` < `private` < `on_prem`). |
| `contains_pii` | Force the PII handling on or off (`null` lets the sensitivity signal decide). |
| `allow_targets` / `deny_targets` | Explicit allow / deny lists of target ids. |
| `allowed_kinds` | Same as `kinds`. |
| `require_tools` | Only targets that can call tools. |

### Quote before routing

`POST /api/v1/estimate` takes the same body and returns the price of each candidate without calling
anything: `targets[]` with `cost_usd`, `input_tokens`, `output_tokens`, vendor and model, plus the
router's `recommended` pick and the `cheapest`, `best_quality` and `fastest` candidates.
`output_tokens` overrides the assumed reply length; `monthly_requests` adds a `monthly` projection
(`recommended_usd`, `cheapest_usd`, `dearest_usd`, `savings_usd`). Without a key it is rate limited
per client and ignores tenant rules; with a key it is metered as `estimate` and applies your plan
and tenants. `GET /api/v1/models/recommended` returns the router's picks per use case from live
quotes. Details, the SDK form and the `/estimate` page: [MCP.md](https://opensmartroute.ai/docs/MCP.md).

## 3. Execute the chosen target

With `"execute": true` the platform runs the chosen target (or plan) with the configured providers and
returns the output, the steps that ran and the metered cost:

```bash
curl -s "$OSR_URL/api/v1/route" -H "Authorization: Bearer $OSR_API_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Summarise this contract clause for a non-lawyer: ...", "execute": true, "objective": {"cost": 0.4}}'
```

The response adds `result` with `text`, `target_id`, `steps[]` (`role`, `target_id`, `ok`,
`latency_ms`), `tokens` and `cost_usd`.

### Model providers

A target executes when it is mapped to a model on an **OpenAI-compatible endpoint** - OpenAI, Azure OpenAI,
OpenRouter, Mistral, Groq, Together, Fireworks, DeepSeek, a local Ollama, vLLM or LiteLLM, anything that
speaks `/v1/chat/completions`. Two sources define the mapping and are merged (the console wins on the same
provider name or target):

* the **file** the deployment mounts, `OSR_PLATFORM_PROVIDERS` (YAML/JSON path or inline JSON) with
  `providers.<name>` (`base_url`, `api_key` or `api_key_env`, optional `api_key_header`, `extra_headers`,
  `timeout_s`, `max_retries`) and `models.<target_id>` (`provider`, `model`, optional `system_prompt` and
  request defaults). On first start it is imported into the console once, so it can be edited there.
* the **operator console**, `/platform/admin/providers`: add an endpoint from a preset, store the key inline or name
  the environment variable that holds it, *Check* it (`GET <base_url>/models` with its credentials records
  reachability, latency and the upstream model ids, which the mapping form then offers), map targets to
  upstream models, disable or delete. Every write re-binds the handlers in the API at once, and every other
  API replica re-reads the store within `OSR_PLATFORM_PROVIDERS_RELOAD_S` (30 s) - no restart, no redeploy.

`GET /api/v1/providers` is the public, OpenRouter-style view of what a deployment executes on, rendered
by the site at `/providers`: one row per
executable target with `provider`, `model`, the recognised reference model, `input_usd_per_1m` /
`output_usd_per_1m` (from the reference catalogue when the upstream model is known, else the target's
declared cost), context window, tool and reasoning support, circuit-breaker `health` and seven-day traffic;
plus each provider's host, kind, health and model count. Credentials never appear: the console shows only
whether a key is set and its last four characters (`api_key_set`, `api_key_hint`). Keys stored through the
console live in the platform database next to the organization SSO secrets; prefer `api_key_env` when the
host injects secrets. Endpoints: `GET|POST /api/v1/admin/providers`, `GET|PATCH|DELETE
/api/v1/admin/providers/{id}`, `POST /api/v1/admin/providers/{id}/check`, `PUT|DELETE
/api/v1/admin/providers/models/{target_id}`. Targets without a mapping still route (decisions, plans,
traces, quotes) and report `executable: false`.

### Response headers

| Header | On | Meaning |
|---|---|---|
| `X-Request-Id` | every response | The id to quote in feedback and support requests; echoed back when you send one. |
| `Server-Timing` | every response | `app;dur=<ms>` - time spent inside the platform. |
| `X-OSR-Target` | `/v1/chat/completions` | The target id that answered. |
| `X-OSR-Confidence` | `/v1/chat/completions` | The router's confidence in that choice. |
| `Retry-After` | `429` | Seconds until the per-minute window, the daily quota or the budget period resets. |
| `X-Quota-Limit` | `429` (daily) | Your plan's requests per day. |
| `X-Budget-Limit`, `X-Budget-Used`, `X-Budget-Period` | `429` (budget) | The exhausted workspace or tenant budget in USD and its period (`daily` / `monthly`). |
| `X-Upgrade: true` | `403` | The feature exists but is not in your plan. |
| `ETag`, `Cache-Control` | public catalogue reads | Conditional requests: send `If-None-Match` and receive `304` when nothing changed. |

## 4. OpenAI-compatible endpoint

Point an OpenAI SDK at `$OSR_URL/v1` and set `model` to `auto`. The router chooses the target per
request, runs it and returns a standard `chat.completion` whose `model` field is the target that
answered. Decision metadata is added in an `opensmartroute` object that other clients ignore.

```python
from openai import OpenAI

client = OpenAI(base_url=f"{OSR_URL}/v1", api_key=OSR_API_KEY)
reply = client.chat.completions.create(
    model="auto",
    messages=[{"role": "user", "content": "Write a haiku about routing."}],
)
print(reply.model)                 # e.g. "llm-small" - the target the router chose
print(reply.choices[0].message.content)
```

```json
"opensmartroute": {
  "request_id": "7f3c...", "target": "llm-small", "kind": "llm", "confidence": 0.88,
  "plan": [], "alternatives": ["llm-mid"], "cost_usd": 0.00012, "latency_ms": 412.5,
  "fallback_from": []
}
```

Options outside the OpenAI schema go in the request body and are ignored by other providers:

| Field | Meaning |
|---|---|
| `model` | `auto` (default) lets the router choose; a target id pins that target. |
| `models` | Candidate list: the router chooses among them and falls back down the list when a target fails. |
| `osr.objective` / `osr.constraints` | Same as on `/api/v1/route`. |
| `osr.tenant` | Tenant slug (or the `X-OSR-Tenant` header); applies the tenant's constraints (**enterprise**). |
| `osr.plan` | `true`/`false` to force or suppress plan composition; omitted = whatever the plan tier allows. |
| `osr.fallbacks` | `false` disables the automatic retry on the next best target (default `true`). |
| `stream` | `true` streams server-sent events in the OpenAI format: each provider token is relayed as it arrives; the last chunk carries `usage` and the `opensmartroute` metadata. Quota, routing and provider errors before the first token are ordinary HTTP errors; a fallback to another target happens only before the first token. |

`GET /v1/models` lists `auto` and every executable target.

## 5. Feedback

`POST /api/v1/feedback` reports how a routed answer turned out. The learners (Bradley-Terry, IRT,
LinUCB) update on the workspace's own traffic. Available on the **pro** plan and above.

```bash
curl -s "$OSR_URL/api/v1/feedback" -H "Authorization: Bearer $OSR_API_KEY" -H "Content-Type: application/json" \
  -d '{"request_id": "7f3c...", "target_id": "llm-frontier", "success": true, "quality": 0.9, "latency_ms": 2100}'
```

Fields: `request_id`, `target_id`, `success` (required); `quality` (0..1), `cost_usd`, `latency_ms`,
`domains`, `complexity`, `preferred_over` (the id of a target this one beat in a comparison).

Every report is kept against its request: `GET /api/v1/activity` summarises it per row (`outcome`),
`GET /api/v1/trace/{request_id}` lists the reports (`outcomes`) next to the spans, and in the enterprise
edition the audit chain records an `outcome` entry after the `decision` entry (section 10). The dashboard
has the same form without curl: expand a row on `/platform/dashboard/activity` (or open its trace) and
report good / poor, a quality score and, optionally, the target that would have done better - the latter
is recorded as a pairwise preference for the Bradley-Terry learner.

## 6. Plans and quotas

| Plan | Requests / day | Requests / min | Keys | Features |
|---|---|---|---|---|
| **free** | 500 | 30 | 2 | route + trace, OpenAI-compatible proxy |
| **pro** | 20,000 | 300 | 10 | + plans (`plan`/`execute`), feedback |
| **enterprise** | 500,000 | 3,000 | 100 | + tenants, `/stats`, hash-chained `/audit`, organization SSO, 50 tenants, 500 seats |

`GET /api/v1/info` (`plans`) returns the values for the deployment you are calling; `GET /api/v1/me`
returns your plan. Upgrades are made in the dashboard (`/platform/dashboard/billing`).

* `429 rate limit exceeded` - per-minute window; wait `Retry-After` seconds.
* `429 daily quota exhausted` - the day's budget; `X-Quota-Limit` shows the ceiling.
* `403` with `X-Upgrade: true` - the feature is not in your plan.
* `404 ... requires the enterprise edition` - the deployment runs the community image, which does not
  expose tenants, statistics or audit regardless of plan.

## 7. Organizations

Signup creates a personal workspace. An **organization** is a second kind of workspace with its own
members, roles, API keys, tenants, plan and usage; one user can belong to several and switches between
them in the dashboard (`/platform/dashboard/workspace`, `POST /api/v1/workspaces/{account_id}/switch`).
Creating one (`POST /api/v1/workspaces` with `name` and an optional `slug`) makes you its owner and
issues its first API key.

Roles are `member` < `admin` < `owner`:

| Action | Role |
|---|---|
| Route, read usage, savings and activity | `member` |
| Create and revoke API keys, change tenants, invite and remove members, rename the workspace | `admin` |
| Grant or remove `owner`, configure organization SSO, delete the connection | `owner` |

API keys belong to the workspace and act with its full rights; the role model applies to signed-in
browser sessions. Members are invited by email (`POST /api/v1/workspace/invites`): the invitee gets a
message with the link when the deployment has a mail server, and the platform returns the same link once
to the inviter so it can be shared directly. Seats and tenants
are limited per plan (`max_members`, `max_tenants` in `GET /api/v1/info`). An organization always
keeps at least one owner; owners transfer ownership before leaving.

**Organization SSO** (enterprise) lets people sign in with the company identity provider:
`PUT /api/v1/workspace/sso` with `kind` (`github`, `google`, `microsoft`, `gitlab` or `oidc` with an
`issuer`), `client_id`, `client_secret`, the allowed email `domains` and the `default_role` given to
anyone who signs in from one of those domains. The workspace needs a slug first; the sign-in page
shows the provider as `org-<slug>`.

## 8. Tenants

Tenants (enterprise) apply a fixed set of hard constraints per customer, team or region without the
caller repeating them. Create a tenant, then send its slug in `X-OSR-Tenant` (or `osr.tenant`).

```bash
curl -s -X PUT "$OSR_URL/api/v1/tenants" -H "Authorization: Bearer $OSR_API_KEY" -H "Content-Type: application/json" \
  -d '{"slug": "eu-health", "config": {"region": "eu", "data_boundary": "private", "max_cost_per_1k": 0.01, "deny_targets": ["llm-frontier"]}}'

curl -s "$OSR_URL/api/v1/route" -H "Authorization: Bearer $OSR_API_KEY" -H "X-OSR-Tenant: eu-health" \
  -H "Content-Type: application/json" -d '{"text": "Anonymise this discharge summary ..."}'
```

Tenant config keys: `region`, `data_boundary`, `max_cost_per_1k`, `max_latency_ms`, `preferred_max_latency_ms`,
`deny_targets`, `allow_targets`, `contains_pii`, `daily_budget_usd`, `monthly_budget_usd`. Explicit request constraints
win over the tenant defaults; the workspace policy (next section) applies on top of both. Executed spend
is attributed to the tenant, so `GET /api/v1/governance` shows spend and budgets per tenant.

## 9. Governance: workspace policy, budgets and controls

A **workspace policy** applies to every request of the workspace, whichever key or tenant sent it. It
takes the same keys as a tenant plus budgets, and needs the `admin` role:

```bash
curl -s -X PUT "$OSR_URL/api/v1/policy" -H "Authorization: Bearer $OSR_API_KEY" -H "Content-Type: application/json" \
  -d '{"config": {"data_boundary": "private", "deny_targets": ["llm-frontier"], "monthly_budget_usd": 250}}'
```

`GET /api/v1/policy` returns the policy, the accepted keys and the current budget lines; `DELETE`
removes it. Constraints merge deterministically: a boundary or region fills in when the request has
none, cost and latency caps take the stricter value, deny lists are merged and allow lists intersected.
Unknown keys, unknown target ids and an id present in both `allow_targets` and `deny_targets` are
rejected with `400`.

**Budgets** count executed spend (`execute: true`, `/v1/chat/completions`, MCP `ask`) - quotes and
route-only calls are free. `daily_budget_usd` resets at midnight UTC, `monthly_budget_usd` on the first
of the month. Once a workspace or tenant budget is exhausted, routed calls return `429 ... budget ...
exhausted` with `Retry-After` and the `X-Budget-*` headers until the period rolls over or the limit is
raised.

`GET /api/v1/governance` is the compliance view of the workspace in one payload:

| Block | Contents |
|---|---|
| `controls` | What the deployment enforces: `input_guard`, `pii_redaction`, `steering_strip`, `metrics`, `tracing`, `audit`, `health_breakers`, `decision_cache`, `learning` (`persisted` or `in-process`). |
| `audit` | Whether the hash-chained audit trail is on, its length and the result of the last verification. |
| `retention_days` | How long usage rows are kept before the background sweep purges them. |
| `policy`, `policy_keys`, `budgets`, `spend` | The workspace policy, its accepted keys, budget lines with used and remaining amounts, spend today and this month. |
| `quota` | Requests today against the plan's daily and per-minute limits. |
| `tenants` | Each tenant with its monthly spend and budget lines. |
| `catalogue` | Targets per `data_boundary`, targets that refuse PII, executable targets, providers, and which targets remain reachable under the policy. |

The dashboard renders the same data at `/platform/dashboard/governance`, with a policy editor for admins.

## 10. Observability and caching

The platform observes itself: every span, event, counter and alert below is produced, stored and served
by the platform - there is no external metrics system, tracing backend or alerting tool to run. Every
deployment exposes liveness and readiness probes and the counters without a key:

| Endpoint | Meaning |
|---|---|
| `GET /healthz` | Process is up: edition, version, target count, uptime. |
| `GET /readyz` | `200` when the database, catalogue, router and (enterprise) audit file all answer; `503` with the failing check otherwise. Also reports `database_dialect` (`sqlite` or `postgres`), `redis` and `events` (the event bus) - those two are informational and never fail readiness. |
| `GET /metrics` | The counters in text exposition format: `osr_platform_http_requests_total{method,route,status}`, `osr_platform_http_request_duration_ms_*`, `osr_platform_http_in_flight`, plus the router's `osr_route_decisions_total`, `osr_route_latency_ms_*`, `osr_outcome_*` and `osr_decision_cache_*`. |

Responses carry `X-Request-Id` (send your own to correlate with your logs) and `Server-Timing`; the
API writes one JSON access-log line per request (`osr.platform.access`) with the route template,
status, latency and account - never the prompt. `GET /api/v1/stats` (enterprise) adds `http`,
`controls`, `cache` and `observability` snapshots; `/platform/dashboard/health` shows them live.

### Time series and alerts

| Endpoint | Returns |
|---|---|
| `GET /api/v1/telemetry/series?window=24h` | The workspace's metered traffic bucketed over `1h` (per minute), `6h` (5 min), `24h` (15 min), `7d` (3 h) or `30d` (per day): requests, failures, `error_rate`, p50 / p95 latency, cost and tokens per bucket, totals per target and per endpoint, and the window total. Computed from the platform's own usage records, so it agrees with the activity log to the row. Plans with `stats` also get `http`: the deployment's requests, 4xx, 5xx and p50 / p95 / p99 latency per bucket from the per-minute counters the process keeps for 24 hours. |
| `GET /api/v1/alerts` | Active conditions, most severe first, each with the dashboard page that shows or fixes it. Workspace rules on every plan: a daily or monthly budget of the workspace or a tenant at 80 % (`warning`) or exhausted (`critical`), and 20 % or more of the last 15 minutes' metered requests failing (10+ requests). Deployment rules on plans with `stats`: a failing readiness check, 5 % or more 5xx over the last five minutes (20+ requests), p95 above 2 000 ms, an open or half-open circuit breaker per target, a routing-drift alarm in the last hour, a failed autopilot cycle, a routing SLM last fitted more than 30 days ago, and tracing being off. |

The overview shows the last 24 hours (traffic, latency) and the alerts; `/platform/dashboard/health` adds the
deployment series and the same alerts, refreshed with the page.

### Notifications: alert delivery

Alerts are evaluated by the API itself - no external scheduler. Every `OSR_PLATFORM_ALERTS_INTERVAL_S`
seconds (default 60; `0` turns the loop off and leaves *Evaluate now* to operators) one replica - the one
holding the `osr:alerts:leader` lease in Redis, or the only one without Redis - re-evaluates the
workspace rules for every workspace that was active in the last 15 minutes or has a channel or an open
alert, plus the deployment rules. Each condition becomes a **notification episode**: opened when the rule
starts firing, refreshed while it stays on (an escalation from `warning` to `critical` is delivered
again), resolved when it clears. Episodes land in an inbox with unread counts per severity, are published
to the `alerts` Kafka topic in cluster mode, and are delivered to the workspace's **channels**:

| Kind | Target | Delivery |
|---|---|---|
| `email` | An address | Through the platform mailer (`OSR_PLATFORM_SMTP_URL`, otherwise the outbox at `/platform/admin/mail`). |
| `webhook` | An `https://` URL | `POST` of `{event, alert, workspace, url}` with `X-OSR-Event` (`alert.firing`, `alert.escalated`, `alert.resolved`, `alert.test`), `X-OSR-Delivery` and, when the channel has a secret, `X-OSR-Signature: t=<unix>,v1=<hex>` = HMAC-SHA256 of `<t>.<body>`; verify like Stripe signatures. |
| `slack` | An incoming-webhook URL | Block Kit message with severity, rule, value against threshold and a link to the page that fixes it. |
| `teams` | An incoming-webhook URL | MessageCard with the same facts. |

A channel filters by minimum severity (`info`, `warning`, `critical`) and optionally by rule names, can be
disabled, records its last delivery and last error, and has *Send test*. Failed webhook deliveries are
retried on the following evaluation rounds up to three attempts. Up to 20 channels per workspace; creating
or changing them takes the `admin` role. Operators have the same inbox and channels for the **deployment**
scope (readiness, 5xx rate, p95, circuit breakers, drift, autopilot, SLM age, tracing) at
`/platform/admin/notifications`.

| Endpoint | Purpose |
|---|---|
| `GET /api/v1/notifications?state=firing&unread=true&limit=100` | The workspace inbox (`items`) and unread counts per severity (`unread`). |
| `GET /api/v1/notifications/unread` | Unread counts only - the dashboard bell polls this. |
| `POST /api/v1/notifications/read` | `{"ids": [...]}` marks those read, `{"ids": null}` marks everything read. |
| `GET|POST /api/v1/notifications/channels` | List channels (with the rule names you can filter on) or create one: `kind`, `name`, `target`, optional `secret`, `min_severity`, `rules`, `enabled`. |
| `PATCH|DELETE /api/v1/notifications/channels/{id}` | Change or remove a channel. |
| `POST /api/v1/notifications/channels/{id}/test` | Deliver a test notification and return `ok`, `status`, `error`. |
| `GET /api/v1/notifications/deliveries` | The last delivery attempts (channel, event, attempt, status, error). |
| `GET|POST /api/v1/admin/notifications` and `.../channels`, `.../deliveries` | The operator (deployment scope) equivalents; `POST /api/v1/admin/notifications/evaluate` runs one round now and returns what it opened, resolved, delivered and failed. |

### Shared state and replicas

A single API replica keeps everything on its data volume (SQLite, learner state, audit chain). A
deployment that runs several replicas moves the shared parts to services that are themselves
containers - no managed cloud database, cache or broker is required, and the same images run on a
laptop, a Kubernetes cluster or Azure Container Apps:

| State | Service | Setting |
|---|---|---|
| Accounts, users, keys, tenants, policies, usage, feedback, marketplace | PostgreSQL | `OSR_PLATFORM_DATABASE_URL` |
| Rate-limit windows, learner state (enterprise) | Redis | `OSR_PLATFORM_REDIS_URL` |
| `usage`, `feedback` and `admin` events as JSON topics `osr.<name>` | Kafka (any Kafka-protocol broker) | `OSR_PLATFORM_KAFKA_BOOTSTRAP`, `OSR_PLATFORM_KAFKA_TOPIC_PREFIX` |

`GET /api/v1/info` -> `storage` says which mode a deployment runs (`{database, clustered, redis,
events}`) and the operator console shows it on `/platform/admin`. Event payloads carry the same fields as the
activity log and never the prompt text. `platform/docker-compose.yml` in the repository starts the
complete stack; the Azure template provisions the three services as container apps with internal TCP
ingress next to the API and web app.

In that stack the routing SLM is its own service too: the API publishes each routed prompt to the
`training` topic (opt-in, `OSR_PLATFORM_TRAINING_EVENTS`), the SLM service pairs prompts with the
outcomes reported through `POST /api/v1/feedback`, trains a challenger on a schedule, keeps it only when
it beats the champion on a held-out split, and writes the promotion to the shared volume from which every
API replica reloads it. Operators see the model in service, the evidence ingested, every cycle and a
prompt probe on `/platform/admin/slm` (`GET /api/v1/admin/slm`, `/slm/reports`, `POST /slm/cycle`, `/slm/predict`).

### Services

The API is one image that can run as one process or as several: the documentation JSON (`docs`), the
public model catalogue and rankings (`rankings`), the marketplace (`marketplace`), provider management
(`providers`), signup and account links (`onboarding`), identity and workspaces (`accounts`: sign-in,
sessions, organizations, keys, tenants, policy, governance, usage), Stripe billing (`billing`), the
operator console API (`admin`), the MCP endpoint (`mcp`), the OpenAI-compatible proxy (`openai`), the
routing core with its trace, event and learning reads (`routing`) and the routing SLM (`slm`) each have
their own entry point and port. `/api/v1/info`, `/api/v1/status` and `/api/v1/estimate` always stay
with the API. The API remains the single public origin and forwards a domain's paths to its
service when `OSR_PLATFORM_<NAME>_URL` is set (streamed completions are relayed as they arrive); the
answer carries `X-OSR-Service` naming the process
that served it. `GET /api/v1/info` -> `services` lists the topology; operators see live health per
service on `/platform/admin/services` (`GET /api/v1/admin/services`). The Compose stack and the Azure template
deploy every service as its own container next to the gateway.

### Traces and events

The router records every request as a tree of **spans** (`request`, `route`, `plan`, `execute`) with
**events** inside them (`route.signals`, `route.policy`, `route.rank`, `route.fallback`, `guard.*`,
`execute.step`, `learn.outcome`, `cache.hit`, ...). The newest ones stay in an in-memory buffer (1000
by default, `OSR_OBSERVABILITY_MEMORY_EVENTS`) and every one of them is also written to the platform
database by the platform's own telemetry store (`OSR_PLATFORM_TELEMETRY_STORE`, on by default) - off
the request path, from a background writer, so tracing never slows routing. Events and traces therefore
survive restarts, can be read by time range and are purged after `OSR_PLATFORM_TELEMETRY_RETENTION_DAYS`
(14). They are readable per workspace:

| Endpoint | Returns |
|---|---|
| `GET /api/v1/trace/{request_id}` | The activity row plus every span and event of one request, in time order, with `trace_id`, `spans` and the root `duration_ms`; `outcomes` (the feedback reported for the request) and `audit` (its hash-chained records, plans with `audit`). `source` says whether the rows came from the buffer or the store. `404` when the request is not the workspace's; `events: []` once the request is older than the telemetry retention. |
| `GET /api/v1/events?name=route.*&kind=&level=&request_id=&since=&until=&limit=200` | Events of the workspace's requests (oldest first, `limit` up to 2000; `name` may end with `*`; `since` / `until` are epoch seconds, default the last 6 hours). Deployment-wide events without a request id (breaker transitions, autopilot) are included on plans with `stats`. `source`, `persisted`, `retention_start` and `retention_days` describe the store. `404` when tracing is off entirely. |
| `GET /api/v1/status` | Public readiness: the `/readyz` checks plus edition, versions, uptime, controls and tracing state; `503` while a check fails. `Cache-Control: no-store`. |

`POST /api/v1/route` responses include `trace_id` and the `X-OSR-Trace-Id` header when the request was
traced, and `GET /api/v1/activity` marks rows whose spans are kept (in memory or in the store) with
`traced: true` and summarises the feedback reported for each request in `outcome` (`POST /api/v1/feedback`
is stored per request, so the loop from decision to result is visible per row). Attributes hold ids,
numbers and short labels; the request text is represented only by its digest and length. The dashboard
shows the history at `/platform/dashboard/events` (window of 1 hour to 7 days, search by request or trace id),
opens a trace from any activity row (with a *How it was routed* summary of signals, policy, ranking and
decision, the outcome and the audit records) and adds a *Trace* tab to the playground.

Public catalogue reads (`/api/v1/info`, `/api/v1/models*`, `/api/v1/rankings`, `/api/v1/llms*`,
`/api/v1/catalogue`, `/api/v1/stats/public`) return an `ETag` and `Cache-Control: public, max-age=60,
stale-while-revalidate=60`; send `If-None-Match` to get `304` when nothing changed. Operators tune this
and the decision cache with:

| Setting | Default | Effect |
|---|---|---|
| `OSR_PLATFORM_METRICS_PUBLIC` | `true` | `false` restricts `/metrics` to requests carrying `X-Admin-Token`. |
| `OSR_PLATFORM_HTTP_CACHE_MAX_AGE_S` | `60` | `Cache-Control` lifetime of public reads; `0` sends `no-store`. |
| `OSR_PLATFORM_ROUTE_CACHE_TTL_S` | `0` | Reuse a routing decision for identical requests within this window (`0` = off). |
| `OSR_PLATFORM_RETENTION_DAYS` | `365` | Usage rows older than this are purged every six hours; `0` keeps everything. |
| `OSR_PLATFORM_TELEMETRY_STORE` | `true` | Persist every span and event to the platform database (the events page, traces and `since` / `until` queries outlive the in-memory buffer). |
| `OSR_PLATFORM_TELEMETRY_RETENTION_DAYS` | `14` | Persisted spans and events older than this are purged with the same sweep; `0` keeps them. |

### Learning: the routing SLM and the autopilot

Every deployment learns from `POST /api/v1/feedback`: the bandit, IRT, Bradley-Terry, LinUCB and Markov
strategies update on each outcome (persisted under the data volume in the enterprise edition). A
deployment can also serve a **routing SLM** - the small routing model `osr slm train` / `osr improve`
write to a file - as one more strategy in the ensemble, and run the **autopilot** in-process so the
platform's own feedback keeps retraining it:

| Setting | Default | Effect |
|---|---|---|
| `OSR_PLATFORM_SLM` | none | Path of the model file; it joins the ensemble as the `slm` strategy (weight `OSR_WEIGHTS_SLM`, default 1.0). |
| `OSR_PLATFORM_AUTOPILOT` | `false` | Run `SelfImprover` cycles on a thread: outcomes and remembered prompts become rows, a challenger is trained on a split and promoted only when it beats the champion on the holdout. The promoted model is written to `DATA_DIR/autopilot/slm.json` and outranks the mounted file after a restart. |
| `OSR_PLATFORM_AUTOPILOT_OFFLINE` | `true` | No catalogue refresh, dataset download or web search from the API process; `false` also refreshes `OSR_PLATFORM_AUTOPILOT_SOURCES` (public dataset presets, comma separated). |
| `OSR_PLATFORM_AUTOPILOT_INTERVAL_S` | `OSR_SLM_AUTOPILOT_INTERVAL_S` (3600) | Schedule; drift alarms from the outcome stream trigger a cycle early (`OSR_SLM_AUTOPILOT_DRIFT_*`, rate-limited by `OSR_SLM_AUTOPILOT_MIN_GAP_S`). |
| `OSR_PLATFORM_AUTOPILOT_MIN_ROWS` | `20` | A cycle needs at least this many rows before it trains a challenger. |

`GET /api/v1/learning` (any plan) returns the strategy ensemble with its weights, the learner state
(persistence, drift resets, quarantined state), per-target outcome statistics, the SLM in service
(rows, sources, encoder, calibration, fit history - never the weights) and the autopilot status with
its recent champion-vs-challenger reports. `controls.slm` / `controls.autopilot` appear in
`GET /api/v1/governance` and `GET /api/v1/info`. Operators trigger a cycle with
`POST /api/v1/admin/autopilot/cycle` (`X-Admin-Token`; 404 without an autopilot). Cycles are traced
as an `autopilot.cycle` span with `learn.improve` / `learn.promote` events, so they appear in
`GET /api/v1/events` and on `/platform/dashboard/events`. The dashboard renders all of it at
`/platform/dashboard/learning`.

## 11. Marketplace

The marketplace (`/marketplace`) is where agents, skills, personas, prompts, tools, model profiles and
stack templates are shared. Every listing is a manifest the SDK, CLI and this API understand; installing
records it on your account and returns the manifest and ready-made snippets, and `registry://<slug>`
references in a stack file resolve to `GET /api/v1/registry/{slug}/manifest`. Browsing is public;
installing, rating and publishing need an account. See the [marketplace guide](https://opensmartroute.ai/docs/MARKETPLACE.md) for
finding, installing, publishing and the review lifecycle; the endpoints are in the
REST API reference (`platform/api/openapi.json`) under *Marketplace*.

## 12. MCP server

The platform is also a [Model Context Protocol](https://modelcontextprotocol.io) server, so an IDE
or agent (VS Code, Cursor, Claude, Windsurf) can route, quote and explain through it. `GET /mcp`
describes the server without a key; `POST /mcp` accepts JSON-RPC 2.0 messages with the same
`Authorization` header as the REST API. Tools: `route`, `estimate`, `recommend`, `explain`,
`list_targets`, `feedback`, `ask` (pro plan and above; routes and executes) and `marketplace_search` /
`marketplace_get`. Routed calls are metered like `/api/v1/route`.

Clients that only speak stdio use the CLI as a bridge:

```json
{"mcpServers": {"opensmartroute": {"command": "osr", "args": ["mcp", "--url", "https://<platform>/mcp", "--api-key", "osr_..."]}}}
```

Per-client configuration, the tool reference and the SDK side are in [MCP.md](https://opensmartroute.ai/docs/MCP.md).

## 13. Dashboard

The site has two halves on one hostname. The **website** - landing page, `/docs`, `/models`, `/vendors`,
`/rankings`, `/pricing`, `/marketplace`, `/compare`, `/roi`, `/estimate` - needs no account. The **platform**
is everything under `/platform`: sign-in and sign-up (`/platform/login`, `/platform/signup`, password
recovery, invitations, `/platform/cli/authorize`), the dashboard (`/platform/dashboard/...`), the operator
console (`/platform/admin/...`), the playground (`/platform/playground`) and the marketplace publish flow
(`/platform/marketplace/publish`). On the hosted service the two are separate deployments; the older
paths without the prefix (for example /dashboard/keys, /login or /auth/callback) redirect permanently to their
`/platform` counterpart with the query string intact, so bookmarks, emailed links and OAuth redirect URIs
registered before the split keep working.

Signed-in users have a dashboard for the data the API exposes:

| Page | What it shows |
|---|---|
| `/platform/dashboard` | Requests, tokens and spend for the last 30 days; plan and quota usage; the last 24 hours as a traffic and latency series (`GET /api/v1/telemetry/series`) and the active alerts (`GET /api/v1/alerts`). |
| `/platform/dashboard/usage` | Per-day and per-target usage (`GET /api/v1/usage`); CSV export of the daily series and both breakdowns. |
| `/platform/dashboard/savings` | Routed cost against a baseline model (`GET /api/v1/savings?baseline=<id>`); CSV export per day and per target for finance. |
| `/platform/dashboard/activity` | Request-level activity log: endpoint, target, domain, complexity, tokens, reported outcome - never the prompt text. Rows still in the tracer buffer open a span waterfall with the decision story, outcome and audit records. CSV export of the loaded rows. |
| `/platform/dashboard/events` | The router's event history for the workspace (`GET /api/v1/events`, persisted by the platform): a window of 1 hour to 7 days, spans and events with level and stage filters, search by request id / trace id / name, most frequent names, click-through to the request trace, CSV export. |
| `/platform/dashboard/learning` | How the router learns (`GET /api/v1/learning`): strategy weights, outcomes per target, the routing SLM (rows, sources, the targets it ranks, accuracy history, calibration) and the autopilot (schedule, drift, cycles, a retraining-in-progress indicator, champion vs challenger; operators can run a cycle). Fits and cycles export as CSV. |
| `/platform/dashboard/keys` | Create, name and revoke API keys. |
| `/platform/dashboard/tenants` | Tenant constraints and budgets (enterprise). |
| `/platform/dashboard/governance` | Controls in force, workspace policy editor, budgets and spend, audit status, retention, catalogue data boundaries. |
| `/platform/dashboard/health` | Deployment readiness (`GET /api/v1/status`), the active alerts, HTTP traffic and latency percentiles over time (`GET /api/v1/telemetry/series`), live counters, routing decisions and latency, circuit breakers, decision cache (enterprise). |
| `/platform/dashboard/notifications` | The alert inbox (firing and resolved episodes, mark read), delivery channels (email, webhook, Slack, Teams) with severity and rule filters and *Send test*, and the delivery log. The bell in the header shows the unread count. |
| `/platform/dashboard/audit` | Hash-chained decision trail with chain verification (enterprise); export as CSV or as the JSONL the offline verifier reads. |
| `/platform/dashboard/billing` | Plan, invoices and upgrades. |
| `/platform/dashboard/workspace` | The current workspace: name, slug, plan; create or switch to an organization; organization SSO. |
| `/platform/dashboard/members` | Members, roles and pending invitations of an organization. |
| `/platform/dashboard/listings` | Marketplace listings you published and everything you installed. |
| `/platform/dashboard/integrations` | MCP configuration for VS Code, Cursor, Windsurf, Claude Code and Claude Desktop with your key. |
| `/platform/dashboard/account` | Your sign-in identities, password and browser sessions. |

Every table with an *Export CSV* button downloads exactly the rows shown (UTF-8 with BOM, RFC 4180 quoting,
cells that start with `=`, `+`, `-` or `@` are prefixed so spreadsheets do not run them as formulas). A failed
read shows what it means - session expired, feature not on the plan, budget exhausted with the amounts, rate
limit with the retry delay, API unreachable - with a *Retry* button; an expired session returns to sign-in and
back to the page afterwards.

The public pages `/platform/playground` (route a request in the browser and copy the equivalent `curl`),
`/models` (every routing target with request, token, cost, latency and health statistics; also
`GET /api/v1/models`, and the reference LLM catalogue with vendor prices and context windows at
`GET /api/v1/llms`), `/vendors` (one page per LLM vendor with prices, context windows and benchmarks),
`/rankings` (targets ordered by observed quality per domain), `/roi` (estimated
saving against always calling one model), `/compare` (alternatives side by side) and `/marketplace` need
no account. Machine-readable companions: `/llms.txt` (documentation map for AI assistants, llmstxt.org;
every documentation page is also served as Markdown at `/docs/<page>.md` and advertised with a
`text/markdown` alternate link), `/llms-full.txt` (the whole documentation as one Markdown file),
`/feed.xml` (Atom feed of releases), `/sitemap.xml` and `/openapi.json` (the complete OpenAPI 3.1
document of the API - the committed snapshot the REST reference renders, not the subset one gateway
process happens to serve).

### Operator console

The people who run a deployment sign in at `/platform/admin/login` with an operator **username and password**
(`POST /api/v1/admin/auth/login` returns an `osr_op_` session token valid for twelve hours; send it as
`Authorization: Bearer`). The first operator comes from the environment: set
`OSR_PLATFORM_ADMIN_USERNAME` and `OSR_PLATFORM_ADMIN_PASSWORD` and the API creates it - or resets its
password - at start-up as a `superadmin`. The static `OSR_PLATFORM_ADMIN_TOKEN` (`X-Admin-Token`)
keeps working for automation and counts as a superadmin. Operators are separate from workspace users.

| Page | What it does | Endpoints |
|---|---|---|
| `/platform/admin` | Deployment counters: users, workspaces, keys, tenants, operators, sessions, requests, errors and provider cost of the last seven days, plan mix, sign-in configuration. | `GET /api/v1/admin/overview`, `GET /api/v1/admin/plans` |
| `/platform/admin/analytics` | The whole deployment over 7, 30 or 90 days: requests and failures per day with mean latency, cost per day, endpoint share, workspaces per plan, signups per day, the busiest workspaces and the targets that carried the traffic. | `GET /api/v1/admin/analytics?days=` |
| `/platform/admin/health` | This replica of the routing process: readiness checks, HTTP traffic and latency series (1 h / 6 h / 24 h), the last five minutes, circuit breakers per target, the deployment alerts in force and the alert evaluator; auto-refresh. | `GET /api/v1/admin/health?window=` |
| `/platform/admin/activity` | Every metered request across workspaces, newest first: workspace, endpoint, target, status, latency, tokens, cost, request id; filter by endpoint, failures, workspace or request id; load older pages; CSV export. | `GET /api/v1/admin/activity?limit=&before=&account_id=&endpoint=&target=&failed=&request_id=` |
| `/platform/admin/users` | The user directory: search by email or name, filter by status, email confirmation and sign-in method (password or SSO / key only), sort by joined, last sign-in, email or name, export the matching rows as CSV. Select rows for a bulk action: disable, enable, mark the email verified, sign out everywhere, send a password reset, delete (each user is reported as done or failed). Create one with a personal workspace, plan, optional password and first key (shown once; without a password a reset link is issued so the person chooses one). Per user: workspaces and roles, linked identities, sessions, lifecycle history; rename, disable, reset password or send a reset link, re-send or force the email confirmation, sign out everywhere, delete (workspaces they alone belonged to are disabled and their keys revoked). | `GET|POST /api/v1/admin/users` (`q`, `disabled`, `verified`, `method`, `sort`, `order`, `format=csv`), `POST /api/v1/admin/users/bulk`, `GET|PATCH|DELETE /api/v1/admin/users/{id}`, `DELETE /api/v1/admin/users/{id}/sessions`, `POST /api/v1/admin/users/{id}/reset-link`, `POST /api/v1/admin/users/{id}/verify-link`, `GET /api/v1/admin/users/{id}/audit` |
| `/platform/admin/workspaces` | The workspace directory: search, filter by kind, plan and status, sort by name, plan or created, export CSV; create an organization for an owner email. Per workspace: plan, name, slug, disable, delete; members with roles (add by email, change role, remove - the last owner stays); API keys (mint, revoke); tenants (create or replace a validated configuration, delete); policy, SSO and 30-day usage. *Open dashboard* (superadmins) mints a one-hour browser session on the workspace as its owner and switches to the workspace portal, which shows an *Operator view* banner with the way back; the view sees every plan feature (statistics, audit, tenants) whatever the workspace's plan, while quotas stay the workspace's; the owner sees the session (provider `operator`) in their session list and the audit log records `workspace.open`. | `GET|POST /api/v1/admin/accounts` (`q`, `kind`, `plan`, `disabled`, `sort`, `order`, `format=csv`), `GET|PATCH|DELETE /api/v1/admin/accounts/{id}`, `.../usage`, `.../members[/{user_id}]`, `.../keys[/{key_id}]`, `.../tenants/{slug}`, `POST /api/v1/admin/accounts/{id}/session` |
| `/platform/admin/tenants` | Every tenant across workspaces with its configuration. | `GET /api/v1/admin/tenants` |
| `/platform/admin/leads` | Product-qualified leads: workspaces scored on volume, tenants, EU or private data boundaries, agentic traffic, several teams and a company email domain; CSV export. | `GET /api/v1/admin/pql?days=&qualified_only=` |
| `/platform/admin/plans` | The plan catalogue - limits, price, feature matrix on this edition - and the workspaces on each plan. | `GET /api/v1/admin/plans`, `GET /api/v1/admin/overview` |
| `/platform/admin/slm` | The routing SLM service: model in service, prompts and outcomes ingested, improvement cycles (champion vs challenger), run a cycle, probe the model with a prompt. | `GET /api/v1/admin/slm`, `GET /api/v1/admin/slm/reports`, `POST /api/v1/admin/slm/cycle`, `POST /api/v1/admin/slm/predict` |
| `/platform/admin/providers` | Model providers: add an OpenAI-compatible endpoint from a preset, check it (latency, upstream model list), map catalogue targets to upstream models with an optional system prompt, disable or delete; shows the mounted file, what is attached on this replica and the targets that cannot execute yet. | `GET|POST /api/v1/admin/providers`, `GET|PATCH|DELETE /api/v1/admin/providers/{id}`, `POST /api/v1/admin/providers/{id}/check`, `PUT|DELETE /api/v1/admin/providers/models/{target_id}` |
| `/platform/admin/services` | Where each domain runs (in the API or as its own service) with a live health probe of every remote service. | `GET /api/v1/admin/services` |
| `/platform/admin/marketplace` | Marketplace moderation: the review queue and every other status, approve, reject or unpublish with a note, featured and verified flags, the imported catalogue per source and the scheduled refresh (run it now). | `GET /api/v1/admin/registry?status=`, `POST /api/v1/admin/registry/{slug}/approve`, `POST /api/v1/admin/registry/{slug}/reject`, `POST /api/v1/admin/registry/{slug}/unpublish`, `POST /api/v1/admin/registry/{slug}/flags`, `GET /api/v1/admin/registry/import`, `POST /api/v1/admin/registry/import/refresh` |
| `/platform/admin/notifications` | Deployment alerts as an inbox (readiness, error rate, latency, breakers, drift, autopilot, SLM, tracing), operator channels (email, webhook, Slack, Teams), the delivery log and *Evaluate now*. | `GET /api/v1/admin/notifications`, `POST /api/v1/admin/notifications/read`, `POST /api/v1/admin/notifications/evaluate`, `GET|POST /api/v1/admin/notifications/channels`, `PATCH|DELETE /api/v1/admin/notifications/channels/{id}`, `POST /api/v1/admin/notifications/channels/{id}/test`, `GET /api/v1/admin/notifications/deliveries` |
| `/platform/admin/reports` | The report scheduler: state, deployment recipients, weekly / monthly workspace counts, every digest sent (kind, window, recipients); *Send a digest now* for the deployment or one workspace to the configured recipients or an address; *Run the scheduler now*. | `GET /api/v1/admin/reports`, `POST /api/v1/admin/reports/send`, `POST /api/v1/admin/reports/tick` |
| `/platform/admin/newsletter` | Subscribers (filter, active only, add by hand, remove, CSV export) and campaigns: draft in plain text, live preview of the branded email, send a test to one address, send once to every active subscriber; sent campaigns stay as records. | `GET /api/v1/admin/newsletter`, `GET /api/v1/admin/newsletter/subscribers.csv`, `POST /api/v1/admin/newsletter/subscribers`, `DELETE /api/v1/admin/newsletter/subscribers/{id}`, `POST /api/v1/admin/newsletter/campaigns`, `GET|PATCH|DELETE /api/v1/admin/newsletter/campaigns/{id}`, `POST .../campaigns/{id}/test`, `POST .../campaigns/{id}/send` |
| `/platform/admin/operators` | Operator accounts (superadmins create, disable, promote and delete them; the last enabled superadmin cannot be removed), your own password and sessions. Superadmins open any operator's console sessions and revoke one or all of them - a compromised console account is locked out at once. | `GET|POST /api/v1/admin/operators`, `PATCH|DELETE /api/v1/admin/operators/{id}`, `GET|DELETE /api/v1/admin/operators/{id}/sessions`, `POST /api/v1/admin/auth/password`, `GET /api/v1/admin/auth/sessions`, `POST /api/v1/admin/auth/logout` |
| `/platform/admin/audit` | Deployment-wide account-lifecycle log: signups, sign-ins and failed attempts, password and email changes, invitations, roles, removals, deletions and every operator action (actor `operator`). Filter by action (the menu lists every action with its count), actor type, time window, user, workspace or free text over actor, IP and details; page back with *Load older*; export the matching events as CSV. | `GET /api/v1/admin/audit-log` (`action`, `actor_type`, `actor_id`, `user_id`, `account_id`, `q`, `after`, `before`, `limit`, `format=csv`) |
| `/platform/admin/mail` | Every email the platform composed with its transport (ACS, SMTP or outbox), delivery state and counters (composed, awaiting delivery, failed); filter by recipient, kind or delivery; open a message to see the rendered HTML and plain text, copy its link, resend it (with a transport) or delete it; send a test message to prove the transport. Without a transport this is the delivery channel. | `GET /api/v1/admin/mail` (`to`, `kind`, `unsent`, `failed`, `limit`, `offset`), `GET|DELETE /api/v1/admin/mail/{id}`, `POST /api/v1/admin/mail/{id}/resend`, `POST /api/v1/admin/mail/test` |
| `/platform/admin/settings` | The effective configuration grouped (sign-in, storage, services, learning, marketplace, mail, billing, observability) with the variable that sets each value; secrets show only whether they are set. Retention: request rows, persisted telemetry, audit log and outbox sizes, and *Run the sweep now*. | `GET /api/v1/admin/settings`, `GET /api/v1/admin/retention`, `POST /api/v1/admin/retention/purge` |
| `/platform/admin/search` | Support search from the header box: users by email or name, workspaces by name, slug or email, API keys by prefix, operators, tenants by slug and request ids - each hit links to its page. | `GET /api/v1/admin/lookup?q=` |

Every `/api/v1/admin/*` call is written to the access log with `operator:<username>` (or
`operator:admin-token`) as the actor. The console is served by the web app but every action is an API
call, so the same administration works from scripts with the static token. Operators keep both portals
open at once: the console's account menu leads to the workspace dashboard (a superadmin opens any
workspace from `/platform/admin/workspaces`) and the dashboard's account menu leads back to the console
while an operator session exists.

## 14. Privacy and data handling

* Prompt text is routed in memory and, when execution is requested, forwarded to the configured
  provider. The platform stores request metadata (endpoint, target, domain, complexity, token and cost
  counts), not the prompt or the answer.
* `profile` values are hashed before they reach the learners.
* PII detection runs on every request; `contains_pii` together with a tenant `data_boundary` keeps
  sensitive requests on private or on-prem targets. Details: [security model](https://opensmartroute.ai/docs/SECURITY.md).
* The public web pages load Google Analytics 4 in consent mode: no analytics or advertising cookies
  until the visitor accepts the banner, IP addresses truncated, nothing on the dashboard. The choice is
  stored in `localStorage` (`osr-consent`) and can be changed on the privacy page. Besides page views the
  tag records Core Web Vitals, clicks on outbound links and calls to action, code-snippet copies and the
  sign-up / sign-in events. Operators of a
  self-hosted web image leave `NEXT_PUBLIC_GA_MEASUREMENT_ID` empty to ship no tag at all.

## 15. Errors

All errors are JSON: `{"detail": "..."}` with a conventional status code and an `X-Request-Id` to quote.

| Status | Typical cause |
|---|---|
| `400` | Invalid `objective`/`constraints`/policy keys, unknown target id in a policy, malformed email, an organization-only call on a personal workspace. |
| `401` | Missing, invalid or revoked API key (`WWW-Authenticate: Bearer`). |
| `403` | Feature not in plan (`X-Upgrade`), role too low, signup disabled. |
| `404` | Unknown target, tenant, model or listing; enterprise-only feature on a community deployment. |
| `409` | Duplicate signup, tenant or workspace slug; removing the last owner. |
| `422` | Body failed validation (the response lists the offending fields). |
| `429` | Rate limit, daily quota or an exhausted workspace/tenant budget (`Retry-After`, `X-Budget-*`). |
| `501` | `/v1/chat/completions` on a deployment with no model provider configured. |
| `502` | The web app could not reach the API. |
| `503` | `/readyz` when a dependency is not ready (the body names the failing check). |

## Related

* REST API reference (`platform/api/openapi.json`) - every endpoint, parameter and schema (rendered at `/docs/api`).
* [Marketplace](https://opensmartroute.ai/docs/MARKETPLACE.md) - find, install and publish agents, skills, personas, prompts and templates.
* [User guide](https://opensmartroute.ai/docs/GUIDE.md) - targets, rules, plans and learning.
* [Deploy](https://opensmartroute.ai/docs/deploy.md) - run the same router on your own infrastructure with `osr serve`.
