Authentication

Who is calling — a signed-in member, a member's API key, or an agent — and how each credential reaches CoreSpeed.

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:

CredentialWho is callingReaches
Browser sign-in (MCP OAuth token)You, a member — from Claude Code, Codex, Cursor, Copilot, or any OAuth-capable clientYour private connections, the organization's shared ones, and accounts shared with you
sk-cs-… API keyThe member who created it — for CI and clients without OAuthThe same as that member
sk-csa-… agent keyAn agent principal, with an identity of its ownThe organization's shared connections and accounts shared with that agent
Session JWTYou, a member — what the cs CLI and the dashboard holdThe same as browser sign-in

Header forms

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

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

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 — 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, with the manage__keys_* tools from a signed-in agent, or with the CLI:

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.

Agents

An agent is the organization's third principal kind, alongside members and the platform. Any member can create one — in Dashboard → 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.

ActionEffectIts keys
SuspendReversible. Every request answers 403 agent_suspended until the owner or a holder of agents:manage resumes it.Stay intact — do not rotate them
RetireTerminal. The record survives for attribution in the activity trail.Revoked — answer 401 invalid_api_key

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 routesMember session or browser sign-inMember keyAgent key
Reads: manage__whoami, manage__keys_list, manage__agents_list, manage__accounts_list, manage__environments_list, GET /me, GET /me/org/membersRunRunRun, as the agent
Owner-scoped writes: manage__keys_revoke / rotate / update, manage__accounts_rename / remove / share / unshare, manage__agents_update / retire on your own agentsRunRunOnly 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:transferRun with the role's permissionRun when the key was minted with it and the member still holds it; otherwise insufficient_role / admin_requiredRefused: an agent key holds none
Minting: POST /api-keys, POST /agents/:id/keys, manage__keys_create, manage__agents_key_createRun, under the credential's ceilingRefused — api_key_cannot_mintRefused — 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-preferencesRunRunRefused — member_principal_required
manage__switch_orgRun (browser sign-in)switch_org_unsupported — a key's organization is fixed at mintswitch_org_unsupported
Deciding an approval: POST /approvals/:id/decideRunRefused — api_key_cannot_decideRefused — 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

Five routes are readable with no credential at all:

  • GET/healthLiveness probe.
  • GET/versionDeployed version.
  • GET/.well-known/oauth-protected-resource/mcpThe RFC 9728 metadata browser sign-in reads before it has a token.
  • GET/.well-known/mcp/server-card.jsonThe built-in tools, for registries that cannot sign in. It is not your tool list.
  • GET/connectors/:id/client-metadata.jsonThe 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

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

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.