API Authentication
The API accepts multiple authentication modes:
| Mode | Who it's for | How the credential is carried |
|---|---|---|
| Session cookie | Browser UI | Cookie: session=… after POST /api/auth/login |
| HTTP Basic | Quick scripts, curl one-liners | Authorization: Basic <base64(user:pass)> |
| OAuth2 Bearer | Automation, CI, third-party apps | Authorization: Bearer <jwt> |
| Client API Token | Companion apps (Argosy, Grout, Playnite, custom scripts) | Authorization: Bearer rmm_<token> |
All of them resolve to the same scope model (see the OAuth scopes in Users & Roles). A request is allowed if the active identity holds all scopes the endpoint requires.
Base URL
When the app is behind a reverse proxy (as it should be when hosted in public), that's your public URL. When running locally without a proxy, the container listens on port 80.
Session login (browsers)
The response sets a romm_session cookie, and subsequent requests from the same browser are authenticated automatically.
Log out:
For OIDC logins, hitting /api/auth/logout also triggers RP-Initiated Logout if your OIDC provider supports it (configured via OIDC_END_SESSION_ENDPOINT).
HTTP Basic
import requests
from requests.auth import HTTPBasicAuth
r = requests.get("https://demo.romm.app/api/roms",
auth=HTTPBasicAuth("alice", "s3cret"))
OAuth2 Bearer token
The RomM backend implements the OAuth2 password grant, where you exchange credentials for a short-lived access token and a refresh token. Access and refresh token expiry times are configurable via OAUTH_ACCESS_TOKEN_EXPIRE_SECONDS and OAUTH_REFRESH_TOKEN_EXPIRE_SECONDS.
POST /api/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=alice&password=s3cret&scope=roms.read%20roms.write
{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"token_type": "bearer",
"expires": 1800,
"refresh_expires": 604800
}
Access tokens are HS256-signed JWTs valid for OAUTH_ACCESS_TOKEN_EXPIRE_SECONDS seconds. Send them as:
Refresh before expiry:
POST /api/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=eyJhbGciOi...
Request only the scopes you need and RomM will issue a token with the intersection of what you asked for and what the user has.
Client API tokens (for companion apps)
For anything long-lived (a running companion app, a cron job, a CI integration), use Client API Tokens instead of OAuth2. They're issued per-user from Administration → Client API Tokens, carry a subset of the user's scopes, and don't expire unless you set an expiry.
Token format: rmm_ + 64 hex chars. Use it as a bearer:
Each user gets up to 25 active tokens. Tokens can be paired with a device via the pairing flow, which saves typing a long token on a handheld.
OIDC
Users signing in through an OIDC provider get a regular RomM session, same as username/password login. For the API side this means you can't use an OIDC access token directly. Authenticate the user through the browser first (they'll be redirected to the OIDC provider, then back to RomM), then use the resulting session cookie, or mint a Client API Token for programmatic use.
OIDC provider setup lives in Administration → OIDC.
Browser clients on another origin (CORS)
A browser app on another domain, port or scheme can only call the API if that origin is listed in ROMM_CORS_ALLOWED_ORIGINS. The list is empty by default, which denies every cross-origin request:
A listed origin gets Access-Control-Allow-Credentials: true, so the browser can send the session cookie with credentials: "include". A * entry answers any origin but turns that header off for every origin, listed ones included, so it only suits requests that carry no cookie, such as ones authenticated with a bearer token in the Authorization header. Native apps, scripts and server-side code don't send an Origin header, so CORS doesn't apply to them.
Which scopes do I need?
Every endpoint in the API Reference lists its required scopes. The short version:
- Read-ish endpoints want the matching
*.readscope. - Write-ish endpoints want
*.write. - Admin-ish endpoints want
users.read,users.write, ortasks.run.
Errors
| HTTP | Meaning |
|---|---|
401 Unauthorized |
No credential, expired credential, bad credential. |
403 Forbidden |
Authenticated but the identity lacks a required scope. |
404 Not Found |
The resource doesn't exist, or, for privacy, the identity can't see it. |
An unusable credential never fails the request on its own. A malformed Authorization header, a Basic header that doesn't decode, wrong Basic credentials, or an invalid or expired JWT all leave the request unauthenticated, and it then gets the route's usual answer: 401 on a route that needs a login, or the normal response on one that doesn't.
When debugging a 403, check:
- The user's role in Administration → Users.
- The token's scopes (for OAuth2/Client API Tokens). Scopes are narrower than the user's role by default.
- The endpoint's scope requirements in the API Reference.
OpenAPI
The full machine-readable schema is served at /openapi.json. It's the source of truth for generated clients, Postman collections, and the in-docs API Reference.
See Consuming OpenAPI for codegen tips.