> ## Documentation Index
> Fetch the complete documentation index at: https://docs.whirl.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth

> Register your app, get a user's approval with PKCE, and manage tokens.

## 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

### Client ID Metadata Document (recommended)

Host a JSON file over HTTPS. Its URL is your client ID. For a public client:

```json theme={null}
{
  "client_id": "https://client.example.com/oauth/client.json",
  "client_name": "Example Whirl client",
  "redirect_uris": ["https://client.example.com/oauth/callback"],
  "token_endpoint_auth_method": "none"
}
```

`client_id` must exactly match the file's URL, and redirects must use one of the listed `redirect_uris`. See [client registration](/authentication/client-metadata) 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:

```bash theme={null}
curl --fail-with-body "https://api.whirl.sh/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "client_id=$WHIRL_CLIENT_ID" \
  --data-urlencode "code=$WHIRL_AUTHORIZATION_CODE" \
  --data-urlencode "redirect_uri=$WHIRL_REDIRECT_URI" \
  --data-urlencode "code_verifier=$WHIRL_CODE_VERIFIER" \
  --data-urlencode "resource=https://api.whirl.sh/api/v1"
```

Clients with a secret also authenticate with it, using Basic auth or form fields. Clients with a signing key add a [signed assertion](/authentication/private-key-jwt). 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.

```bash theme={null}
curl --fail-with-body "https://api.whirl.sh/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode "client_id=$WHIRL_CLIENT_ID" \
  --data-urlencode "refresh_token=$WHIRL_REFRESH_TOKEN"
```

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.

```bash theme={null}
curl --fail-with-body "https://api.whirl.sh/oauth/revoke" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=$WHIRL_CLIENT_ID" \
  --data-urlencode "token=$WHIRL_REFRESH_TOKEN"
```

## 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.