OAuth endpoints
Discovery, registration, authorization, tokens and revocation.
You do not need this page to connect Claude or ChatGPT — they do all of it for you. It is here for anyone building their own MCP client against us.
Discovery#
An unauthenticated POST to the MCP endpoint answers 401 with a WWW-Authenticate header naming where the metadata lives. Following it is the whole discovery step.
# POST https://app.prepaid.ai/api/mcp with no token answers:
#
# HTTP/1.1 401
# WWW-Authenticate: Bearer
# resource_metadata="https://app.prepaid.ai/.well-known/oauth-protected-resource/api/mcp",
# scope="workspace:read"RFC 9728 protected resource metadata. Names the authorization server.
RFC 8414 authorization server metadata. Names the four endpoints below.
/.well-known/oauth-protected-resource/api/mcp, and /api/mcp/.well-known/… is a different resource that does not exist here.Registration#
RFC 7591 dynamic client registration. No authentication.
A client registers itself with a JSON body carrying at least redirect_uris. It gets back a client_id and no secret — token_endpoint_auth_method is none, because these are public clients and PKCE is what proves a token request is theirs.
| Field | Type | Description |
|---|---|---|
| redirect_urisrequired | string[] | Where a code may be returned. HTTPS, or http on loopback for a desktop app. No fragments, and at most ten. |
| client_nameoptional | string | Shown to the customer on the approval screen, so they can see who is asking. |
| client_urioptional | string | A link to the client, shown alongside its name. |
Authorization#
Checks the request, then sends the person to the consent screen in their console.
| Field | Type | Description |
|---|---|---|
| client_idrequired | string | From registration. |
| redirect_urirequired | string | Must match one this client registered, exactly — never a prefix. |
| response_typerequired | string | Only code. |
| code_challengerequired | string | The S256 challenge for your verifier. |
| code_challenge_methodrequired | string | Only S256. plain is refused. |
| scopeoptional | string | Only workspace:read, which is also the default. Anything else is refused. |
| stateoptional | string | Returned to you untouched with the code. |
| resourceoptional | string | RFC 8707. The token is stamped with it, and refused at any other server. |
client_id or an unregistered redirect_uri is answered in the browser rather than redirected — redirecting an error to a URI we have not validated is how an open redirector gets built. Everything after that check goes back to your registered URI.Tokens#
Exchanges a code for tokens, or refreshes them. Form-encoded, not JSON.
curl https://app.prepaid.ai/api/oauth/token \
-d grant_type=authorization_code \
-d code=the_code_you_received \
-d code_verifier=your_pkce_verifier \
-d client_id=your_client_id \
-d redirect_uri=https://your.app/callback{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "workspace:read"
}- A code is spent the first time it is presented, whether or not the rest of the request was valid.
- The access token lasts an hour. The refresh token has no expiry.
grant_type=refresh_tokenrotates: the refresh token you sent is revoked and a new pair is issued.- Errors are RFC 6749 codes —
invalid_grant,invalid_request— with a readable sentence inerror_description.
Revocation#
RFC 7009. Form-encoded, with the token to revoke.
Revoking any token of a connection ends the whole connection, not just the row you sent — a client disconnecting expects to be disconnected.