prepaid.ai docs
StatusOpen the console
API referenceOAuth endpoints

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.

The challengebash
# 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"
GET/.well-known/oauth-protected-resource/api/mcp

RFC 9728 protected resource metadata. Names the authorization server.

GET/.well-known/oauth-authorization-server

RFC 8414 authorization server metadata. Names the four endpoints below.

Worth knowingThe well-known segment is INSERTED before the resource path, never appended: the document is at /.well-known/oauth-protected-resource/api/mcp, and /api/mcp/.well-known/… is a different resource that does not exist here.

Registration#

POST/api/oauth/register

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.

FieldTypeDescription
redirect_urisrequiredstring[]Where a code may be returned. HTTPS, or http on loopback for a desktop app. No fragments, and at most ten.
client_nameoptionalstringShown to the customer on the approval screen, so they can see who is asking.
client_urioptionalstringA link to the client, shown alongside its name.

Authorization#

GET/api/oauth/authorize

Checks the request, then sends the person to the consent screen in their console.

FieldTypeDescription
client_idrequiredstringFrom registration.
redirect_urirequiredstringMust match one this client registered, exactly — never a prefix.
response_typerequiredstringOnly code.
code_challengerequiredstringThe S256 challenge for your verifier.
code_challenge_methodrequiredstringOnly S256. plain is refused.
scopeoptionalstringOnly workspace:read, which is also the default. Anything else is refused.
stateoptionalstringReturned to you untouched with the code.
resourceoptionalstringRFC 8707. The token is stamped with it, and refused at any other server.
NoteAn unknown 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#

POST/api/oauth/token

Exchanges a code for tokens, or refreshes them. Form-encoded, not JSON.

Exchanging a codebash
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
Responsejson
{
  "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_token rotates: 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 in error_description.

Revocation#

POST/api/oauth/revoke

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.

NoteThis answers 200 for a token we have never seen, which is the RFC’s rule rather than an oversight: the goal is "this token is not usable", and an unknown token already satisfies it. Distinguishing the cases would make the endpoint an oracle for guessing which tokens exist.