Skip to main content
OAuth 2.0 is an alternative to API keys. 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.

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.
  • 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 — 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.
PurposeMethodURL
Authorization Server Metadata (RFC 8414)GEThttps://rootly.com/.well-known/oauth-authorization-server
OpenID Connect DiscoveryGEThttps://rootly.com/.well-known/openid-configuration
JWKSGEThttps://rootly.com/oauth/discovery/keys
AuthorizationGEThttps://rootly.com/oauth/authorize
TokenPOSThttps://rootly.com/oauth/token
Token introspectionPOSThttps://rootly.com/oauth/introspect
Token revocationPOSThttps://rootly.com/oauth/revoke
UserInfo (OIDC)GEThttps://rootly.com/oauth/userinfo
Dynamic Client Registration (RFC 7591)POSThttps://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

TypeClient secretGrant typesHow to create
PublicNo (PKCE)authorization_codeSelf-register via /oauth/register, or in the Rootly UI
ConfidentialYesauthorization_code, client_credentialsSelf-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 — 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:
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)
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.

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

ScopePurpose
openidRequired for OIDC flows; enables id_token issuance.
profileAdds name, team_id, role, on_call_role to claims.
emailAdds email to claims.

Meta scopes

ScopeGrants
allFull read/write across every Incident Response and On-Call resource.
ir.allFull read/write across every Incident Response resource.
oc.allFull 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
:write includes read. Request the narrowest scope set your integration needs — users see the full list on the consent screen.

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):
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):
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:
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "id_token": "…",
  "scope": "openid profile email ir.incidents:read"
}

4. Call the Rootly API

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:
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:
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.
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):
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:
{
  "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):
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.
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.
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

curl --request GET \
  --header 'Authorization: Bearer <ACCESS_TOKEN>' \
  --url https://rootly.com/oauth/userinfo
Returned claims depend on the granted OIDC scopes:
ClaimRequires scopeDescription
subopenidRootly user ID.
emailemailUser email.
nameprofileUser full name.
team_idprofileTeam the token is scoped to.
roleprofileIncident Response role name on that team.
on_call_roleprofileOn-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.
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.

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 is an AI gateway proxy that supports OAuth 2.0 token validation 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.
1

Configure LiteLLM to validate against Rootly

Set LiteLLM’s environment variables to point at Rootly’s UserInfo endpoint and the matching claim names:
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:
general_settings:
  master_key: sk-1234
  enable_oauth2_auth: true
2

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. A typical scope set for gateway use:
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.
3

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)
  • Server-to-server automation — Client Credentials (above)
Then forwards the token to LiteLLM unchanged:
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).

Why this is useful

OutcomeHow Rootly OAuth makes it work
One identity surfaceA user added to Rootly automatically has gateway access; removal revokes both in lockstep.
Per-team cost attributionLiteLLM’s spend tracking groups by team_id from the UserInfo response — same team_id Rootly already scopes incidents and on-call schedules to.
Central revocationRevoke a Rootly OAuth token via POST /oauth/revoke and the gateway stops honoring it immediately.
No per-gateway credential sprawlThe gateway never holds long-lived API keys for individual users; it only validates short-lived OAuth tokens at request time.
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.