Skip to main content

Discovery

Fetch GET /.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

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 to POST /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

  1. For each sign-in attempt, generate a random state and a PKCE code_verifier. Set code_challenge = BASE64URL(SHA256(code_verifier)), without padding.
  2. Send the user to the authorization endpoint with response_type=code, your client_id, a registered redirect_uri, a space-separated scope, state, code_challenge and code_challenge_method=S256.
  3. Set resource to the API origin plus /mcp for MCP, or /api/v1 for 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.
  4. Whirl asks the user to pick an organization, approve the scopes, and optionally limit which workspaces you can access.
  5. On your callback, check state matches before you use code, and handle error redirects. Don’t log the callback URL.
  6. Exchange the code at the token endpoint straight away, with the same redirect_uri and your code_verifier.
For a public client:
Clients with a secret also authenticate with it, using Basic auth or form fields. Clients with a signing key add a signed assertion. API and MCP calls only take the access token, never the client secret.

Access and refresh tokens

Access tokens last an hour. Use expires_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.
Refreshes need the same client authentication as the code exchange. Reusing a code or an old refresh token revokes the connection. If a refresh fails partway and you lost the new token, ask the user to sign in again instead of retrying with the old one.

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 uses private_key_jwt: anyone who has one can use it.