Discovery
FetchGET /.well-known/oauth-authorization-server from the API origin, and use the endpoints and methods it returns. Protected-resource metadata is at /.well-known/oauth-protected-resource/mcp and /.well-known/oauth-protected-resource/api/v1.
Whirl uses the authorization code and refresh token grants, so a user always approves access. A client secret or signing key identifies your app when it exchanges a code.
Register your app
Client ID Metadata Document (recommended)
Host a JSON file over HTTPS. Its URL is your client ID. For a public client:client_id must exactly match the file’s URL, and redirects must use one of the listed redirect_uris. See client registration for the redirect rules and signing keys.
Dynamic Client Registration
Register by posting your metadata toPOST /oauth/register, the registration_endpoint in discovery. Store the client_id, and any client_secret, that it returns.
Registered clients can use none, client_secret_basic, client_secret_post or private_key_jwt. Registering doesn’t grant access; a user still has to approve your app. Both registration methods work for MCP clients and API clients.
Authorization code flow with PKCE
- For each sign-in attempt, generate a random
stateand a PKCEcode_verifier. Setcode_challenge = BASE64URL(SHA256(code_verifier)), without padding. - Send the user to the authorization endpoint with
response_type=code, yourclient_id, a registeredredirect_uri, a space-separatedscope,state,code_challengeandcode_challenge_method=S256. - Set
resourceto the API origin plus/mcpfor MCP, or/api/v1for the API. Repeat the parameter to request both. If you leave it out, you get all supported resources, but it’s better to be explicit. - Whirl asks the user to pick an organization, approve the scopes, and optionally limit which workspaces you can access.
- On your callback, check
statematches before you usecode, and handle error redirects. Don’t log the callback URL. - Exchange the code at the token endpoint straight away, with the same
redirect_uriand yourcode_verifier.
Access and refresh tokens
Access tokens last an hour. Useexpires_in from the response instead of hardcoding it. The granted scope can be narrower than what you asked for, so check it.
To keep access while the user is away, request offline_access. Each refresh returns a new refresh token. Refresh tokens expire after 30 days unused, or 90 days in total. Save the new refresh token before you discard the old one, and only run one refresh at a time per connection.
Revoking tokens
POST the token, with your client authentication, to/oauth/revoke as form data. Users can also disconnect apps in Settings → API & MCP. If a user leaves an organization or loses workspace access, that applies straight away, even if their token hasn’t expired.
Keeping credentials safe
Keep signing keys, client secrets and refresh tokens on your server. Don’t put them in frontend code, URLs, logs, analytics or source control. Access tokens are bearer tokens even if your app usesprivate_key_jwt: anyone who has one can use it.
