MCP authorization

How MCP clients sign in to Pallyy with OAuth 2.1: discovery documents, client identification, PKCE, scopes, token lifetimes, refresh rotation, and revocation.

The Pallyy MCP server is protected by an OAuth 2.1 authorization server that Pallyy runs itself, following the MCP authorization specification. This page is for people building or configuring an MCP client. If you just want to connect Claude, ChatGPT, or Cursor, the MCP server page has the steps, and the consent flow takes care of everything below.

Discovery

A request to https://app.pallyy.com/mcp without a bearer token answers 401 with a challenge pointing at the protected resource metadata:

WWW-Authenticate: Bearer resource_metadata="https://app.pallyy.com/.well-known/oauth-protected-resource/mcp"

That document (RFC 9728) names the authorization server, and the server's own metadata (RFC 8414) lists every endpoint:

DocumentURL
Protected resource metadatahttps://app.pallyy.com/.well-known/oauth-protected-resource/mcp
Authorization server metadatahttps://app.pallyy.com/.well-known/oauth-authorization-server

The endpoints it advertises:

EndpointURL
Authorizationhttps://app.pallyy.com/oauth/authorize
Tokenhttps://app.pallyy.com/api/oauth/token
Revocationhttps://app.pallyy.com/api/oauth/revoke

Both discovery documents and the token and revocation endpoints allow cross-origin requests, so browser-based clients can complete the flow.

Client identification

Pallyy does not offer dynamic client registration. Clients identify themselves with a Client ID Metadata Document: the client_id is the https URL of a JSON document describing the client (its client_name, redirect_uris, and so on, in RFC 7591 format), and Pallyy fetches, validates, and caches that document the first time it sees the URL.

The metadata document, and every https redirect URI it lists, must be hosted on one of the supported platforms, including their subdomains:

  • claude.ai
  • chatgpt.com
  • cursor.com

Native apps such as Claude Code may instead redirect to a loopback address (http://localhost, http://127.0.0.1, or http://[::1]) on any port, as RFC 8252 allows. A client_id on any other host is rejected with invalid_client, which is why other MCP clients connect with an API key instead.

Every client is a public client. The only supported token_endpoint_auth_method is none, and PKCE is required.

Authorization request

The client sends the user to the authorization endpoint with these parameters:

ParameterRequiredMeaning
client_idYesThe URL of the client's metadata document
redirect_uriYesOne of the URIs registered in the document
response_typeYescode
code_challengeYesPKCE challenge, S256 only
code_challenge_methodYesS256
scopeNoSpace-separated scopes, see below
stateNoReturned unchanged on the redirect
resourceNoMust be https://app.pallyy.com/mcp when present (RFC 8707)

The user signs in to Pallyy if needed, reviews what the client asked for, optionally limits it to specific social sets, and approves or cancels. Approval redirects to redirect_uri with a single-use code that expires after 10 minutes, plus state and an iss parameter naming the issuer (RFC 9207). Cancelling redirects with error=access_denied.

Consent must come from the account owner in person: an administrator viewing an account on a user's behalf cannot approve a connection.

Scopes

The scopes are the same ones API keys use. Every token can read, so a request with no scope produces a read-only connection.

ScopeGrants
post-sets:writeCreate and update draft post sets
post-sets:publishRequired on top of post-sets:write to create or update post sets with status SCHEDULED
media:writeCreate media uploads
notes:writeCreate and update calendar notes
offline_accessNot a permission: asks for a refresh token so the connection outlives the access token

An unknown scope fails the request with invalid_scope. The consent page shows the user every scope requested, and the grant records exactly those.

Token exchange

The client exchanges the code at the token endpoint with a form-encoded or JSON POST:

ParameterMeaning
grant_typeauthorization_code
codeThe code from the redirect
client_idThe same client_id as the authorization request
redirect_uriThe same redirect_uri as the authorization request
code_verifierThe PKCE verifier matching code_challenge
resourceOptional, must match the authorization request when present

A successful exchange returns the tokens:

{
  "access_token": "pallyy_oat_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "pallyy_ort_...",
  "scope": "post-sets:write post-sets:publish offline_access"
}

A code is consumed on first use, so a failed exchange burns it and the client has to start over. The code is bound to the client, redirect URI, PKCE challenge, and resource it was issued for, and any mismatch fails with invalid_grant.

Token lifetimes

TokenPrefixLifetime
Access tokenpallyy_oat_1 hour
Refresh tokenpallyy_ort_30 days from issue, capped at 365 days after the first token of the connection

Access tokens are sent as Authorization: Bearer pallyy_oat_... to /mcp. They are also accepted by the REST API directly, with the grant's scopes and social set restriction, so a client that already holds a token can call https://app.pallyy.com/api/v1 with it.

A refresh token is only issued when the user approved offline_access. Refreshing uses grant_type=refresh_token with refresh_token and client_id, returns a new access token, and rotates the refresh token: the old one stops working and the new one gets a fresh 30-day lifetime, until the connection reaches its 365-day cap and the user has to approve it again.

Presenting a refresh token that has already been rotated is treated as a leak: every token issued from that authorization is revoked and the client has to send the user through consent again. Clients must store the rotated token as soon as the refresh response arrives.

Scopes and the social set restriction are read from the grant on every request rather than baked into the token. Re-approving a client updates its outstanding tokens immediately, and a grant that drops offline_access ends its refresh chain at the next rotation.

Revocation

Clients revoke a token by posting it to the revocation endpoint (RFC 7009) with token and client_id. Users revoke a whole connection from Settings > Connected Apps, which invalidates every token the client holds.

Errors

OAuth endpoints answer with the standard { "error", "error_description" } body. The codes you are most likely to meet:

CodeMeaning
invalid_clientThe client_id is not a metadata document URL on a supported host, or the document could not be fetched or is invalid
invalid_redirect_uriThe redirect_uri is not registered in the client's metadata document
invalid_requestA required parameter is missing, or PKCE is missing or not S256
invalid_scopeThe scope names something other than the scopes above
invalid_targetThe resource is not https://app.pallyy.com/mcp
invalid_grantThe code or refresh token is expired, used, revoked, or does not match the request
access_deniedThe user cancelled on the consent page

Once a token is issued, the MCP server and the REST API report problems with the API's own error format. A connection whose account no longer includes the MCP server gets a 403 with the code oauth-token:plan_disabled at the token endpoint, and apis_not_available on tool calls.

View as markdown

More in MCP server

Questions about the API? Email us at hey@pallyy.com and we'll get back to you.