Skip to main content

OAuth & Authentication

This guide covers how third-party apps obtain and refresh API credentials for the Roam API. For which endpoints each credential type can call, see Access Models. For which scopes to request, see the Scopes catalog.

Choose a credential type

CredentialWhen to useActs as
API KeyServer-to-server bots you install in one Roam (admin-issued)Organization bot identity for that Roam
OAuth appMulti-tenant / third-party product users installOrganization or Personal, chosen at consent
Personal Access Token (PAT)Local scripts, MCP clients, single-user toolsThe authorizing user (Personal only)

API Keys and PATs are created in product UI and used as Bearer tokens immediately. OAuth apps need the authorization-code flow below.

client_credentials is not supported. Every API call binds a user or a Roam install; server-to-server callers use API Keys.

Create an OAuth app

  1. Open Roam Administration → Developer (gear icon → Settings).
  2. Click Add ApiClient (or equivalent “create app”).
  3. Choose OAuth as the authorization type.
  4. Set a name, description, and redirect URIs (exact-match list — no wildcards; http://localhost… is allowed for local development if you register it).
  5. Select the scopes your app needs.
  6. Save. Copy the Client ID and Client Secret (confidential clients).

Redirect URIs must match the redirect_uri you send on authorize exactly (scheme, host, path, and query). Roam also allows a first-party dev callback used by the in-product token inspector; do not rely on that for your app.

Authorization code flow

Endpoints

StepURL
Authorize (browser)https://ro.am/oauth/authorize
Token (server)https://ro.am/oauth/token
Authorization-server metadatahttps://ro.am/.well-known/oauth-authorization-server

MCP / Dynamic Client Registration uses additional discovery documents; see MCP for DCR and PKCE assistant setup. Third-party web apps normally use a pre-registered confidential client.

1. Send the user to authorize

GET https://ro.am/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour.app%2Foauth%2Fcallback
&scope=chat%3Ahistory%20chat%3Asend_message%20user%3Aread
&state=opaque-csrf-value
ParameterRequiredNotes
response_typeyesMust be code
client_idyesFrom Developer settings
redirect_uriyesMust be an exact registered URI
scoperecommendedSpace-separated scope list; omit only if you want the app's default grant set
statestrongly recommendedCSRF token; echo-checked on callback
code_challenge / code_challenge_methodpublic clientsPKCE S256 required for public clients (token_endpoint_auth_method: none). Confidential clients may use PKCE optionally. plain is rejected.

The user signs in, picks the Roam / access model (Organization vs Personal when both are allowed), and consents. Roam redirects to:

https://your.app/oauth/callback?code=AUTH_CODE&state=opaque-csrf-value

2. Exchange the code for tokens

POST https://ro.am/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https%3A%2F%2Fyour.app%2Foauth%2Fcallback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Confidential clients may also send the secret via HTTP Basic (client_secret_basic) instead of the form body (client_secret_post).

Public clients omit client_secret and must include:

&code_verifier=THE_PKCE_VERIFIER

3. Token response

Successful responses look like:

{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 604800,
"refresh_token": "…",
"scope": "chat:history chat:send_message user:read"
}
FieldMeaning
access_tokenSend as Authorization: Bearer … on https://api.ro.am/v1/…
expires_inLifetime of the access token in seconds (604800 = 7 days)
refresh_tokenLong-lived credential used only at the token endpoint (365 days)
scopeGranted scopes (space-separated)

Store the refresh token securely. Access tokens alone will expire after one week.

Refresh tokens

POST https://ro.am/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=YOUR_REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Refresh cannot escalate scopes. Requesting additional scopes on refresh is rejected; the new access token keeps the prior grant (or a subset if you narrow). A successful refresh returns a new access_token and typically a rotated refresh_token — discard the old refresh token when a new one is returned.

Call the API

curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://api.ro.am/v1/token.info

Use /token.info to inspect the grant (client, principal, scopes). Revoke with /token.revoke (access and refresh for that grant). Apps can also subscribe to lifecycle webhooks token.revoked and app.uninstalled.

Auth failure codes

A 401 carries a machine-readable code: not_authed, invalid_token, or token_revoked. For an OAuth app the distinction that matters is that invalid_token is worth a refresh, while token_revoked is terminal — the grant is gone, so discard both tokens and send the user back through authorization instead of retrying.

Full table: Authentication failures. Complete catalog: Error Codes.

Personal Access Tokens

PATs are created under User Settings → Developer. They always use the Personal access model and expand from PAT scope groups (e.g. pat:chat:write) into concrete OAuth scopes — see Scopes → PAT groups.

PATs are accepted only on endpoints in the personal-access allowlist. Each endpoint page's Access line documents whether Personal is supported.

Webhook signing secret

OAuth apps and API keys receive a per-app Standard Webhooks signing secret (shown in Developer settings). Use it to verify delivery signatures — see the Webhooks overview. The secret is not the same as the OAuth client secret.

Next steps