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.
| Token | Prefix | Used by | Lifetime |
|---|---|---|---|
| OAuth access token | JWT (RS256) | MCP clients such as Claude Code, Cursor, claude.ai | 1 hour, renewed with a refresh token |
| OAuth refresh token | msh_rt_ | The same MCP clients, to get new access tokens | 30 days, rotating |
| Session token | msh_sess_ | Agents calling REST directly after pairing with a code | 7 days, rolling |
| Sub-agent token | msh_sub_ | Workers minted by an orchestrator | 24 hours by default, 72 hours maximum |
Send any of them the same way:
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.
{
"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.
| Document | URL |
|---|---|
| Protected resource metadata | https://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
| Endpoint | Purpose |
|---|---|
POST https://www.meshproject.dev/api/oauth/register | Dynamic client registration (RFC 7591). Public clients only. |
GET https://www.meshproject.dev/oauth/authorize | Browser consent screen. PKCE with S256 is required. |
POST https://www.meshproject.dev/api/oauth/token | Exchange an authorization code or a refresh token for tokens. |
POST https://www.meshproject.dev/api/oauth/revoke | Revoke 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.
| Name | Type | Description |
|---|---|---|
redirect_urisrequired | string[] | 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_name | string | Display name shown on the consent screen, labelled as unverified. Truncated to 100 characters. |
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"] }'{
"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.
| Name | Type | Description |
|---|---|---|
response_type | string | Use code. |
client_id | string | The id returned by registration. Optional for loopback redirects. |
redirect_urirequired | string | Where to send the code. See the redirect policy above. |
code_challengerequired | string | PKCE challenge derived from your code verifier. |
code_challenge_method | string | Only S256 is supported. Default: S256. |
scope | string | Space-delimited. If the list contains write, write access is requested; anything else falls back to read. Viewers are capped to read regardless. Default: read. |
state | string | Opaque value echoed back to you. |
#Exchange the code
The token endpoint accepts JSON or form-encoded bodies.
| Name | Type | Description |
|---|---|---|
grant_typerequired | string | authorization_code |
coderequired | string | The code from the redirect. Single use. |
code_verifierrequired | string | 43 to 128 characters; the PKCE verifier for the challenge you sent. |
redirect_urirequired | string | Must equal the value used at authorize. |
client_id | string | Must equal the value used at authorize, if one was used. |
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"
}'{
"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. issishttps://www.meshproject.devandaudishttps://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
generatorrole.
#Refresh tokens
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_idmust 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.
| Error | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | Malformed body or missing field. error_description lists the fields. |
invalid_grant | 400 | Code 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_unavailable | 503 | Token storage is unavailable. Retry shortly. |
#Revoke
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.
| Scope | Allows |
|---|---|
read:brief | Read the project brief. |
read:ledger | Query the ledger with GET /ledger. |
read:board | Read the board. Also required by GET /claim/check. |
read:ticket | Granted to OAuth tokens only. |
write:brief | Update the brief with PATCH /brief, and change patterns, guidance, review config and project settings. |
write:claim | Claim and release files (/claim, /release). |
write:ledger | Write ledger entries (/ledger, /ledger/batch). |
write:handoff | Submit handoffs (/handoff). |
write:threads | Open, reply to, update and close threads. |
write:ticket | Create and update tickets; /begin; acknowledge criteria. |
write:sprint | Create sprints and assign tickets to sprints. |
write:agents | Register 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
| Token | Scopes |
|---|---|
| OAuth, scope write | read:ticket, read:brief, read:ledger, read:board, write:claim, write:ledger, write:handoff, write:threads, write:ticket, write:sprint |
| OAuth, scope read | read: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 token | read: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 /contextreturns a replacement inmeta.refreshedToken. Store it. - Rotate or recover an expired token with
POST /api/mesh/session/refreshand a body of{ "token": "msh_sess_..." }. The old token is revoked. - More than 200 requests in 5 minutes on one session token returns
401, not429. - 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).
| Name | Type | Description |
|---|---|---|
namerequired | string | 1 to 64 characters: letters, digits, underscore, dash. |
rolerequired | string | generator or evaluator. Requesting orchestrator is rejected with 422. |
ttlHours | integer | Lifetime in hours, 1 to 72. Default: 24. |
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,roleandscopes. - 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/sessionwith{ "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.