# Workspaces, members and principals

Every inference key belongs to one organization, one workspace and one principal. A principal
is the identity a key acts for: a service, an end user or a member. Use workspaces and
principals to split limits and usage inside an organization. To sync members and groups from
an identity provider, use [SCIM provisioning](scim.md).

In the console, open Team > Members and Team > Workspaces.

## Rules for keys and principals

- A key's organization, workspace and principal cannot change. To change them, mint a new key.
- All keys of one principal share its RPM, TPM and concurrency limits.
- Request values never authenticate a principal: `user`, `metadata`, `x-user-id`,
  `x-tm-principal-id` and `x-tm-workspace-id`.
- Your backend creates an end-user principal and keeps that principal's key. Do not let an end
  user choose a different backend key.
- There is no per-request delegation or SSO.

These keys use the organization's **Default** workspace and its shared `_default` service
principal:

- existing keys;
- keys created through browser onboarding;
- keys created in the console, unless you choose another workspace there.

A shared older key keeps its access, but RouterPlus cannot tell its individual users apart.

## Management authentication

Management calls use a verified console session cookie. Inference keys have no management
rights.

- `x-tm-org-id: <UUID>` selects an organization. The session email must be an active member
  of it. The header never grants access.
- Without the header, RouterPlus picks the caller's own organization first, then their oldest
  active membership.
- `GET /api/identity/organizations` lists your memberships.
- The console shows your default organization. [`/console/byok`](https://demo.routerplus.com/console/byok)
  and the playground take `?org=<UUID>` to switch. Manage the other identities with the API
  below.

Request rules:

- POSTs need `Origin: https://demo.routerplus.com` (origin only) and `Content-Type: application/json`.
- Responses are not cacheable.
- Adding a membership sends no invitation or email.
- RouterPlus normalizes membership and member-principal emails like account emails: lowercase,
  `+tag` removed, and Gmail dots and domain normalized.

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

This API cannot disable, demote or replace the initial owner.

### Workspace roles

A person also has a role in each workspace: `admin`, `member` or `viewer`.

- An org owner or admin is a workspace admin in every workspace.
- An org viewer is a workspace viewer, unless the workspace has a row for them. Then they get
  that row's role in that workspace only.
- Set a row with `PUT /v1/workspaces/<id>/members/<email>` and `{role}`.

| Workspace role | View usage | Create keys | Disable keys | Manage members and settings |
|---|---|---|---|---|
| admin | Yes | Yes | Yes | Yes |
| member | Yes | Yes | Yes | No |
| viewer | Yes | No | No | No |

- The workspace role applies to key and principal writes, in the console and in the API below.
- A key's workspace is the workspace of its principal.
- A write that the role does not allow returns `403` with the reason.
- The API keys page in the console shows your role in each workspace.
- Capping or copying a key needs an org owner or admin.
- Removing a membership removes that person's access to this organization. Their memberships in
  other organizations stay valid.

## Identity endpoints

Each list GET returns at most 500 entries. Use the returned UUIDs for later writes. A create
returns `201`. An update or a member write returns `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 and principal bindings |
| `POST /api/identity/keys` | `{principal_id,name,connection_id?}` → `{key_id,key,copyable}`. 300 RPM. `copyable: true` means an owner or admin can copy the key again at `/console/keys`. It is `false` when the organization has copying turned off. |
| `POST /api/identity/keys/<id>` | `{disabled:boolean}`. Disabling deletes the key's copy, so it cannot be copied again. |

Principal fields:

- `subject` is a stable identifier from your backend. It is unique within a workspace and at
  most 160 characters. A leading `_` is reserved.
- `kind:member` with an existing member's `member_email` ties the principal to that member.
  Revoking the membership also stops their inference keys.
- An `end_user` or `service` principal does not depend on a membership. Disable it to revoke
  it.

With `connection_id`, RouterPlus checks that the connection is active and belongs to the same
organization. It then creates the key with that binding in one transaction, so an interrupted
BYOK setup never leaves a key that RouterPlus funds. Without `connection_id`, key creation works
as for any non-BYOK key.

### Revocation

- 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 requests see a revocation within 5 seconds. A stream already running can finish.
- A successful write returns only after every gateway can see it. If a write returns 503, check
  the state before you retry. The change may have saved anyway.

## Routes, limits and usage

The restrictions in force are the intersection of **org → workspace → key → request**. Attach a
[routing policy](/docs/routing-policies) to a workspace with
`POST /api/identity/workspaces/<id>` and `{route_policy_id:"..."}`. A workspace policy cannot
grant a route that an org or key policy denies.

The `/api/admission-limits` API accepts `scope:"workspace"` and `scope:"principal"`, with the
matching UUID in `subject`.

- The defaults for each are 300 RPM, 1,000,000 TPM and 8 concurrent requests.
- All org, key, model, platform and provider-pool limits also apply.
- These limits create no separate workspace or principal balances or spend caps. Money stays
  with the organization and the key.
- Set `outage_mode:"closed"` when your contract needs RouterPlus to refuse work while the
  counters are down.

Usage:

- `GET /v1/usage` and `/v1/generation?id=...` show the attempts of the calling principal and
  workspace, including its other keys.
- Unknown generations, and generations of other principals, return 404.
- Org balance fields are `null` for scoped principals. The default service principal keeps
  them.
- The console shows org-wide accounting to authorized members.
- Each attempt's route record holds `workspaceId`, `principalId` and up to three policy
  revisions.
