Skip to content

Powered by Grav + Helios

Users

Users

Endpoints for managing user accounts including listing, creating, updating, and deleting users.

Endpoints for managing user accounts including listing, creating, updating, and deleting users.

List Users

GET /users
List all user accounts.

Parameters

Name Type Description
page optional integer Page number for pagination (default: 1)
per_page optional integer Number of results per page (default: 20, max: 100)
search optional string Filter by a free-text query across username, email, and fullname.
access optional string Filter to users with effective access to a permission (e.g. `admin.login`, `api.super`). `permission` is accepted as an alias.
group optional string Filter to members of a single account group.
filter optional string Active Users-tab id (see `GET /users/filters`). Fires `onApiUserListFilter` so a plugin can narrow the listing before pagination. Omit or use `all` for the unfiltered list.

Response Codes

200 Success
401 Unauthorized
403 Forbidden

List User Tabs

GET /users/filters
List the filter tabs available for the Users list, along with the landing-view policy. Returns the built-in "All Users" tab plus any tabs contributed by plugins through the `onApiUserListFilters` event, gated by the caller's permissions. Selecting a tab adds `?filter=` to `GET /users`, which fires `onApiUserListFilter` so the owning plugin can narrow the listing before pagination.
JSON
{
    "data": {
        "tabs": [
            { "id": "all", "plugin": "api", "label": "All Users" },
            { "id": "active", "plugin": "my-plugin", "label": "Active", "icon": "fa-bolt", "priority": 10 }
        ],
        "defaultFilter": "all",
        "showAll": true
    }
}

Response Codes

200 Success
401 Unauthorized
403 Forbidden — requires `api.users.read`

The response data is an object with three keys:

  • tabs — the ordered tab row. Each tab carries an id (sent back as ?filter=<id>; all is reserved for the built-in tab), a plugin slug, and a plain-text label. Tabs may also include icon, priority (higher sorts earlier), and badge / badgeEndpoint for a live count.
  • defaultFilter — the tab the client should open on when no filter is present in the URL. Defaults to all; a plugin can nominate another tab via $event['defaultFilter'] (it is honoured only when it maps to a tab the caller can see, otherwise it falls back to the first tab).
  • showAll — whether the built-in "All Users" tab is present. It is true by default; a plugin can set $event['showAll'] = false to drop it when showing every account isn't a useful (or safe) landing view. "All Users" is only dropped once the plugin has contributed at least one authorized tab, so the row is never empty.

A plugin filter can only narrow the listing: search, the caller's permission scope, and pagination are all applied by the core endpoint after the plugin's filter runs, so a tab can never widen visibility or change the response envelope. See the API Events page for the onApiUserListFilters and onApiUserListFilter contracts.

List User Columns

GET /users/columns
List the extra columns plugins have added to the Users list through the `onApiUserListColumns` event, gated by the caller's permissions. Each column declares how a plugin-owned value is labelled and formatted; the values themselves ride along inside each user's `extra` map on `GET /users`, populated by `onApiUserListColumnData` for the current page only.
JSON
{
    "data": {
        "columns": [
            {
                "id": "my-plugin-valid-till",
                "plugin": "my-plugin",
                "label": "Valid until",
                "field": "subscription.valid_till",
                "formatter": "datetime",
                "sortable": false,
                "priority": 50
            }
        ]
    }
}

Response Codes

200 Success
401 Unauthorized
403 Forbidden — requires `api.users.read`

The response data.columns is the ordered list of plugin-declared columns. Each column carries:

  • id — unique column id.
  • plugin — the owning plugin slug.
  • label — the plain-text column header.
  • field — the key to read from each user's extra map on GET /users.
  • formatter — one of text, link, date, datetime, boolean, number, badge. The server validates this against the whitelist and falls back to text for anything else; the client renders it, so no markup or renderer crosses the wire.
  • sortable — optional; whether the column offers a client-side sort of the current page.
  • priority — optional; higher sorts earlier.

Columns are plugin-owned but safe by construction: the values in each user's extra map are scalars only, an authorize check is re-run server-side (and stripped from this response), and the data event is isolated so a misbehaving plugin degrades to missing values rather than breaking the listing. Column values are resolved only for the accounts on the current page — never for every account. Like the filter tabs, columns require the Flex-accounts backend.

See the API Events page for the onApiUserListColumns and onApiUserListColumnData contracts.

Get User

GET /users/{username}
Get user details.

Parameters

Name Type Description
username required string The username to retrieve

Response Codes

200 Success
401 Unauthorized
404 User not found

Create User

POST /users
Create a new user account.

Parameters

Name Type Description
username required string Unique username
password required string User password
email required string User email address
fullname optional string Full display name
state optional string Account state (e.g. enabled, disabled)
access optional object Permission access object
JSON
{"username": "editor", "password": "SecurePass123!", "email": "[email protected]", "fullname": "Jane Editor"}

Response Codes

201 User created
401 Unauthorized
409 Username already exists
422 Validation error

Update User

PATCH /users/{username}
Update a user account.

Parameters

Name Type Description
username required string The username to update (path parameter)
email optional string Updated email address
fullname optional string Updated full display name
state optional string Updated account state
access optional object Updated permission access object
password optional string New password

Response Codes

200 User updated
401 Unauthorized
404 User not found
422 Validation error

Delete User

DELETE /users/{username}
Delete a user account.

Parameters

Name Type Description
username required string The username to delete

Response Codes

204 User deleted
401 Unauthorized
403 Forbidden
404 User not found

Upload Avatar

POST /users/{username}/avatar
Upload a custom avatar image for a user. Send as multipart/form-data.

Parameters

Name Type Description
username required string The username to upload the avatar for
avatar required file Image file (JPEG, PNG, GIF, or WebP)
JSON
{"data": {"username": "admin", "email": "[email protected]", "avatar": {"name": "admin-a1b2c3d4.jpg", "type": "image/jpeg"}}}

Response Codes

201 Avatar uploaded
401 Unauthorized
403 Forbidden
404 User not found
422 Validation error (no file or invalid image type)

Delete Avatar

DELETE /users/{username}/avatar
Remove the custom avatar for a user.

Parameters

Name Type Description
username required string The username to remove the avatar for

Response Codes

200 Avatar removed
401 Unauthorized
403 Forbidden
404 User not found

Generate 2FA Secret

POST /users/{username}/2fa
Generate or regenerate a TOTP two-factor authentication secret and return a QR code data URI.

Parameters

Name Type Description
username required string The username to generate 2FA for
JSON
{"data": {"secret": "JBSW Y3DP EHPK 3PXP", "qr_code": "data:image/png;base64,..."}}

Response Codes

200 Secret generated
401 Unauthorized
403 Forbidden
404 User not found
500 Login plugin with 2FA support not installed

List API Keys

GET /users/{username}/api-keys
List all API keys for a user. Key secrets are not returned.

Parameters

Name Type Description
username required string The username to list API keys for
JSON
{"data": [{"id": "abc123", "name": "CI Deploy Key", "scopes": [], "created": 1710000000}]}

Response Codes

200 Success
401 Unauthorized
403 Forbidden
404 User not found

Create API Key

POST /users/{username}/api-keys
Generate a new API key for a user. The raw key is returned only once in the response.

Parameters

Name Type Description
username required string The username to create the API key for
name optional string A descriptive name for the API key
scopes optional array List of permission scopes for the key
expiry_days optional integer Number of days until the key expires. Omit or set to null for no expiry.
JSON
{"name": "CI Deploy Key", "scopes": [], "expiry_days": 90}
JSON
{"data": {"id": "abc123", "name": "CI Deploy Key", "api_key": "grav_...", "scopes": [], "created": 1710000000}}

Response Codes

201 API key created
401 Unauthorized
403 Forbidden
404 User not found

Revoke API Key

DELETE /users/{username}/api-keys/{keyId}
Revoke and delete an API key.

Parameters

Name Type Description
username required string The username who owns the API key
keyId required string The ID of the API key to revoke

Response Codes

204 API key revoked
401 Unauthorized
403 Forbidden
404 API key not found

Enable 2FA

POST /users/{username}/2fa/enable
Verify a TOTP code against the previously generated secret and mark 2FA as enabled on the account. Only the account owner can enable 2FA on their own account (not admins). Fires `onApiUser2faEnabled`.

Parameters

Name Type Description
username required string Username (path param).
code required string Current 6-digit TOTP code from the authenticator app.
JSON
{"code": "123456"}
JSON
{"data": {"twofa_enabled": true}}

Response Codes

200 2FA enabled.
400 Missing code, invalid code, or no 2FA secret generated yet (call `POST /users/{username}/2fa` first).
401 Unauthorized.
403 Only the account owner can enable 2FA.
404 User not found.
500 Login plugin with 2FA support not installed.

Disable 2FA

POST /users/{username}/2fa/disable
Disable 2FA on an account. Self-disable requires a valid current TOTP code — this stops a stolen session from unilaterally removing 2FA. Admins with `api.users.write` (or superadmin) can force-disable without a code, for lost-device recovery. Both paths clear the `twofa_secret`. Fires `onApiUser2faDisabled` with `forced_by_admin` flag.

Parameters

Name Type Description
username required string Username (path param).
code optional string Current TOTP code. Required for self-disable; ignored for admin-forced disable.
JSON
{"code": "123456"}
JSON
{"data": {"twofa_enabled": false}}

Response Codes

200 2FA disabled.
400 Missing/invalid code (self-disable path only).
401 Unauthorized.
403 Caller is neither the account owner nor holds `api.users.write`.
404 User not found.
500 Login plugin with 2FA support not installed.

List User Row Actions

GET /users/row-actions
List the per-user action buttons plugins have added to the Users list through the `onApiUserListRowActions` event, filtered by the caller's permissions. Each action is a formatter-free descriptor Admin2 renders in a user row's Actions cell; invoking one calls `POST /users/{username}/row-action`.
JSON
{
    "data": {
        "actions": [
            {
                "id": "impersonate-user",
                "plugin": "impersonate",
                "label": "Impersonate",
                "icon": "fa-user-secret",
                "action": "start",
                "confirm": "Impersonate this user?",
                "priority": 80
            }
        ]
    }
}

Response Codes

200 Success
401 Unauthorized
403 Forbidden — requires `api.users.read`

The response data.actions is the ordered list of plugin-declared row actions. Each action carries:

  • id — unique action id; the key posted to the execution endpoint.
  • plugin — the owning plugin slug.
  • label — the plain-text button label (also its accessible name and tooltip).
  • icon — optional Font Awesome icon class.
  • action — optional verb passed back to the handler; opaque to the client.
  • confirm — optional prompt shown in a confirmation dialog before the action runs.
  • priority — optional; higher sorts earlier.

Actions are plugin-owned but safe by construction: no markup or renderer crosses the wire, the list is capped, and the authorize check is re-run server-side and stripped from this response. The authorize on a declaration gates only which buttons render — it is not a security boundary; the execution endpoint re-authorizes independently. Like the filter tabs and columns, row actions require the Flex-accounts backend.

See the API Events page for the onApiUserListRowActions and onApiUserListRowAction contracts.

Execute User Row Action

POST /users/{username}/row-action
Run a plugin-declared Users-list row action against one account. The API re-checks the declared action's authorize against the caller, then fires `onApiUserListRowAction` with the target username; the plugin handler re-authorizes against that target and returns a result. Any redirect URL in the result is validated to a safe same-origin target before it reaches the client.

Parameters

Name Type Description
username required The target account the action runs against.
JSON
{
    "id": "impersonate-user"
}
JSON
{
    "data": {
        "status": "success",
        "message": "Impersonating alice",
        "url": "/admin/impersonate/alice"
    }
}

Response Codes

200 Action executed; body carries the handler result.
401 Unauthorized
403 Forbidden — caller lacks `api.users.read`, or the handler rejected this target
404 Unknown username, or an action id the caller isn't authorized for

Post the id of a declared row action. The API loads the target account, confirms the caller may list users (api.users.read), and re-checks the action's own authorize — an unknown or unauthorized id is an indistinguishable 404, so nothing leaks. It then fires onApiUserListRowAction with the resolved username; the handler must guard on $event['plugin'], re-authorize against the target, and set $event['result'].

The result is normalized to a fixed shape:

  • statussuccess or error; drives whether Admin2 shows a success or error toast.
  • message — optional text shown as a toast (length-capped).
  • url — optional redirect. Only a root-relative path (not the protocol-relative //host form) or a same-origin absolute URL survives validation; a javascript:/data: scheme or a cross-origin URL is dropped. Admin2 opens the surviving URL in a new tab with noopener.

A handler that throws degrades to an error toast rather than breaking the Users list; a thrown ForbiddenException propagates as a 403.

See the API Events page for the onApiUserListRowAction contract.