Skip to content

Powered by Grav + Helios

Users

Users

Endpoints for managing user accounts (listing, creating, updating and deleting users, avatars, API keys and 2FA), user groups, and invitations.

Endpoints for managing user accounts (listing, creating, updating and deleting users, avatars, API keys and 2FA), user groups, and invitations.

List Users

GET /users
List user accounts, sorted by username. A caller with `api.users.read` sees every account; any other caller gets a one-row listing holding only their own account, in the same paginated format.

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: 1000, both set in the plugin's `pagination` config)
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. Works with both the Flex and the plain file account storage. Omit or use `all` for the unfiltered list.
JSON
{
    "data": [
        {
            "username": "editor",
            "email": "[email protected]",
            "fullname": "Jane Editor",
            "title": "",
            "state": "enabled",
            "language": "",
            "content_editor": "",
            "access": { "api": { "access": true, "pages": { "read": true, "write": true } } },
            "groups": [],
            "avatar_url": null,
            "twofa_enabled": false,
            "twofa_secret": false,
            "created": "2026-09-01T10:00:00+00:00",
            "modified": "2026-09-01T10:00:00+00:00"
        }
    ],
    "meta": {
        "pagination": { "page": 1, "per_page": 20, "total": 1, "total_pages": 1 }
    },
    "links": {
        "self": "/api/v1/users?page=1&per_page=20"
    }
}

Response Codes

200 Success
401 Unauthorized

Each user row also carries the values of any custom fields the site added to the account blueprint, and an extra map when a plugin supplies column values for it. twofa_secret is a boolean that only says whether a secret has been generated; the secret itself is never returned.

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: it runs after search, and the access and group filters and pagination are applied by the core endpoint after it, so a tab can never widen visibility or change the response envelope. Tabs work with both account backends. With Flex accounts the event's collection is a Flex collection; with plain file accounts it is an ArrayCollection of users keyed by username, and only accounts already in the list are kept from whatever the plugin hands back. 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.

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

Get User

GET /users/{username}
Get user details. Reading your own account needs only `api.access`; reading anyone else's needs `api.users.read`.

Parameters

Name Type Description
username required string The username to retrieve
JSON
{
    "data": {
        "username": "editor",
        "email": "[email protected]",
        "fullname": "Jane Editor",
        "title": "",
        "state": "enabled",
        "language": "",
        "content_editor": "",
        "access": { "api": { "access": true, "pages": { "read": true, "write": true } } },
        "groups": [],
        "avatar_url": null,
        "twofa_enabled": false,
        "twofa_secret": false,
        "created": "2026-09-01T10:00:00+00:00",
        "modified": "2026-09-01T10:00:00+00:00",
        "twofa_global_enabled": true
    }
}

Response Codes

200 Success
401 Unauthorized
403 Forbidden (reading another account without `api.users.read`)
404 User not found

The response carries an ETag header; send it back as If-Match on Update User to guard against overwriting a concurrent change. twofa_secret only says whether a secret has been generated, and twofa_global_enabled says whether the Login plugin's 2FA support is installed, so a client knows whether to offer 2FA setup. Any custom fields the site added to the account blueprint are included too.

Create User

POST /users
Create a new user account. Requires `api.users.write`.

Parameters

Name Type Description
username required string Unique username, 3 to 64 characters. It cannot start with a period or contain `..`, and cannot contain \ / ? * : ; { } or a line break.
password required string User password. It must match the site's `system.pwd_regex` when one is set, and be at least 8 characters when none is.
email required string User email address
fullname optional string Full display name
title optional string Title shown with the account
state optional string Account state, `enabled` (default) or `disabled`
access optional object Permission access object. Only a super admin can grant `admin.super` or `api.super`.
groups optional array Group names. Applied only when the caller is a super admin; ignored otherwise.
JSON
{"username": "editor", "password": "SecurePass123!", "email": "[email protected]", "fullname": "Jane Editor"}
JSON
{"data": {"username": "editor", "email": "[email protected]", "fullname": "Jane Editor", "title": "", "state": "enabled", "language": "", "content_editor": "", "access": {}, "groups": [], "avatar_url": null, "twofa_enabled": false, "twofa_secret": false, "created": "2026-09-21T10:00:00+00:00", "modified": "2026-09-21T10:00:00+00:00"}}

Response Codes

201 User created. The `Location` header points to the new user.
401 Unauthorized
403 Forbidden (missing `api.users.write`, or a non-super caller granting super access)
409 Username already exists
422 Validation error (missing field, invalid username, password fails the policy, or a field fails the account blueprint)

Any custom fields the site added to the account blueprint (user/blueprints/user/account.yaml) can be sent in the body too, and are validated against that blueprint.

Update User

PATCH /users/{username}
Update a user account. Only the fields you send are changed. You can update your own account with `api.access`; updating anyone else needs `api.users.write`.

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
title optional string Updated title
language optional string Preferred admin language
content_editor optional string Preferred content editor
state optional string Updated account state (`enabled` or `disabled`). Requires `api.users.write`.
access optional object Updated permission access object. Requires `api.users.write`; only a super admin can grant super access.
groups optional array Updated group names. Super admins only.
password optional string New password. It must match `system.pwd_regex` when set, and be at least 8 characters when not.
JSON
{"fullname": "Jane Q. Editor", "password": "N3wSecurePass!"}
JSON
{"data": {"username": "editor", "email": "[email protected]", "fullname": "Jane Q. Editor", "title": "", "state": "enabled", "language": "", "content_editor": "", "access": {}, "groups": [], "avatar_url": null, "twofa_enabled": false, "twofa_secret": false, "created": "2026-09-01T10:00:00+00:00", "modified": "2026-09-21T10:00:00+00:00", "twofa_global_enabled": true}}

Response Codes

200 User updated
401 Unauthorized
403 Forbidden (missing `api.users.write`, sending `state`/`access`/`groups` without the right permission, or a non-super caller editing a super admin)
404 User not found
409 Conflict (ETag mismatch)
422 Validation error (empty body, password fails the policy, or a field fails the account blueprint)

Supports optimistic concurrency control via the If-Match header. Include the ETag from your last GET request to prevent overwriting concurrent changes.

twofa_enabled and twofa_secret are ignored here, so a client can send back the whole user object without error. Two-factor authentication only changes through the Enable 2FA and Disable 2FA endpoints, which check a code when you change your own account.

Changing the password or setting state to disabled invalidates every API token already issued for the account. Custom fields the site added to the account blueprint can be sent as well.

Delete User

DELETE /users/{username}
Delete a user account. Requires `api.users.write`.

Parameters

Name Type Description
username required string The username to delete

Response Codes

204 User deleted
401 Unauthorized
403 Forbidden (missing `api.users.write`, deleting your own account, or a non-super caller deleting a super admin)
404 User not found

Upload Avatar

POST /users/{username}/avatar
Upload a custom avatar image for a user. Send as multipart/form-data. Your own avatar needs no extra permission; anyone else's needs `api.users.write`.

Parameters

Name Type Description
username required string The username to upload the avatar for
avatar required file Image file (PNG, JPEG, or WebP, up to 8 MB). The field may also be named `file`. The file contents are checked, not the declared type.
JSON
{"data": {"username": "admin", "email": "[email protected]", "fullname": "Site Admin", "title": "", "state": "enabled", "language": "", "content_editor": "", "access": {"admin": {"super": true}}, "groups": [], "avatar_url": "/api/v1/thumbnails/3f2a9c1b.jpg", "twofa_enabled": false, "twofa_secret": false, "created": "2026-09-01T10:00:00+00:00", "modified": "2026-09-01T10:00:00+00:00"}}

Response Codes

201 Avatar uploaded; the body is the updated user
401 Unauthorized
403 Forbidden (missing `api.users.write`, or a non-super caller changing a super admin)
404 User not found
422 Validation error (no file, file too large, or not a PNG, JPEG or WebP image)

Delete Avatar

DELETE /users/{username}/avatar
Remove the custom avatar for a user. Your own avatar needs no extra permission; anyone else's needs `api.users.write`.

Parameters

Name Type Description
username required string The username to remove the avatar for
JSON
{"data": {"username": "admin", "email": "[email protected]", "fullname": "Site Admin", "title": "", "state": "enabled", "language": "", "content_editor": "", "access": {"admin": {"super": true}}, "groups": [], "avatar_url": null, "twofa_enabled": false, "twofa_secret": false, "created": "2026-09-01T10:00:00+00:00", "modified": "2026-09-01T10:00:00+00:00"}}

Response Codes

200 Avatar removed; the body is the updated user
401 Unauthorized
403 Forbidden (missing `api.users.write`, or a non-super caller changing a super admin)
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. Generating a secret turns 2FA off until a code is confirmed with Enable 2FA. For your own account this needs `api.access`; for anyone else's it needs `api.users.write`.

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. Permissions are checked before the account is loaded, so a caller without them gets 403 whether or not the username exists.
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. Your own keys need `api.access`; anyone else's need `api.users.read`.

Parameters

Name Type Description
username required string The username to list API keys for
JSON
{"data": [{"id": "9f86d081884c7d65", "name": "CI Deploy Key", "prefix": "grav_1a2b3c4...", "scopes": [], "active": true, "created": 1710000000, "last_used": null, "expires": 1717776000}]}

Response Codes

200 Success
401 Unauthorized
403 Forbidden
404 User not found

created, last_used and expires are Unix timestamps; expires is null for a key that never expires.

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. Your own keys need `api.access`; keys for anyone else need `api.users.write`.

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 (default: `API Key`)
scopes optional array List of permission scopes for the key. An empty list means the key carries the account's full access.
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": ["api.pages.read"], "expiry_days": 90}
JSON
{"data": {"id": "9f86d081884c7d65", "name": "CI Deploy Key", "prefix": "grav_1a2b3c4...", "scopes": ["api.pages.read"], "active": true, "created": 1710000000, "last_used": null, "expires": 1717776000, "api_key": "grav_1a2b3c4d5e6f..."}}

Response Codes

201 API key created
401 Unauthorized
403 Forbidden (missing permission, a non-super caller creating a key for a super admin, or a scoped key asking for a scope outside its own)
404 User not found

When the request is authenticated with a scoped API key, the new key must be scoped too, and every scope it asks for must be within the calling key's own scopes.

Revoke API Key

DELETE /users/{username}/api-keys/{keyId}
Revoke and delete an API key. Your own keys need `api.access`; anyone else's need `api.users.write`.

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 User or 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), and the owner needs `api.access`. 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.
401 Unauthorized.
403 Caller is not the account owner, or lacks `api.access`. Ownership is checked before the account is loaded, so this does not reveal whether a username exists.
404 User not found.
422 Missing code, invalid code, or no 2FA secret generated yet (call `POST /users/{username}/2fa` first).
500 Login plugin with 2FA support not installed.

Disable 2FA

POST /users/{username}/2fa/disable
Disable 2FA on an account. Self-disable requires `api.access` and a valid current TOTP code, which stops a stolen session from removing 2FA on its own. Admins with `api.users.write` (or superadmin) can force-disable without a code, for lost-device recovery; only a super admin can do this for a super admin account. 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 without `api.users.write`; ignored for admin-forced disable.
JSON
{"code": "123456"}
JSON
{"data": {"twofa_enabled": false}}

Response Codes

200 2FA disabled.
401 Unauthorized.
403 Caller is neither the account owner nor holds `api.users.write`, or a non-super admin targets a super admin. Checked before the account is loaded, so this does not reveal whether a username exists.
404 User not found.
422 Missing/invalid code (self-disable path only).
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.

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.
id required string The id of a declared row action (from `GET /users/row-actions`).
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`, a non-super caller targets a super admin, or the handler rejected this target
404 Unknown username, or an action id the caller isn't authorized for
422 No action `id` in the body

Post the id of a declared row action. The API confirms the caller may list users (api.users.read), loads the target account (only a super admin may act on a super admin account), 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 — success 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 a 200 with status: error and a generic message rather than breaking the Users list; a thrown ForbiddenException propagates as a 403.

See the API Events page for the onApiUserListRowAction contract.

List Groups

GET /groups
List user groups, sorted by group name. Requires `api.users.read`.

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: 1000)
search optional string Filter by a free-text query across the group name, readable name and description.
JSON
{
    "data": [
        {
            "groupname": "editors",
            "readableName": "Editors",
            "description": "Can edit site content",
            "icon": "users",
            "enabled": true,
            "access": { "api": { "access": true, "pages": { "read": true, "write": true } } }
        }
    ],
    "meta": {
        "pagination": { "page": 1, "per_page": 20, "total": 1, "total_pages": 1 }
    },
    "links": {
        "self": "/api/v1/groups?page=1&per_page=20"
    }
}

Response Codes

200 Success
401 Unauthorized
403 Forbidden (missing `api.users.read`)

Groups are always read from, and saved to, user/config/groups.yaml. Groups that exist only in an environment overlay are not listed, so every group this endpoint returns can also be edited or deleted.

Get Group

GET /groups/{name}
Get a single user group. Requires `api.users.read`.

Parameters

Name Type Description
name required string The group name (path parameter)
JSON
{"data": {"groupname": "editors", "readableName": "Editors", "description": "Can edit site content", "icon": "users", "enabled": true, "access": {"api": {"access": true, "pages": {"read": true, "write": true}}}}}

Response Codes

200 Success
401 Unauthorized
403 Forbidden (missing `api.users.read`)
404 Group not found

The response carries an ETag header; send it back as If-Match on Update Group to guard against overwriting a concurrent change.

Create Group

POST /groups
Create a user group in `user/config/groups.yaml`. Requires a super admin, since group membership can grant any permission. Fires `onApiGroupCreated`.

Parameters

Name Type Description
groupname required string Group name, 1 to 200 letters, numbers, hyphens or underscores. It is the storage key and cannot be changed later.
readableName optional string Display name
description optional string Description
icon optional string Icon name
enabled optional boolean Whether the group is active (default: true)
access optional object Permission access object granted to members
JSON
{"groupname": "editors", "readableName": "Editors", "description": "Can edit site content", "access": {"api": {"access": true, "pages": {"read": true, "write": true}}}}
JSON
{"data": {"groupname": "editors", "readableName": "Editors", "description": "Can edit site content", "icon": "", "enabled": true, "access": {"api": {"access": true, "pages": {"read": true, "write": true}}}}}

Response Codes

201 Group created. The `Location` header points to the new group.
401 Unauthorized
403 Forbidden (not a super admin)
409 A group with that name already exists
422 Validation error (missing or invalid `groupname`)

Update Group

PATCH /groups/{name}
Update a user group. Only the fields you send are changed; `access` is replaced as a whole. Requires a super admin. Fires `onApiGroupUpdated`.

Parameters

Name Type Description
name required string The group name (path parameter). Groups cannot be renamed.
readableName optional string Display name
description optional string Description
icon optional string Icon name
enabled optional boolean Whether the group is active
access optional object Permission access object. Replaces the existing one.
JSON
{"description": "Can edit and publish site content", "enabled": true}
JSON
{"data": {"groupname": "editors", "readableName": "Editors", "description": "Can edit and publish site content", "icon": "users", "enabled": true, "access": {"api": {"access": true, "pages": {"read": true, "write": true}}}}}

Response Codes

200 Group updated
401 Unauthorized
403 Forbidden (not a super admin)
404 Group not found
409 Conflict (ETag mismatch)
422 Validation error (empty body)

Supports optimistic concurrency control via the If-Match header. Include the ETag from your last GET request to prevent overwriting concurrent changes.

Delete Group

DELETE /groups/{name}
Delete a user group from `user/config/groups.yaml`. Requires a super admin. Fires `onApiGroupDeleted`.

Parameters

Name Type Description
name required string The group name (path parameter)

Response Codes

204 Group deleted
401 Unauthorized
403 Forbidden (not a super admin)
404 Group not found

Accounts that list the deleted group in their groups keep the entry; it simply no longer grants anything.

List Invitations

GET /invitations
List pending invitations, newest first. Expired invitations are purged first. Requires `api.users.write`, because each record carries the token that accepts it.
JSON
{"data": {"invitations": [{"token": "b7c1e0f4a9d2...", "email": "[email protected]", "fullname": "New Editor", "groups": [], "created": 1758448800, "created_by": "admin", "created_by_name": "Site Admin", "expires": 1759053600, "expired": false}]}}

Response Codes

200 Success
401 Unauthorized
403 Forbidden (missing `api.users.write`)

created and expires are Unix timestamps. The access an invitation grants is not included in the listing.

Create Invitation

POST /invitations
Create an invitation with preset access and send it by email when the Email plugin is configured. The recipient picks their own username and password when accepting, and gets exactly the access set here. Requires `api.users.write`.

Parameters

Name Type Description
email required string Address to invite. Any pending invitation for the same address is replaced.
fullname optional string Name to prefill on the accept form
access optional object Permission access object the new account gets. `admin.super` and `api.super` are stripped unless the caller is a super admin.
groups optional array Groups the new account joins. Applied only when the caller is a super admin; ignored otherwise.
expiration optional integer Seconds until the invitation expires. Defaults to `plugins.api.invitations.expiration` (7 days). Values under 300 fall back to the default, and the maximum is 30 days (2592000).
message optional string Personal note included in the email
admin_base_url optional string Admin base URL to build the invite link from, when the admin is served somewhere other than the default
JSON
{"email": "[email protected]", "fullname": "New Editor", "access": {"api": {"access": true, "pages": {"read": true}}}, "expiration": 604800}
JSON
{"data": {"token": "b7c1e0f4a9d2...", "email": "[email protected]", "fullname": "New Editor", "groups": [], "created": 1758448800, "created_by": "admin", "created_by_name": "Site Admin", "expires": 1759053600, "expired": false, "link": "https://example.com/admin/invite?token=b7c1e0f4a9d2...", "email_sent": false, "warning": "Email is not configured, so no invitation email was sent. Share the link manually."}}

Response Codes

201 Invitation created. The `Location` header points to `/auth/invite/{token}`, the public endpoint that validates and accepts it.
401 Unauthorized
403 Forbidden (missing `api.users.write`)
409 A user with that email already exists
422 Validation error (missing or invalid email)

The invitation is created even when the email can't be sent. In that case email_sent is false and warning explains why, so the link can be shared by hand. The recipient opens the link, and the accept form uses GET /auth/invite/{token} and POST /auth/invite/{token} to create the account.

Revoke Invitation

DELETE /invitations/{token}
Revoke a pending invitation so its link no longer works. Requires `api.users.write`.

Parameters

Name Type Description
token required string The invitation token (path parameter)

Response Codes

204 Invitation revoked
401 Unauthorized
403 Forbidden (missing `api.users.write`)
404 Invitation not found

Resend Invitation

POST /invitations/{token}/resend
Send the email for a pending invitation again. The token and expiry stay the same. Requires `api.users.write`.

Parameters

Name Type Description
token required string The invitation token (path parameter)
message optional string Personal note included in the email
admin_base_url optional string Admin base URL to build the invite link from
JSON
{"message": "Reminder: your invitation is waiting."}
JSON
{"data": {"token": "b7c1e0f4a9d2...", "email": "[email protected]", "fullname": "New Editor", "groups": [], "created": 1758448800, "created_by": "admin", "created_by_name": "Site Admin", "expires": 1759053600, "expired": false, "link": "https://example.com/admin/invite?token=b7c1e0f4a9d2...", "email_sent": true}}

Response Codes

200 Email sent
401 Unauthorized
403 Forbidden (missing `api.users.write`)
404 Invitation not found or expired
422 Email is not configured