# Authentication (/docs/authentication)

Every request resolves to **one principal inside one organization**, and that
pair decides which connectors, accounts, memories, and tools the response
contains — never the request body. Four credentials carry the two kinds of
principal a request can be, a member or an agent:

| Credential                        | Who is calling                                                                        | Reaches                                                                                |
| --------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Browser sign-in (MCP OAuth token) | You, a member — from Claude Code, Codex, Cursor, Copilot, or any OAuth-capable client | Your private connections, the organization's shared ones, and accounts shared with you |
| `sk-cs-…` API key                 | The member who created it — for CI and clients without OAuth                          | The same as that member                                                                |
| `sk-csa-…` agent key              | An agent principal, with an identity of its own                                       | The organization's shared connections and accounts shared with that agent              |
| Session JWT                       | You, a member — what the `cs` CLI and the dashboard hold                              | The same as browser sign-in                                                            |

## Header forms \[#header-forms]

```http
Authorization: Bearer <token>
x-api-key: <key>
```

`Authorization: Bearer` takes any of the four — an MCP OAuth token, a session
JWT, or an `sk-cs-…` / `sk-csa-…` key; `x-api-key` takes the keys only. An MCP
OAuth token is accepted only on `POST /mcp`: it is issued by a different
authorization server than the
session JWT and is audience-bound to the `/mcp` resource (RFC 8707). On the connector routes it answers `401 invalid_jwt` — use
an API key or a session JWT for `GET` and `DELETE /connectors`.

## Browser sign-in \[#browser-sign-in]

Interactive agents authorize `/mcp` with nothing to paste. The first call
returns `401` with a `WWW-Authenticate` header naming the RFC 9728
protected-resource metadata; the client discovers the authorization server at
`login.corespeed.io`, registers itself, runs OAuth 2.1 with PKCE in your
browser, and retries with the token. Per-client setup is on the
[MCP server](/docs/mcp) page.

A first sign-in from a client creates your organization on its first request
to `/mcp` — no dashboard visit is needed. A sign-in from before your organization
existed keeps working too: the worker resolves the organization server-side
until your next sign-in carries it.

The result is a **member session**: everything a signed-in member may do,
including the account-management tools below.

## API keys \[#api-keys]

A key belongs to an organization and lives until it is revoked, rotated, or
reaches its optional expiry. It can carry a **monthly spend cap** in credits:
once the month's usage reaches the cap, metered calls with that key stop until
the month rolls over or the cap is raised. Keys are per environment — a
production key is rejected on staging.

A key can also be bound, when it is created, to one of your
[environments](/docs/mcp#environments-a-session-with-fewer-accounts) — a named
subset of your accounts, not a deployment. It then reaches only that
environment's accounts, for its whole life.

Create and manage keys in [Dashboard → API keys](https://app.corespeed.io/keys),
with the `manage__keys_*` tools from a signed-in agent, or with the CLI:

```bash
cs login                      # browser sign-in; stores tokens locally
cs keys create production     # prints the sk-cs- secret once
cs mcp-config                 # emits agent config for /mcp
```

A member key **acts as you**: it sees your private connections, the
organization's shared ones, and accounts shared with you. It holds **no
organization permissions** unless you name them when you create it — the
Permissions section of the dashboard's key dialog, `cs keys create --permission <slug>`, or `permissions` on `POST /api-keys` and
`manage__keys_create` — and then only the ones you hold yourself. A key never
carries more than the token that minted it, and a key minted through an MCP
sign-in carries none. `cs whoami` and `manage__whoami` list what you hold. A key holds each slug only while you still do: if you are
demoted, the key loses it on its next request. Registering a remote MCP
server, for example, needs `connections:org`. The slugs are listed on
[manage\_\_\* tools](/docs/reference/tools/manage#permissions).

## Agents \[#agents]

An agent is the organization's third principal kind, alongside members and the
platform. Any member can create one — in
[Dashboard → Settings](https://app.corespeed.io/settings) or with
`manage__agents_create` — and becomes its **owner**: the member who answers for
it and who, with holders of `agents:manage`, may manage it. Ownership is
accountability, not permission: the agent inherits neither the owner's
connections nor the owner's role, and it never holds a permission.

Its credentials are `sk-csa-` keys, minted by the owner or a holder of `agents:manage` with
`manage__agents_key_create` or from the dashboard. A request carrying one
**authenticates as the agent** and reaches the organization's shared
connections and the accounts a member shared with that agent — never a
member's other private ones.

| Action  | Effect                                                                                                             | Its keys                               |
| ------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------- |
| Suspend | Reversible. Every request answers `403 agent_suspended` until the owner or a holder of `agents:manage` resumes it. | Stay intact — do not rotate them       |
| Retire  | Terminal. The record survives for attribution in the activity trail.                                               | Revoked — answer `401 invalid_api_key` |

## What a credential may do \[#what-a-credential-may-do]

Every credential is answered as its principal, with what that principal
holds in the organization. A member session, a browser sign-in and a member
key (`sk-cs-`) are the same member; the key carries the permissions it was
minted with, at most what the member holds. An agent key (`sk-csa-`) is the
agent, which holds no permission and owns no account, agent or environment.
What a tool or route does with that is its own rule — a permission, or
ownership — not a rule about keys:

| Tools and routes                                                                                                                                                                                      | Member session or browser sign-in   | Member key                                                                                                          | Agent key                                  |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| Reads: `manage__whoami`, `manage__keys_list`, `manage__agents_list`, `manage__accounts_list`, `manage__environments_list`, `GET /me`, `GET /me/org/members`                                           | Run                                 | Run                                                                                                                 | Run, as the agent                          |
| Owner-scoped writes: `manage__keys_revoke` / `rotate` / `update`, `manage__accounts_rename` / `remove` / `share` / `unshare`, `manage__agents_update` / `retire` on your own agents                   | Run                                 | Run                                                                                                                 | Only on what the agent owns (its own keys) |
| Permission-gated writes: organization settings and members, billing, `manage__remote_add` / `refresh` / `remove` and organization connections (`connections:org`), `agents:manage`, `agents:transfer` | Run with the role's permission      | Run when the key was minted with it and the member still holds it; otherwise `insufficient_role` / `admin_required` | Refused: an agent key holds none           |
| Minting: `POST /api-keys`, `POST /agents/:id/keys`, `manage__keys_create`, `manage__agents_key_create`                                                                                                | Run, under the credential's ceiling | Refused — `api_key_cannot_mint`                                                                                     | Refused — `api_key_cannot_mint`            |
| A member's own acts: `POST /agents`, `manage__agents_create`, the environment writes, `/me/sync`, `/me/orgs`, `/me/provision`, `/me/analytics-preferences`                                            | Run                                 | Run                                                                                                                 | Refused — `member_principal_required`      |
| `manage__switch_org`                                                                                                                                                                                  | Run (browser sign-in)               | `switch_org_unsupported` — a key's organization is fixed at mint                                                    | `switch_org_unsupported`                   |
| Deciding an approval: `POST /approvals/:id/decide`                                                                                                                                                    | Run                                 | Refused — `api_key_cannot_decide`                                                                                   | Refused — `api_key_cannot_decide`          |

`api_key_cannot_mint` exists because a key's spend cap is its own counter and
does not carry to the keys it would mint; `member_principal_required` because
an agent cannot own an agent or shape an environment; `api_key_cannot_decide`
because an approval is a person's click between an agent and its write, and
a member key is the credential agents run on. On `/mcp` the first two arrive
as `200` with `isError: true` and the code in `structuredContent.error`; on
the REST routes as `403` in the nested `error` envelope below.

`GET /me` itself answers an API key. It returns the key's principal — the
member who created a member key, or the agent an agent key authenticates as —
with the active organization and its standing; a member key also lists the
organizations that member belongs to.

## Requests without a credential \[#requests-without-a-credential]

Five routes are readable with no credential at all:

* `GET /health` — Liveness probe.
* `GET /version` — Deployed version.
* `GET /.well-known/oauth-protected-resource/mcp` — The RFC 9728 metadata browser sign-in reads before it has a token.
* `GET /.well-known/mcp/server-card.json` — The built-in tools, for registries that cannot sign in. It is not your tool list.
* `GET /connectors/:id/client-metadata.json` — The OAuth client metadata a provider fetches to learn who is asking for consent.

Two more carry a credential of their own instead of yours:

* `PUT <upload_url>` — A file upload into your Drive, carrying the one-use ticket issued by drive\_\_create\_upload.
* `GET /artifacts/…` — An artifact download, on a short-lived signed URL. Do not strip or reuse its signature.

## Failure states \[#failure-states]

Authentication failures use the nested `error` envelope, and the `code` says
what to fix:

```json title="401 response"
{
  "error": {
    "type": "authentication_error",
    "code": "missing_authorization",
    "message": "Authorization header or x-api-key is required"
  }
}
```

* `401` — the credential itself: the header is missing, the JWT failed
  verification, or the key is unknown, revoked, or expired (a retired agent's
  keys included). Fix or replace it; do not retry.
* `403` — the credential is valid but what it acts for is stopped. A suspended
  agent answers `agent_suspended`: resume the agent; a new key would be refused
  the same way. A suspended organization answers `org_suspended` on `/mcp`
  and every other organization route: contact support.
* `503` — the key authority was unreachable on a cache miss. CoreSpeed fails
  closed rather than trusting an unverified key; retry with backoff.

A credential that authenticates can still be refused for **standing** — a
billing hold or the key's monthly spend cap. On `/mcp` those arrive as tool
results, HTTP `200` with `isError: true`, and
discovery, account reads, and key management keep working through a hold.
Every code and its remedy is listed once, in
[Errors & status codes](/docs/reference/errors).