---
title: "MCP authorization"
description: "How MCP clients sign in to Pallyy with OAuth 2.1: discovery documents, client identification, PKCE, scopes, token lifetimes, refresh rotation, and revocation."
category: "mcp"
emoji: "🔐"
order: 3
datePublished: "2026-09-04"
dateUpdated: "2026-09-04"
---

The Pallyy [MCP server](/docs/api/mcp) 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](/docs/api/mcp) 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:

| Document | URL |
| --- | --- |
| Protected resource metadata | `https://app.pallyy.com/.well-known/oauth-protected-resource/mcp` |
| Authorization server metadata | `https://app.pallyy.com/.well-known/oauth-authorization-server` |

The endpoints it advertises:

| Endpoint | URL |
| --- | --- |
| Authorization | `https://app.pallyy.com/oauth/authorize` |
| Token | `https://app.pallyy.com/api/oauth/token` |
| Revocation | `https://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](/docs/api/mcp#connect-any-other-client-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:

| Parameter | Required | Meaning |
| --- | --- | --- |
| `client_id` | Yes | The URL of the client's metadata document |
| `redirect_uri` | Yes | One of the URIs registered in the document |
| `response_type` | Yes | `code` |
| `code_challenge` | Yes | PKCE challenge, S256 only |
| `code_challenge_method` | Yes | `S256` |
| `scope` | No | Space-separated scopes, see below |
| `state` | No | Returned unchanged on the redirect |
| `resource` | No | Must 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.

| Scope | Grants |
| --- | --- |
| `post-sets:write` | Create and update draft post sets |
| `post-sets:publish` | Required on top of `post-sets:write` to create or update post sets with status `SCHEDULED` |
| `media:write` | Create media uploads |
| `notes:write` | Create and update calendar notes |
| `offline_access` | Not 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`:

| Parameter | Meaning |
| --- | --- |
| `grant_type` | `authorization_code` |
| `code` | The code from the redirect |
| `client_id` | The same `client_id` as the authorization request |
| `redirect_uri` | The same `redirect_uri` as the authorization request |
| `code_verifier` | The PKCE verifier matching `code_challenge` |
| `resource` | Optional, must match the authorization request when present |

A successful exchange returns the tokens:

```json
{
  "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

| Token | Prefix | Lifetime |
| --- | --- | --- |
| Access token | `pallyy_oat_` | 1 hour |
| Refresh token | `pallyy_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](https://app.pallyy.com/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:

| Code | Meaning |
| --- | --- |
| `invalid_client` | The `client_id` is not a metadata document URL on a supported host, or the document could not be fetched or is invalid |
| `invalid_redirect_uri` | The `redirect_uri` is not registered in the client's metadata document |
| `invalid_request` | A required parameter is missing, or PKCE is missing or not S256 |
| `invalid_scope` | The `scope` names something other than the scopes above |
| `invalid_target` | The `resource` is not `https://app.pallyy.com/mcp` |
| `invalid_grant` | The code or refresh token is expired, used, revoked, or does not match the request |
| `access_denied` | The 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](/docs/api/errors). 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.
