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
/users
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
List User Tabs
/users/filters
{
"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
The response data is an object with three keys:
tabs— the ordered tab row. Each tab carries anid(sent back as?filter=<id>;allis reserved for the built-in tab), apluginslug, and a plain-textlabel. Tabs may also includeicon,priority(higher sorts earlier), andbadge/badgeEndpointfor a live count.defaultFilter— the tab the client should open on when nofilteris present in the URL. Defaults toall; 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 istrueby default; a plugin can set$event['showAll'] = falseto 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
/users/columns
{
"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
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'sextramap onGET /users.formatter— one oftext,link,date,datetime,boolean,number,badge. The server validates this against the whitelist and falls back totextfor 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
/users/{username}
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | The username to retrieve |
Response Codes
Create User
/users
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 |
{"username": "editor", "password": "SecurePass123!", "email": "[email protected]", "fullname": "Jane Editor"}
Response Codes
Update User
/users/{username}
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
Delete User
/users/{username}
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | The username to delete |
Response Codes
Upload Avatar
/users/{username}/avatar
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | The username to upload the avatar for |
| avatar required | file | Image file (JPEG, PNG, GIF, or WebP) |
{"data": {"username": "admin", "email": "[email protected]", "avatar": {"name": "admin-a1b2c3d4.jpg", "type": "image/jpeg"}}}
Response Codes
Delete Avatar
/users/{username}/avatar
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | The username to remove the avatar for |
Response Codes
Generate 2FA Secret
/users/{username}/2fa
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | The username to generate 2FA for |
{"data": {"secret": "JBSW Y3DP EHPK 3PXP", "qr_code": "data:image/png;base64,..."}}
Response Codes
List API Keys
/users/{username}/api-keys
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | The username to list API keys for |
{"data": [{"id": "abc123", "name": "CI Deploy Key", "scopes": [], "created": 1710000000}]}
Response Codes
Create API Key
/users/{username}/api-keys
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. |
{"name": "CI Deploy Key", "scopes": [], "expiry_days": 90}
{"data": {"id": "abc123", "name": "CI Deploy Key", "api_key": "grav_...", "scopes": [], "created": 1710000000}}
Response Codes
Revoke API Key
/users/{username}/api-keys/{keyId}
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
Enable 2FA
/users/{username}/2fa/enable
Parameters
| Name | Type | Description |
|---|---|---|
| username required | string | Username (path param). |
| code required | string | Current 6-digit TOTP code from the authenticator app. |
{"code": "123456"}
{"data": {"twofa_enabled": true}}
Response Codes
Disable 2FA
/users/{username}/2fa/disable
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. |
{"code": "123456"}
{"data": {"twofa_enabled": false}}
Response Codes
List User Row Actions
/users/row-actions
{
"data": {
"actions": [
{
"id": "impersonate-user",
"plugin": "impersonate",
"label": "Impersonate",
"icon": "fa-user-secret",
"action": "start",
"confirm": "Impersonate this user?",
"priority": 80
}
]
}
}
Response Codes
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
/users/{username}/row-action
Parameters
| Name | Type | Description |
|---|---|---|
| username required | The target account the action runs against. |
{
"id": "impersonate-user"
}
{
"data": {
"status": "success",
"message": "Impersonating alice",
"url": "/admin/impersonate/alice"
}
}
Response Codes
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:
status—successorerror; 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//hostform) or a same-origin absolute URL survives validation; ajavascript:/data:scheme or a cross-origin URL is dropped. Admin2 opens the surviving URL in a new tab withnoopener.
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.