Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Admin API (OpenAPI)

Everything the admin console does, it does through the admin API under /admin, and the same API is there for your own automation. This page describes how the API is organised and the conventions every operation shares. The operation-by-operation reference is rendered from the OpenAPI document:

  • Rendered reference: every path, parameter, request body, response schema and required permission.
  • openapi.json: the raw OpenAPI 3.1 document, the same one the server serves at GET /openapi.json.

The document also covers the self-service account API (/t/{slug}/account/..., tag account), which follows the same conventions but authenticates differently; see HTTP endpoints.

Where the document comes from

The document is derived from the admin routers themselves with utoipa (api/src/openapi.rs), so an undocumented route fails the build, and a test keeps the committed api/openapi.json equal to what the binary produces. Three ways to get it:

curl -s https://id.example.com/openapi.json          # from a running server, no auth
cargo run -p ridm-api -- openapi > openapi.json      # from source, no database needed
docker run --rm <image> openapi > openapi.json       # from the container image

info.version is the server’s crate version (0.4.1 today). Swagger UI is served at /docs when DOCS_ENABLED=true.

Organisation

Operations are grouped by tag. Operation ids are the handler names prefixed with their tag (users_list, clients_create), unique across the document as client generators require.

TagCovers
auththe caller’s identity (GET /admin/me) and the permission catalogue with the built-in roles (GET /admin/permissions)
tenantstenants and their settings, CAPTCHA provider, profile schema, statistics
tenant_configconfiguration as code: GET .../export, POST .../import
clientsOAuth / OIDC clients, secrets, service accounts, registration access tokens, and the initial access tokens of dynamic registration (/admin/tenants/{slug}/dcr/initial-access-tokens)
usersusers and everything attached to them: password, sessions, credentials, trusted devices, roles, groups, consents, linked identities, personal access tokens, bulk import and export
groupsgroups, membership and group roles
organizationsorganizations, their members, domains, invitations and the roles granted inside them
rolesroles, composites, permission grants and holders
resource_serversresource servers (audiences) and their permissions
scopesOAuth scopes
mappersclaim mappers
keyssigning keys, and master-key rotation
invitationsinvitations
messagingemail and SMS delivery settings, templates, the delivery log
auditthe audit log, export and chain verification
webhookswebhooks and their deliveries
scimSCIM provisioning tokens
ip_rulesIP allow and deny rules
identity_providersupstream OpenID Connect and OAuth 2.0 providers
accountthe self-service account API

Almost everything is tenant-scoped under /admin/tenants/{slug}/.... The exceptions are /admin/me, /admin/permissions, /admin/tenants (list and create), /admin/master-key and the global audit chain at /admin/audit.

Authentication

Every admin operation takes a token in the Authorization header only (never a query or form parameter), with one exception, the download tickets below:

  • An access token whose aud includes urn:ridm:admin: a JWT (typ: at+jwt), or an opaque at_... token of a client registered with access_token_format: opaque. The token endpoint grants that audience only to clients that list it in allowed_audiences explicitly; it is never implied, even for clients whose audiences are otherwise unrestricted. The subject must be an active user, or the service-account user of a machine client. When the token was issued in a browser session, the session must still be alive, so signing out ends admin access before the token expires. A DPoP-bound token must be presented as Authorization: DPoP with a valid proof.
  • A personal access token (rpat_...) minted by the user in the account console, carrying admin permissions as its scopes. Its reach is the intersection of those scopes and what the user still holds.

The token may come from any tenant. A token issued by master is global: it may act on every tenant. A token from any other tenant reaches only that tenant; a request for another tenant is 403. Tenant lifecycle operations (creating and deleting tenants, master-key rotation, the global audit chain) need a global token.

Permissions are resolved from the user’s effective roles on every request (cached, and evicted whenever a role, group or grant changes), so revoking a role takes effect at once regardless of the permissions claim in the token. See Administrator access.

A token that names an organization (the org_id claim, put there by the sign-in) also carries the roles granted inside it. Those count only on the routes of that organization, under /admin/tenants/{slug}/organizations/{org}/...; everywhere else the caller is judged by their tenant-wide grants alone. See Organization administrators.

Download tickets

The exports (GET /admin/tenants/{slug}/export, .../users/export, .../audit/export and /admin/audit/export) can be larger than a browser should hold in memory, and a download made with fetch (the only way to send the header) must hold it. POST /admin/download-tickets with {"path": "<export path and query>"} answers 201 with {"url", "expires_in": 60}: the export’s URL with a download_ticket parameter that authenticates exactly one GET of exactly that path and query, within 60 seconds, as the caller. The browser’s download manager then fetches it and streams it to disk. The console downloads every export this way.

  • Only the export routes take a ticket; anything else, a different query, a second use or a method other than GET is 401, and a ticket presented anywhere is spent.
  • The caller must hold what the export requires (403 otherwise, when asking); redeeming runs the whole authentication again with the caller’s token, so a session signed out or a role revoked meanwhile ends it.
  • Tickets are 256 random bits, kept in Valkey only by their hash, and the answer carrying one is no-store. A DPoP-bound token’s ticket is redeemed without a proof (a navigation cannot make one); the request for the ticket was checked for it.

Permissions

Each operation requires one permission named ridm:<resource>:<action>; the rendered reference and GET /admin/permissions list which. The catalogue:

ResourcePermissions
tenantsridm:tenants:read, write, create, delete, export, import
usersridm:users:read, write
invitationsridm:invitations:read, write (also bulk user import)
groupsridm:groups:read, write
rolesridm:roles:read, write
clientsridm:clients:read, write
scopesridm:scopes:read, write
claim mappersridm:mappers:read, write
resource serversridm:resource-servers:read, write
identity providersridm:idps:read, write
keysridm:keys:read, write
messagingridm:messaging:read, write
auditridm:audit:read
webhooksridm:webhooks:read, write
SCIM tokensridm:scim:read, write

IP rules and the CAPTCHA provider fall under ridm:tenants:read / write. Custom roles may be granted wildcards (ridm:users:*, ridm:*). Every tenant is seeded with five immutable built-in roles:

RoleGrants
ridm:ownerevery permission
ridm:admineverything except ridm:tenants:create, ridm:tenants:delete, ridm:tenants:import
ridm:user-managerusers, invitations, groups and SCIM tokens; read tenant settings, roles and audit
ridm:client-managerclients, scopes, claim mappers, resource servers; read tenant settings, roles and audit
ridm:viewerevery *:read permission

An administrator can never grant a permission they do not hold themselves, whether through a role assignment, a composite, a group membership or an invitation.

Request and response conventions

ConventionDetail
Bodiesapplication/json, except user import, which also takes text/csv. Create and update bodies reject unknown fields, so a typo fails loudly instead of being ignored.
IdentifiersPath parameters take the resource’s UUID; clients also accept the public client_id, identity providers their alias.
CreatePOST to the collection, usually 201 with the created resource. A generated secret (client secret, webhook secret, SCIM token, DCR initial access token, temporary password) appears in that response only.
UpdatePATCH with the changed fields only. For tenant settings and client metadata the body is a JSON merge patch (RFC 7396): absent means unchanged, null clears a field or resets it to its default.
Replace, linkPUT on a sub-resource (.../roles/{role_id}, .../members/{user_id}) creates the link; DELETE removes it. Whole documents such as the profile schema and messaging settings are replaced with PUT.
DeleteDELETE, usually 204. Users are soft-deleted and purged after the tenant’s retention.
Secrets on readNever returned. Reads report secret_set, password_set or client_secret_set instead; omitting a secret on update keeps the stored one.
CachingResponses that carry secrets or exports are Cache-Control: no-store.

Pagination and filtering

Large collections (tenants, users, clients, invitations, the audit log) use keyset pagination:

GET /admin/tenants/acme/users?limit=100&cursor=eyJ0IjoiMjAyNi0wOS0xOFQwOTowMDowMFoiLCJpIjoi...
{
  "items": [ ... ],
  "next_cursor": "eyJ0IjoiMjAyNi0wOS0xOFQwOTowMToxMloiLCJpIjoi..."
}
ParameterMeaning
limitpage size, default 50, clamped to 1–500
cursorthe previous page’s next_cursor, passed back unchanged. It is opaque; a malformed one is 400

next_cursor is absent on the last page. Pages are stable while rows are inserted, because the cursor encodes the position (creation time and id) rather than an offset. Smaller collections (groups, roles, resource servers, scopes, claim mappers, keys, webhooks, IP rules, identity providers) return every item as one JSON array. The webhook delivery log and the message log take ?status= and ?limit= (default 50, at most 500) and return the most recent entries.

Filters are query parameters on the collection:

CollectionFilters
userssearch (username or email prefix), status, org_id, include_deleted
clientssearch (prefix of client_id or name)
invitationsopen_only
rolesclient_id, realm_only
claim-mappers, ip-rulesclient_id, tenant_wide
keysstatus (pending, active, retiring, revoked)
auditfrom, to, name (exact, or a prefix when it ends in . or *), actor_id, subject_id, user_id
statsdays (1–365, default 30)
users/export, audit/exportformat (json or csv)

Errors

Errors are RFC 9457 problem documents (application/problem+json) with a stable type URN such as urn:ridm:error:forbidden; see Errors. A missing or invalid token is 401 with WWW-Authenticate: Bearer realm="ridm-admin"; a valid token without the needed permission is 403 with detail naming it. The admin API is not rate limited by the request guard.

Example

A machine client for automation: create it with the client_credentials grant and urn:ridm:admin in allowed_audiences, give it a service account (PUT /admin/tenants/{slug}/clients/{client}/service-account), and assign that account a role such as ridm:user-manager. Then:

ISSUER=https://id.example.com/t/acme

TOKEN=$(curl -s "$ISSUER/token" \
  -u "provisioner:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=urn:ridm:admin | jq -r .access_token)

curl -s "https://id.example.com/admin/tenants/acme/users?search=alice&limit=20" \
  -H "Authorization: Bearer $TOKEN" | jq '.items[] | {id, username, email}'

For scripts run by a person, a personal access token is simpler: mint one in the account console with the admin permissions it needs and send it the same way (Authorization: Bearer rpat_...). The ridm command line wraps the common operations and accepts either kind of token.

Generating a client

Any OpenAPI 3.1 generator works against the document. The bundled UI generates its TypeScript types with openapi-typescript and calls the API through openapi-fetch:

cargo run -p ridm-api -- openapi > api/openapi.json
cd ui && npm run gen:api          # writes lib/api/openapi.d.ts

For another language, point your generator at the same file, for example:

npx @openapitools/openapi-generator-cli generate \
  -i api/openapi.json -g python -o ridm-admin-client

The document declares one security scheme, bearer (HTTP bearer, JWT format), and every operation requires it. A personal access token is sent under the same scheme.