# Workspaces, members and principals

Every inference key belongs to one org, workspace and principal. These bindings are
immutable: mint a replacement key to change identity. Multiple keys for one principal
share its RPM, TPM and concurrency limits. Request `user`, `metadata`, `x-user-id`,
`x-tm-principal-id` and `x-tm-workspace-id` values do not authenticate a principal.
Your trusted backend provisions an end-user principal and retains that principal's key;
do not let an end user choose a different backend key. There is no per-request
delegation, SSO or SCIM.

Existing keys and keys created through browser onboarding or the console use an org's **Default** workspace
and shared `_default` service principal. This preserves existing access. It does not
retroactively identify individual users of a shared legacy key.

## Management authentication

Use a verified console session cookie. Inference bearer keys have no management rights.
`x-tm-org-id: <UUID>` selects an org in which the session email must be an active member;
it never grants access. Without the header, the caller's own org is preferred, then their
oldest active membership. Query `/api/identity/organizations` to discover memberships.
The console shows your default org; [`/console/byok`](https://app.routerplus.com/console/byok) and the
playground take `?org=<UUID>` to switch, and the other scoped identities are managed with
the API below.

POSTs require `Origin: https://app.routerplus.com` (origin only) and `Content-Type: application/json`.
Responses are not cacheable. No API sends an invitation or email when adding membership.
Membership and member-principal emails use the same canonicalization as marketplace account emails
(lowercase, plus-tag removal, and Gmail dot/domain normalization).

| Role | Read org configuration | Manage workspaces, principals, keys, connections, policies, limits | Manage members |
|---|---|---|---|
| owner | Yes | Yes | Yes |
| admin | Yes | Yes | No |
| viewer | Yes | No | No |

Roles apply across the whole org. There are no workspace-specific human roles in this
version. The initial owner cannot be disabled, demoted or replaced through this API.
Removing a membership removes that session's authority in this org without invalidating
its memberships elsewhere.

## Identity endpoints

All collection GETs return at most 500 entries; use returned UUIDs for subsequent writes.
A create returns `201`; an update, and a member write, `200`.

| Request | JSON body / result |
|---|---|
| `GET /api/identity/organizations` | `{organizations:[{org_id,name,role}]}` |
| `GET /api/identity/members` | `{members:[{email,role,disabled}]}` |
| `POST /api/identity/members` | `{email,role:"admin"\|"viewer",disabled?:boolean}`; owner only, creates or updates |
| `GET /api/identity/workspaces` | Workspace metadata and policy pointers |
| `POST /api/identity/workspaces` | `{name}` → `{workspace_id}` |
| `POST /api/identity/workspaces/<id>` | Any of `{name,disabled,route_policy_id}`; `null` clears its policy |
| `GET /api/identity/principals` | Principal metadata |
| `POST /api/identity/principals` | `{workspace_id,kind:"service"\|"end_user"\|"member",subject,member_email?}` → `{principal_id}` |
| `POST /api/identity/principals/<id>` | `{disabled:boolean}` |
| `GET /api/identity/keys` | Metadata only, including workspace/principal bindings |
| `POST /api/identity/keys` | `{principal_id,name,connection_id?}` → `{key_id,key}`; raw key shown once; 300 RPM |
| `POST /api/identity/keys/<id>` | `{disabled:boolean}` |

`subject` is an opaque stable backend identifier, unique within a workspace, limited to
160 characters. Leading `_` is reserved. Use `kind:member` with an existing member's
`member_email` to make membership revocation also stop their inference keys. An `end_user`
or `service` principal is independent of human membership; disable it directly to revoke it.

Supplying `connection_id` while issuing a key checks that the connection is active and owned
by the same org, then creates the key with that binding in the same transaction. The BYOK
console uses this path so an interrupted setup never reveals a temporarily house-funded key.
Omitting it preserves the original key-creation behavior for non-BYOK callers.

Disabling a workspace denies all its keys. Disabling a principal denies all its keys.
Disabling a member denies their control access and their `member` principals. New dispatches
observe the existing five-second revocation maximum; a previously dispatched stream can
finish. A successful mutation response waits for publication of its Redis fence. If it
returns 503, inspect state before retrying: SQL may have committed before fence publication.

## Routes, limits and evidence

Effective authority is the intersection of **org → workspace → key → request** restrictions.
Attach an immutable routing policy with `POST /api/identity/workspaces/<id>` and
`{route_policy_id:"..."}`. This cannot grant a route denied by an org or key policy.

The `/api/admission-limits` API accepts `scope:"workspace"` and `scope:"principal"`,
with the corresponding UUID in `subject`. Defaults for each are 300 RPM, 1,000,000 TPM,
eight concurrent requests. All org/key/model/platform/provider-pool limits also apply.
These rate limits do not create separate workspace/principal monetary balances or spend
caps; money remains org/key scoped. Configure `outage_mode:"closed"` when contractual
limits require refusing work during counter failure.

`GET /v1/usage` and `/v1/generation?id=...` expose attempts belonging to the authenticated
principal and workspace, including its other keys. Unknown or foreign generations return
404. Org balance fields are `null` for explicitly scoped principals; the default
service principal retains those fields. The console provides org-wide accounting to
authorized members. Attempt route evidence records `workspaceId`, `principalId` and
up to three policy revisions.
