Skip to main content

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.

Preview — OAuth 2.0 / OIDC support is currently in beta. Endpoints, scopes, and behavior may change before general availability. To request early access for your team, contact your Customer Success Manager or email support@rootly.com.
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.