Admin API & CLI
Everything you can do in the admin console — create realms, register clients, configure federation —
you can also do programmatically over the Admin API, authenticated with a bearer token. The API is
hosted at https://api.self.semauth.cc (self is the reserved system-organization slug).
How access works
Admin API tokens are minted by your own organization's admin realm and carry an
org_id claim. The API verifies the token against that realm's keys and scopes every query to your org —
so a token for org A can never touch org B's data. Two ways to get a token:
- Machine-to-machine — an Admin API client using
client_credentials(for backends, scripts, CI). - Human-from-terminal — the
semauth-clipublic client using authorization-code + PKCE (audit attributes to the person).
Every token must carry the scope management:full; the audience is the Admin API URL.
The APIs catalog
The admin console's APIs page (/apis) lists the resource servers your clients can
request tokens for — the platform Admin API (semauth-admin-api) plus any APIs your org owns. It's the
source of truth for valid audience values when registering a client.
Creating a machine-to-machine client
In the admin console open API Access (/api-access) and click
New Admin API client:
- Label — e.g. prod-backend or ci-pipeline.
- Auth method —
- Client secret (default): SemAuth generates a secret (starting with
sa_cs_) and shows it once. Simplest. - private_key_jwt: you host a JWKS URL and sign assertions locally; SemAuth never holds your private key.
- Client secret (default): SemAuth generates a secret (starting with
The confirmation screen shows the Client ID (sa_mgmt_…), the secret (once), the
token endpoint, the Admin API base URL, and a ready-to-run curl example. Grant
type, scope (management:full), and audience are fixed for you. From API Access you can
also Rotate secret (client_secret clients) — the previous secret stops working immediately — or
Revoke a client.
Getting a token and calling the API
# Mint a token from your admin realm's token endpoint (shown on the confirmation page):
TOKEN=$(curl -sS -X POST "$TOKEN_URL" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
-d audience="$ADMIN_API_URL" \
-d scope=management:full | jq -r .access_token)
# Use it against the Admin API:
curl -sS "$ADMIN_API_URL/v1/realms" -H "Authorization: Bearer $TOKEN"
The CLI client (human from a terminal)
Every admin realm also has a built-in public client semauth-cli for interactive use. It uses
authorization-code + PKCE with loopback redirects (RFC 8252): a local tool opens your browser, you sign in with your
normal realm methods (magic link / passkey / federation), approve the tool on a consent screen that names the exact
local callback it will receive the code on — asked on every run, so a signed-in browser alone never hands a code to
something listening on your machine — and the tool receives a user-scoped token — no secret on disk, and audit logs
attribute actions to you rather than a service account. The callback must be http://localhost/callback
or http://127.0.0.1/callback on any port. Scopes include
openid email profile offline_access management:full; the refresh token rotates and keeps minting tokens
for the Admin API audience.
Endpoints (v1)
All requests require Authorization: Bearer <token>. Every mutation is recorded as an event and
shows up in your Activity feed, exactly like the equivalent console action.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/realms | Create a realm (body { "name": "…" }). Returns issuer_id, discovery URL, and defaults. |
| GET | /v1/realms | List your org's realms. |
| GET | /v1/realms/{issuer_id} | Fetch one realm. |
| PATCH | /v1/realms/{issuer_id} | Update name, enabled, magic_link_enabled, passkey_enabled, min_aal (1–3), and custom_domain (with CNAME verification; null to clear). |
| POST | /v1/realms/{issuer_id}/clients | Create an OIDC client (application, label, auth_method, grant types, scopes, audiences, redirect URIs). Returns the secret once for client_secret clients. |
| POST | /v1/realms/{issuer_id}/external-idps | Configure a federation provider. You supply all endpoints (no server-side discovery). |
| POST | /v1/realms/{issuer_id}/users/{user_id}/erasure | Erase a user. See below. |
| POST | /v1/realms/{issuer_id}/erasures | Erase an email address (body { "email": "…" }), whether or not it has an account. |
| POST | /v1/realms/{issuer_id}/erasure | Erase a disabled realm. |
Erasure
The three erasure endpoints answer data-subject deletion requests from your own tooling. They do exactly what the console's Erase actions do, and they are permanent: the person's account, sign-in methods and sessions are removed and their personal data in the audit history becomes unreadable. None takes a request body except erasure by address.
# Erase a user
curl -sS -X POST "$ADMIN_API_URL/v1/realms/$ISSUER_ID/users/$USER_ID/erasure" \
-H "Authorization: Bearer $TOKEN"
{ "erased_at": "2026-10-24T09:12:03Z", "user_id": "4f1c…", "events": 37 }
A person's receipt carries erased_at, user_id (null when the address had
no account) and events, the number of audit records that mentioned them. A realm's receipt carries
erased_at and users, the number of accounts removed. Erasing a realm that is still enabled
is refused — disable it first with PATCH /v1/realms/{issuer_id} — and so is erasing the admin realm
your organization signs in to the console with.
Example: create a realm
curl -sS -X POST "$ADMIN_API_URL/v1/realms" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Production"}'
# 201 Created
{
"issuer_id": "vctxj6g1egkv",
"name": "Production",
"enabled": true,
"discovery_url": "https://vctxj6g1egkv.semauth.app/.well-known/openid-configuration",
"magic_link_enabled": true,
"passkey_enabled": true,
"min_aal": 1
}
Error responses
The API uses RFC 6749-style JSON errors. Common cases: 401 with invalid_token (missing
/ invalid / wrong-issuer token), 403 with insufficient_scope (token lacks
management:full), and 400 for validation failures (e.g. an unreachable min_aal
or an unverifiable custom domain).