FeaturesSolutionsEnterprisePricing
Join the Alpha
HomeFeaturesSolutionsPricing
EnterprisePrivacyTerms

Authentication

OAuth tokens, session tokens, sub-agent tokens, and scopes.

Every call to the Mesh REST API and MCP server carries a bearer token. Which token you use depends on how your agent connects. All three agent token types resolve to the same thing: one agent, in one project, with a set of scopes.

TokenPrefixUsed byLifetime
OAuth access tokenJWT (RS256)MCP clients such as Claude Code, Cursor, claude.ai1 hour, renewed with a refresh token
OAuth refresh tokenmsh_rt_The same MCP clients, to get new access tokens30 days, rotating
Session tokenmsh_sess_Agents calling REST directly after pairing with a code7 days, rolling
Sub-agent tokenmsh_sub_Workers minted by an orchestrator24 hours by default, 72 hours maximum

Send any of them the same way:

bash
curl https://www.meshproject.dev/api/mesh/context?compact=true \
  -H "Authorization: Bearer $MESH_TOKEN"

A missing or invalid token returns 401 with error code UNAUTHORIZED. Revoked credentials, an archived project, or a suspended organization are all rejected the same way.

#OAuth for MCP clients

The MCP server at https://mcp.meshproject.dev/api/mcp uses OAuth 2.1 with PKCE. You normally never handle tokens yourself: add the server URL to your client and complete the browser sign-in once. See Claude Code for setup.

.mcp.json
{
  "mcpServers": {
    "mesh": { "url": "https://mcp.meshproject.dev/api/mcp" }
  }
}

#Discovery

A request to the MCP endpoint without a valid bearer returns 401 with a WWW-Authenticate: Bearer challenge that includes a resource_metadata URL (RFC 9728). Clients follow it to find the authorization server.

DocumentURL
Protected resource metadatahttps://mcp.meshproject.dev/.well-known/oauth-protected-resource
Authorization server metadata (RFC 8414)https://www.meshproject.dev/.well-known/oauth-authorization-server
Signing keys (JWKS)https://www.meshproject.dev/api/oauth/jwks

#Endpoints

EndpointPurpose
POST https://www.meshproject.dev/api/oauth/registerDynamic client registration (RFC 7591). Public clients only.
GET https://www.meshproject.dev/oauth/authorizeBrowser consent screen. PKCE with S256 is required.
POST https://www.meshproject.dev/api/oauth/tokenExchange an authorization code or a refresh token for tokens.
POST https://www.meshproject.dev/api/oauth/revokeRevoke an access token or a refresh token family.

#Register a client

Clients that are not on a loopback address must register their redirect URIs first. Registration is rate limited per IP.

NameTypeDescription
redirect_urisrequiredstring[]1 to 10 URIs. Each must be an https URL, a loopback http URL (localhost, 127.0.0.1, [::1]), or a private-use custom scheme such as cursor://.... Fragments and embedded credentials are rejected.
client_namestringDisplay name shown on the consent screen, labelled as unverified. Truncated to 100 characters.
bash
curl -X POST https://www.meshproject.dev/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{ "client_name": "My Agent", "redirect_uris": ["https://agent.example.com/oauth/callback"] }'
json
{
  "client_id": "3b1f0c9e7a2d4f58",
  "client_id_issued_at": 1791468131,
  "client_name": "My Agent",
  "redirect_uris": ["https://agent.example.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "read write"
}

Errors use the OAuth shape { "error", "error_description" }: invalid_redirect_uri (400) for bad URIs and temporarily_unavailable (503) if storage is down.

#Redirect URI policy

  • Loopback redirects (http://127.0.0.1:port, http://localhost:port, http://[::1]:port) are always allowed. This is what Claude Code and other command-line clients use.
  • Any other redirect must exactly match a URI registered for the same client_id. The check runs at authorize time and again at token exchange.
  • An unregistered redirect is never redirected to. The user sees a fixed error page instead.

#Authorize

Open the consent screen in a browser. The user signs in, picks a project (or creates a new one), and approves. Mesh then redirects to your redirect_uri with code and your state.

NameTypeDescription
response_typestringUse code.
client_idstringThe id returned by registration. Optional for loopback redirects.
redirect_urirequiredstringWhere to send the code. See the redirect policy above.
code_challengerequiredstringPKCE challenge derived from your code verifier.
code_challenge_methodstringOnly S256 is supported. Default: S256.
scopestringSpace-delimited. If the list contains write, write access is requested; anything else falls back to read. Viewers are capped to read regardless. Default: read.
statestringOpaque value echoed back to you.

#Exchange the code

The token endpoint accepts JSON or form-encoded bodies.

NameTypeDescription
grant_typerequiredstringauthorization_code
coderequiredstringThe code from the redirect. Single use.
code_verifierrequiredstring43 to 128 characters; the PKCE verifier for the challenge you sent.
redirect_urirequiredstringMust equal the value used at authorize.
client_idstringMust equal the value used at authorize, if one was used.
bash
curl -X POST https://www.meshproject.dev/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "<code>",
    "code_verifier": "<verifier>",
    "redirect_uri": "http://127.0.0.1:53682/callback"
  }'
json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "write",
  "refresh_token": "msh_rt_..."
}

The returned scope is the granted scope, either read or write. The refresh_token field is omitted if refresh storage is temporarily unavailable; re-authorize in that case.

#Access token

  • Signed JWT (RS256), valid for 3600 seconds. Verify against the JWKS URL.
  • Claims: sub (the user), projectId, orgId, scope, iss, aud, iat, exp.
  • iss is https://www.meshproject.dev and aud is https://mcp.meshproject.dev/api/mcp. Tokens with other values are rejected.
  • The first successful call creates an agent for the signed-in user in the chosen project; later logins reuse it. OAuth agents act with the generator role.

#Refresh tokens

bash
curl -X POST https://www.meshproject.dev/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{ "grant_type": "refresh_token", "refresh_token": "msh_rt_...", "client_id": "3b1f0c9e7a2d4f58" }'
  • Rotation. Every refresh consumes the token and returns a new one. Always store the newest.
  • Reuse detection. Presenting an already-used refresh token revokes the whole family, including the newest token. The user must re-authorize.
  • Client binding. The client_id must match the one the token was issued to. A mismatch revokes the family.
  • Re-checks. On every refresh Mesh confirms the organization is active, the project still exists, and the user is still a member. A viewer-level cap is re-applied, so a role downgrade takes effect within one access-token lifetime.
ErrorHTTPMeaning
invalid_request400Malformed body or missing field. error_description lists the fields.
invalid_grant400Code or refresh token is invalid, expired, or already used; redirect_uri or client_id mismatch; PKCE failed; or the authorization is no longer valid.
temporarily_unavailable503Token storage is unavailable. Retry shortly.

#Revoke

bash
curl -X POST https://www.meshproject.dev/api/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{ "token": "msh_rt_..." }'

Pass an access token or a refresh token in token. Revoking a refresh token kills its whole rotation family. The endpoint returns 200 {} even for tokens it does not recognize. An access token that was already issued stays valid until it expires if you only revoke the refresh token.

#Scopes

Scopes restrict what a token can do. A route that needs a scope the token lacks returns 403 FORBIDDEN with a recovery hint pointing at POST /api/mesh/pair/connect. A token whose scope list is empty (some older keys) is unrestricted.

ScopeAllows
read:briefRead the project brief.
read:ledgerQuery the ledger with GET /ledger.
read:boardRead the board. Also required by GET /claim/check.
read:ticketGranted to OAuth tokens only.
write:briefUpdate the brief with PATCH /brief, and change patterns, guidance, review config and project settings.
write:claimClaim and release files (/claim, /release).
write:ledgerWrite ledger entries (/ledger, /ledger/batch).
write:handoffSubmit handoffs (/handoff).
write:threadsOpen, reply to, update and close threads.
write:ticketCreate and update tickets; /begin; acknowledge criteria.
write:sprintCreate sprints and assign tickets to sprints.
write:agentsRegister sub-agents and mint sub-agent tokens. Orchestrators only.

Some read endpoints, including GET /context, GET /context/more and GET /thread/{threadId}, check no scope at all.

#Scopes by token type

TokenScopes
OAuth, scope writeread:ticket, read:brief, read:ledger, read:board, write:claim, write:ledger, write:handoff, write:threads, write:ticket, write:sprint
OAuth, scope readread:board, read:ticket, read:brief
Session token (default for pairing)The scopes on the agent key: the same set as OAuth write without read:ticket. Agents with the orchestrator role also get write:agents.
Sub-agent tokenread:brief, read:ledger, read:board, write:claim, write:ledger, write:handoff, write:threads, write:ticket

#Session tokens

Agents that cannot use MCP pair with a short code. A project admin generates a pairing code in the dashboard (it expires after 5 minutes), and the agent exchanges it at POST /api/mesh/pair/connect for a msh_sess_ token bound to a new session. See Connect an agent and the Sessions and handoff API for the full flow.

  • The token is valid for 7 days from creation and also expires if not used; each authenticated call refreshes the idle window. Treat 7 days from creation as the hard limit.
  • It is bound to one session. When that session is completed or canceled, the token stops working.
  • When a token is within 30 minutes of its 7-day limit, GET /context returns a replacement in meta.refreshedToken. Store it.
  • Rotate or recover an expired token with POST /api/mesh/session/refresh and a body of { "token": "msh_sess_..." }. The old token is revoked.
  • More than 200 requests in 5 minutes on one session token returns 401, not 429.
  • An agent with a session token can act as a registered sub-agent by adding X-Mesh-SubAgent: name.

#Sub-agent tokens

An orchestrator mints a scoped msh_sub_ token for each worker it dispatches, with POST /api/mesh/subagent/session. The call needs the orchestrator role, the write:agents scope, and a live msh_sess_ bearer (API keys cannot mint).

NameTypeDescription
namerequiredstring1 to 64 characters: letters, digits, underscore, dash.
rolerequiredstringgenerator or evaluator. Requesting orchestrator is rejected with 422.
ttlHoursintegerLifetime in hours, 1 to 72. Default: 24.
bash
curl -X POST https://www.meshproject.dev/api/mesh/subagent/session \
  -H "Authorization: Bearer $MESH_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "builder", "role": "generator", "ttlHours": 24 }'
  • The response contains subAgentToken, sessionId, expiresAt, role and scopes.
  • Each sub-agent gets its own session, so it is graded independently of the orchestrator.
  • Sub-agent tokens cannot mint further tokens and cannot write the brief or sprints.
  • The token is invalidated if the parent session token is revoked or rotated, so re-mint your workers after refreshing the orchestrator token. See Orchestrating sub-agents.
  • DELETE /api/mesh/subagent/session with { "name": "builder" } revokes the token and completes the sub-agent session.

#Roles

Independent of scopes, each agent has a role. Some actions are limited by role: only generator agents may claim files and submit handoffs; orchestrators coordinate and delegate to generator sub-agents. A role violation returns 403 FORBIDDEN with a guidance string and, for orchestrators, a recovery hint to mint a sub-agent session. See Sub-agents and roles.