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.
List users
Section titled “List users”GET /api/v1/usersReturns a list of UserPublic:
[ { "id": "...", "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 user
Section titled “Get user”GET /api/v1/users/{user_id}Create user
Section titled “Create user”POST /api/v1/usersCSRF-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.
Update user
Section titled “Update user”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.
Reset password
Section titled “Reset password”POST /api/v1/users/{user_id}/passwordCSRF-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 user
Section titled “Delete user”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.
Admin: system + settings
Section titled “Admin: system + settings”These routes back the dashboard's Settings, Admin pages. Caller must be is_admin=true.
Effective settings snapshot
Section titled “Effective settings snapshot”GET /api/v1/admin/settingsReturns 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.
Versions snapshot
Section titled “Versions snapshot”GET /api/v1/admin/system/versionsReturns 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.
Check for updates
Section titled “Check for updates”POST /api/v1/admin/system/versions/checkFetches 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.
First-boot setup
Section titled “First-boot setup”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 tokenThe 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.