Skip to content

Users and admin API

Except for first-boot setup, these endpoints require a user with is_admin=true; an admin:* scope alone does not confer that role. Only GET /users and GET /users/{id} are usable with an API key (with a suitable users/admin scope and a globally-admin owner). User mutations require a fresh MFA browser session, and /admin/settings plus /admin/system/* are not available to API keys. First-boot setup is instead guarded by the one-shot token and an empty users table.

User scopes inside a project are still managed through memberships; this page is for the parallel surface of global user state that lives outside of any single project.

Base path: /api/v1/users. All routes require global admin.

GET /api/v1/users

Returns a list of UserPublic:

[
{
"id": "...",
"email": "[email protected]",
"first_name": "Alice",
"last_name": "Doe",
"display_name": "Alice",
"is_admin": true,
"is_active": true,
"timezone": "UTC",
"created_at": "...",
"updated_at": "...",
"last_login_at": "..."
}
]
GET /api/v1/users/{user_id}
POST /api/v1/users

CSRF-protected.

{
"email": "[email protected]",
"first_name": "Alice",
"last_name": "Doe",
"display_name": "Alice",
"password": "Str0ng-example-pass!",
"is_admin": false,
"timezone": "UTC"
}

Password is at most 256 characters and must satisfy the configured password policy: Z4J_PASSWORD_MIN_LENGTH defaults to 12, at least three of lowercase / uppercase / digit / symbol are required unless the password is 16+ characters, and common passwords are denied. It is hashed with argon2id. A policy failure on this route currently escapes as HTTP 500 rather than a validation envelope. The created user has no project memberships; add them through memberships or an invitation.

PATCH /api/v1/users/{user_id}

CSRF-protected. Any subset of:

{
"first_name": "...",
"last_name": "...",
"display_name": "...",
"is_admin": true,
"is_active": false,
"timezone": "America/New_York"
}

email is not an update field, but unknown fields are ignored: sending an email key returns 200 with the old address rather than rejecting the request. Renaming therefore requires creating a new user and migrating memberships. An admin may not demote or deactivate their own account, nor demote or deactivate the final active is_admin=true user.

POST /api/v1/users/{user_id}/password

CSRF-protected.

{"new_password": "new-password-that-meets-policy"}

Resets the user's password without requiring the old one. The new value must satisfy the same password policy. Every existing session and remembered MFA device is removed immediately. The action writes a user.password.reset audit row attributed to the caller.

DELETE /api/v1/users/{user_id}

CSRF-protected and fresh-MFA/session-only. This is a permanent delete: memberships, sessions, API keys, preferences, and subscriptions cascade away; automation rules created by the user are disabled; and surviving audit rows and commands anonymize their actor reference. It refuses self-deletion and deletion of the final active admin. For a reversible operation that preserves the row and attribution, PATCH {"is_active": false} instead.

These routes back the dashboard's Settings, Admin pages. Caller must be is_admin=true.

GET /api/v1/admin/settings

Returns the resolved Settings dict (post-env-var, post-defaults, post-.env). SecretStr values and recognized secret-shaped field names are masked. Plain URL fields are not guaranteed to be masked: database_url and scheduler_trigger_url, for example, are returned as configured and can contain credentials. Treat the response as sensitive admin-only data, not as a shareable configuration export. The source labels mirror z4j config show on the CLI.

GET /api/v1/admin/system/versions

Returns the brain's installed package versions for z4j and every adapter, plus the dashboard build's commit hash if shipped that way. Used by the dashboard's "About" page.

POST /api/v1/admin/system/versions/check

Fetches the upstream version manifest (Z4J_VERSION_CHECK_URL, default points at the public repo) and returns a list of packages with newer releases available. The check is read-only; nothing is installed. Cached server-side so multiple admins clicking the button don't hammer the upstream URL.

The setup window is active only while the entire users table is empty -- a non-admin row closes it too. Afterwards, POST /api/v1/setup/complete returns 409 conflict with reason="already_initialised"; GET /setup returns 404; and anonymous GET /api/v1/setup/status returns 401 (an authenticated caller gets {"first_boot": false}).

GET /api/v1/setup/status # is setup needed?
POST /api/v1/setup/complete # mint the first admin using a one-shot token

The setup form is also served at GET /setup as HTML for evaluators booting a brain locally.

Token TTL is Z4J_FIRST_BOOT_TOKEN_TTL_SECONDS (default 15 minutes). A hard-coded process-local setup bucket permits only five completion requests per IP per 15 minutes. Z4J_FIRST_BOOT_ATTEMPTS_PER_IP (default 30) is a second, audit-log-backed budget; with defaults the five-request gate wins, so raising 30 does not permit more attempts.

For deployments that prefer to skip the form, set Z4J_BOOTSTRAP_ADMIN_EMAIL and Z4J_BOOTSTRAP_ADMIN_PASSWORD. Accepted values create the admin at boot and keep setup closed. Invalid email or password policy values do not fail startup closed: the brain logs the failure, falls back to minting a setup token, and prints a /setup?token=... URL. Confirm the z4j auto-bootstrap complete startup log message before assuming the form was never exposed. See first-admin.