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

# OAuth 2.0 & OpenID Connect

> Authenticate with the Rootly API via OAuth 2.0 and OpenID Connect: browser login for CLIs, third-party app consent flows, and automation tokens.

<Note>
  OAuth 2.0 is an alternative to [API keys](/api-reference/overview#how-to-generate-an-api-key). Both authentication methods are supported and accept the same `Authorization: Bearer <token>` header on every API endpoint. You can keep using API keys — nothing changes for existing integrations.
</Note>

## When to use OAuth 2.0

Reach for OAuth 2.0 over API keys when you need any of the following:

* **Browser-based login for CLI, TUI, or desktop tools** — end users sign in through Rootly and consent to scopes instead of copy-pasting a token. See the [CLI & TUI quick start](#quick-start-cli-tui-tools).
* **Third-party applications** that act on behalf of a Rootly user and need scoped, revocable access.
* **Unattended automation** (CI, internal services, MCP servers) that needs a user-independent token scoped to a specific team and permission set.
* **OpenID Connect discovery** — MCP clients and SSO-style integrations that expect `.well-known/openid-configuration` and a signed ID token.

## Managing OAuth applications

Team admins can create, edit, and revoke OAuth applications from **Organization Settings → OAuth Applications**. Both public and confidential auth-code clients can also self-register at runtime via [Dynamic Client Registration](#dynamic-client-registration-rfc-7591) — no admin setup required. Only client-credentials (server-to-server) apps must be created by an admin.

## Endpoints

OAuth endpoints are served from `https://rootly.com` (the same host users log in to). API calls authenticated with the resulting token still go to `https://api.rootly.com`.

| Purpose                                  | Method | URL                                                         |
| ---------------------------------------- | ------ | ----------------------------------------------------------- |
| Authorization Server Metadata (RFC 8414) | `GET`  | `https://rootly.com/.well-known/oauth-authorization-server` |
| OpenID Connect Discovery                 | `GET`  | `https://rootly.com/.well-known/openid-configuration`       |
| JWKS                                     | `GET`  | `https://rootly.com/oauth/discovery/keys`                   |
| Authorization                            | `GET`  | `https://rootly.com/oauth/authorize`                        |
| Token                                    | `POST` | `https://rootly.com/oauth/token`                            |
| Token introspection                      | `POST` | `https://rootly.com/oauth/introspect`                       |
| Token revocation                         | `POST` | `https://rootly.com/oauth/revoke`                           |
| UserInfo (OIDC)                          | `GET`  | `https://rootly.com/oauth/userinfo`                         |
| Dynamic Client Registration (RFC 7591)   | `POST` | `https://rootly.com/oauth/register`                         |

Clients that support discovery can bootstrap from either `.well-known` document — all endpoint URLs, supported scopes, grant types, and signing algorithms are advertised there.

## Client types

| Type             | Client secret | Grant types                                | How to create                                            |
| ---------------- | ------------- | ------------------------------------------ | -------------------------------------------------------- |
| **Public**       | No (PKCE)     | `authorization_code`                       | Self-register via `/oauth/register`, or in the Rootly UI |
| **Confidential** | Yes           | `authorization_code`, `client_credentials` | Self-register via `/oauth/register`, or in the Rootly UI |

Public clients must use PKCE with `S256`. Native CLIs, desktop apps, and MCP clients should register as public.

Confidential clients created via Dynamic Client Registration receive a `client_secret` in the registration response — it is returned **once** and cannot be retrieved again. Client-credentials apps (no redirect URI, server-to-server only) must be created by a team admin in the UI.

## Quick start: CLI & TUI tools

Native CLIs and TUIs follow the [native app pattern](https://www.rfc-editor.org/rfc/rfc8252) — self-register at first launch, open the system browser for sign-in, and capture the authorization code on a local loopback port. No client secret, no copy-pasted tokens.

The full flow:

1. **Register a public client** (once, cached on disk) via Dynamic Client Registration.
2. **Generate a PKCE pair** (`code_verifier` + `code_challenge`).
3. **Start a loopback HTTP listener** on an ephemeral port — its URL is the `redirect_uri`.
4. **Open the system browser** to `https://rootly.com/oauth/authorize` with the PKCE challenge.
5. **Receive the `code` on the loopback listener**, then exchange it at `/oauth/token`.
6. **Persist the access + refresh tokens** to a per-user secret store.

End-to-end Python example:

```python theme={null}
import http.server, secrets, hashlib, base64, urllib.parse, webbrowser, requests

ROOTLY = "https://rootly.com"

# 1. Register once — cache client_id on disk for subsequent runs.
client = requests.post(f"{ROOTLY}/oauth/register", json={
    "client_name": "My CLI",
    "redirect_uris": [f"http://127.0.0.1:0/callback"],  # port set per-launch below
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code"],
    "response_types": ["code"],
}).json()
client_id = client["client_id"]

# 2. PKCE
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
    hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)

# 3. Loopback listener — capture ?code=… on the redirect.
code_holder = {}
class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        qs = urllib.parse.urlparse(self.path).query
        code_holder.update(urllib.parse.parse_qs(qs))
        self.send_response(200); self.end_headers()
        self.wfile.write(b"You can close this window.")
    def log_message(self, *_): pass

server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
redirect_uri = f"http://127.0.0.1:{server.server_port}/callback"

# 4. Open browser to /oauth/authorize.
params = urllib.parse.urlencode({
    "response_type": "code",
    "client_id": client_id,
    "redirect_uri": redirect_uri,
    "scope": "openid profile email ir.incidents:read",
    "state": state,
    "code_challenge": challenge,
    "code_challenge_method": "S256",
})
webbrowser.open(f"{ROOTLY}/oauth/authorize?{params}")
server.handle_request()  # blocks until the redirect arrives

assert code_holder["state"][0] == state, "state mismatch"

# 5. Exchange code for tokens.
tokens = requests.post(f"{ROOTLY}/oauth/token", data={
    "grant_type": "authorization_code",
    "code": code_holder["code"][0],
    "redirect_uri": redirect_uri,
    "client_id": client_id,
    "code_verifier": verifier,
}).json()

# 6. Use the token against the resource API.
me = requests.get(
    "https://api.rootly.com/v1/users/me",
    headers={"Authorization": f"Bearer {tokens['access_token']}"},
).json()
print(me)
```

<Tip>
  **Where to store tokens.** Use the OS keychain — `keyring` on Python, `go-keyring` on Go, the system credential helpers on macOS/Windows/Linux. Never write tokens to a plaintext file in `$HOME` or check them into a repo.
</Tip>

### TUI tools that can't open a browser

If the TUI runs over SSH or in a headless container, print the authorize URL and have the user open it on their workstation. Use SSH port forwarding so the loopback redirect still lands on the remote host:

```bash theme={null}
ssh -L 7890:127.0.0.1:7890 your-server
```

Then register with `http://127.0.0.1:7890/callback` as the redirect URI — the browser redirects to the forwarded port, and the TUI captures the code locally.

### Token lifecycle in CLIs

* Access tokens last **1 hour**. Refresh proactively on `401` or when expiry is \<5 min away.
* Refresh tokens rotate on use — overwrite the cached refresh token after every refresh.
* On logout, call `POST /oauth/revoke` with the refresh token and clear the keychain entry.

## Scopes

Scopes are domain-prefixed and combine with your Rootly role to form a permission ceiling — **effective permissions = user RBAC ∩ granted scopes**. Granting a scope never elevates a user beyond what their role already allows.

### OIDC scopes

| Scope     | Purpose                                                   |
| --------- | --------------------------------------------------------- |
| `openid`  | Required for OIDC flows; enables `id_token` issuance.     |
| `profile` | Adds `name`, `team_id`, `role`, `on_call_role` to claims. |
| `email`   | Adds `email` to claims.                                   |

### Meta scopes

| Scope    | Grants                                                               |
| -------- | -------------------------------------------------------------------- |
| `all`    | Full read/write across every Incident Response and On-Call resource. |
| `ir.all` | Full read/write across every Incident Response resource.             |
| `oc.all` | Full read/write across every On-Call resource.                       |

### Incident Response (`ir.*`)

Each resource exposes `:read` and `:write`. For example, `ir.incidents:read` or `ir.services:write`.

`incidents`, `services`, `environments`, `functionalities`, `severities`, `incident_types`, `incident_roles`, `workflows`, `catalogs`, `groups`, `playbooks`, `retrospectives`, `status_pages`, `form_fields`, `pulses`

### On-Call (`oc.*`)

Each resource exposes `:read` and `:write`. For example, `oc.alerts:read` or `oc.schedules:write`.

`alerts`, `schedules`, `escalation_policies`, `alert_routing_rules`, `heartbeats`, `alert_sources`, `live_call_routing`, `shift_overrides`

<Tip>
  `:write` includes read. Request the narrowest scope set your integration needs — users see the full list on the consent screen.
</Tip>

## Authorization Code flow (with PKCE)

Use this flow for end-user sign-in. Required for public clients, recommended for confidential clients.

### 1. Register the client

Either call the Dynamic Client Registration endpoint (see below) or have a team admin create the application in **Organization Settings → OAuth Applications**.

### 2. Redirect the user to `/oauth/authorize`

```
https://rootly.com/oauth/authorize
  ?response_type=code
  &client_id=<CLIENT_ID>
  &redirect_uri=<REDIRECT_URI>
  &scope=openid%20profile%20email%20ir.incidents:read
  &state=<RANDOM_STATE>
  &code_challenge=<PKCE_CHALLENGE>
  &code_challenge_method=S256
```

The user signs in, selects the team the token will operate against, and reviews the requested scopes on the consent screen.

### 3. Exchange the code for tokens

Authorization codes are single-use and expire after **60 seconds** — exchange them immediately.

**Public client** (PKCE, no secret):

```bash theme={null}
curl --request POST \
  --url https://rootly.com/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=authorization_code \
  --data code=<CODE> \
  --data redirect_uri=<REDIRECT_URI> \
  --data client_id=<CLIENT_ID> \
  --data code_verifier=<PKCE_VERIFIER>
```

**Confidential client** (authenticate with client secret via HTTP Basic; PKCE still recommended):

```bash theme={null}
curl --request POST \
  --url https://rootly.com/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --user '<CLIENT_ID>:<CLIENT_SECRET>' \
  --data grant_type=authorization_code \
  --data code=<CODE> \
  --data redirect_uri=<REDIRECT_URI> \
  --data code_verifier=<PKCE_VERIFIER>
```

Response:

```json theme={null}
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "id_token": "…",
  "scope": "openid profile email ir.incidents:read"
}
```

### 4. Call the Rootly API

```bash theme={null}
curl --request GET \
  --header 'Content-Type: application/vnd.api+json' \
  --header 'Authorization: Bearer <ACCESS_TOKEN>' \
  --url https://api.rootly.com/v1/incidents
```

### 5. Refresh the token

Access tokens expire after **1 hour**. Refresh tokens rotate on use — the old refresh token is invalidated after a short grace period.

**Public client:**

```bash theme={null}
curl --request POST \
  --url https://rootly.com/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=refresh_token \
  --data refresh_token=<REFRESH_TOKEN> \
  --data client_id=<CLIENT_ID>
```

**Confidential client:**

```bash theme={null}
curl --request POST \
  --url https://rootly.com/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --user '<CLIENT_ID>:<CLIENT_SECRET>' \
  --data grant_type=refresh_token \
  --data refresh_token=<REFRESH_TOKEN>
```

## Client Credentials flow

Use this flow for server-to-server automation where no end user is involved (CI jobs, scheduled tasks, internal services). Requires a **confidential** application created by a team admin.

When the application is created, Rootly auto-provisions a dedicated service user in the team. The service user's `Role` and `OnCallRole` permissions are derived from the app's scopes, so the token's effective access is exactly what the scopes describe.

```bash theme={null}
curl --request POST \
  --url https://rootly.com/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --user '<CLIENT_ID>:<CLIENT_SECRET>' \
  --data grant_type=client_credentials \
  --data scope='ir.incidents:write oc.alerts:read'
```

Client-credentials applications must request at least one resource scope (`ir.*` or `oc.*`).

## Dynamic Client Registration (RFC 7591)

CLIs, desktop apps, MCP clients, and third-party integrations can self-register without authentication. Both public and confidential auth-code clients are supported.

**Public client** (no secret — typical for CLIs and native apps):

```bash theme={null}
curl --request POST \
  --url https://rootly.com/oauth/register \
  --header 'Content-Type: application/json' \
  --data '{
    "client_name": "My CLI",
    "redirect_uris": ["http://127.0.0.1:7890/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code"],
    "response_types": ["code"]
  }'
```

Response:

```json theme={null}
{
  "client_id": "…",
  "client_id_issued_at": 1745000000,
  "client_secret_expires_at": 0,
  "client_name": "My CLI",
  "redirect_uris": ["http://127.0.0.1:7890/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "scope": "openid profile email ir.incidents:read ir.incidents:write …"
}
```

**Confidential client** (secret returned once — typical for server-side web apps):

```bash theme={null}
curl --request POST \
  --url https://rootly.com/oauth/register \
  --header 'Content-Type: application/json' \
  --data '{
    "client_name": "My Web App",
    "redirect_uris": ["https://myapp.example.com/callback"],
    "token_endpoint_auth_method": "client_secret_basic",
    "grant_types": ["authorization_code"],
    "response_types": ["code"]
  }'
```

The response includes a `client_secret` field — **store it immediately**, as it cannot be retrieved again.

<Warning>
  Client-credentials apps (server-to-server, no redirect URI) cannot be created via Dynamic Client Registration. A team admin must create them in **Organization Settings → OAuth Applications**.
</Warning>

Rules:

* `token_endpoint_auth_method` must be one of: `none`, `client_secret_post`, or `client_secret_basic`.
* Redirect URIs must use HTTPS. HTTP is allowed for the loopback addresses `127.0.0.1`, `[::1]`, and `localhost` for local development.
* If no `scope` is provided, the app is registered with all available granular scopes so the user can narrow access on the consent screen.
* Meta scopes (`all`, `ir.all`, `oc.all`) are not accepted during registration — use individual `ir.*` and `oc.*` scopes instead.
* Registration is rate-limited to **10 requests per hour per IP**.
* The team the application operates against is assigned when the first user authorizes it.

## UserInfo

```bash theme={null}
curl --request GET \
  --header 'Authorization: Bearer <ACCESS_TOKEN>' \
  --url https://rootly.com/oauth/userinfo
```

Returned claims depend on the granted OIDC scopes:

| Claim          | Requires scope | Description                               |
| -------------- | -------------- | ----------------------------------------- |
| `sub`          | `openid`       | Rootly user ID.                           |
| `email`        | `email`        | User email.                               |
| `name`         | `profile`      | User full name.                           |
| `team_id`      | `profile`      | Team the token is scoped to.              |
| `role`         | `profile`      | Incident Response role name on that team. |
| `on_call_role` | `profile`      | On-Call role name on that team.           |

ID tokens are signed with **RS256**. Fetch signing keys from `https://rootly.com/oauth/discovery/keys`.

## Authenticating API calls

On Rootly's resource API (`https://api.rootly.com/v1/*`), OAuth 2.0 access tokens and API keys use the same `Authorization: Bearer …` header. The API tries API keys first, then OAuth tokens — you never need to tell Rootly which one you are sending.

```bash theme={null}
curl --request GET \
  --header 'Content-Type: application/vnd.api+json' \
  --header 'Authorization: Bearer <ACCESS_TOKEN_OR_API_KEY>' \
  --url https://api.rootly.com/v1/incidents
```

This applies only to resource endpoints under `/v1/*`. The OAuth protocol endpoints on `rootly.com` (`/oauth/token`, `/oauth/authorize`, `/oauth/register`, `/.well-known/*`) use their own authentication rules described above.

Rate limits, pagination, and the JSON:API contract are identical to the API-key path — see the [API Overview](/api-reference/overview).

## Using Rootly tokens with external services

Rootly's OAuth 2.0 + OIDC server isn't only for authenticating to Rootly's API. Any external service that supports OAuth 2.0 token validation can accept Rootly-issued tokens and use Rootly's UserInfo response as the source of truth for user identity, team scope, and role.

The pattern:

1. Your application obtains a Rootly OAuth token using one of the flows above.
2. The application sends the token as `Authorization: Bearer <ACCESS_TOKEN>` to the external service.
3. The external service validates the token by calling `https://rootly.com/oauth/userinfo` with that same `Authorization` header, then reads the returned claims (`sub`, `team_id`, `role`, `on_call_role`) for access decisions.

Because Rootly's UserInfo claims align with the field names most OAuth-aware proxies expect by default, the gateway-side configuration is usually three or four environment variables plus a single config toggle. The example below walks through LiteLLM specifically; the same shape applies to any OAuth 2.0 token-validating proxy.

### Example: LiteLLM AI gateway

[LiteLLM](https://docs.litellm.ai/) is an AI gateway proxy that supports [OAuth 2.0 token validation](https://docs.litellm.ai/docs/proxy/oauth2) as an Enterprise feature. Pointing it at Rootly's UserInfo endpoint lets your team reuse Rootly identities — and the existing Rootly role and team model — for AI-gateway access control, cost attribution, and rate limiting.

<Steps>
  <Step title="Configure LiteLLM to validate against Rootly">
    Set LiteLLM's environment variables to point at Rootly's UserInfo endpoint and the matching claim names:

    ```bash theme={null}
    export OAUTH_TOKEN_INFO_ENDPOINT="https://rootly.com/oauth/userinfo"
    export OAUTH_USER_ID_FIELD_NAME="sub"
    export OAUTH_USER_ROLE_FIELD_NAME="role"
    export OAUTH_USER_TEAM_ID_FIELD_NAME="team_id"
    ```

    Then enable OAuth 2.0 auth in LiteLLM's `config.yaml`:

    ```yaml theme={null}
    general_settings:
      master_key: sk-1234
      enable_oauth2_auth: true
    ```
  </Step>

  <Step title="Issue tokens with the right scopes">
    LiteLLM reads `sub`, `team_id`, and `role` from Rootly's UserInfo response. Those claims require the `openid` and `profile` scopes to be granted on the access token — see [OIDC scopes](#oidc-scopes). A typical scope set for gateway use:

    ```text theme={null}
    openid profile email
    ```

    The token itself does not need any `ir.*` or `oc.*` resource scopes because the request never touches Rootly's resource API — only the UserInfo endpoint, which any valid token can call.
  </Step>

  <Step title="Call the gateway with a Rootly token">
    Application code obtains a Rootly access token using whichever flow fits the deployment model:

    * **Interactive users** — Authorization Code with PKCE ([above](#authorization-code-flow-with-pkce))
    * **Server-to-server automation** — Client Credentials ([above](#client-credentials-flow))

    Then forwards the token to LiteLLM unchanged:

    ```bash theme={null}
    curl --request POST \
      --url http://your-litellm-host:4000/chat/completions \
      --header 'Authorization: Bearer <ROOTLY_ACCESS_TOKEN>' \
      --header 'Content-Type: application/json' \
      --data '{
        "model": "gpt-4",
        "messages": [{"role": "user", "content": "summarize the incident postmortem"}]
      }'
    ```

    LiteLLM calls Rootly's UserInfo endpoint with the same bearer token, extracts the user identity and team, and applies whatever per-user or per-team policies you've configured in LiteLLM (rate limits, allowed models, spend caps).
  </Step>
</Steps>

### Why this is useful

| Outcome                          | How Rootly OAuth makes it work                                                                                                                     |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| One identity surface             | A user added to Rootly automatically has gateway access; removal revokes both in lockstep.                                                         |
| Per-team cost attribution        | LiteLLM's spend tracking groups by `team_id` from the UserInfo response — same `team_id` Rootly already scopes incidents and on-call schedules to. |
| Central revocation               | Revoke a Rootly OAuth token via `POST /oauth/revoke` and the gateway stops honoring it immediately.                                                |
| No per-gateway credential sprawl | The gateway never holds long-lived API keys for individual users; it only validates short-lived OAuth tokens at request time.                      |

<Note>
  LiteLLM's OAuth 2.0 token validation is a paid Enterprise feature. The Rootly-side OAuth 2.0 server is the same one documented above and is included in standard Rootly access — no additional plan tier is required on Rootly's side to use Rootly as the identity provider for an external service.
</Note>
