Skip to content

Powered by Grav + Helios

Admin2 Integration

Admin2 Integration

Endpoints that power Admin2's extensibility and preferences: menubar toolbar items, floating widgets, slide-in context panels, settings-page panels, the registry of plugin-provided custom field components, markdown editor toolbar buttons, and Admin2's user and site preferences and branding. All en...

Endpoints that power Admin2's extensibility and preferences: menubar toolbar items, floating widgets, slide-in context panels, settings-page panels, the registry of plugin-provided custom field components, markdown editor toolbar buttons, and Admin2's user and site preferences and branding.

All endpoints require api.access and are meant to be called by Admin2, not by end-user applications. Writing site preferences and branding also needs a super user. The UI registries are backed by events (onApiMenubarItems, onApiFloatingWidgets, onApiContextPanels, onApiAdminSettingsPanels, onApiMarkdownEditorButtons), which plugins hook to register items. See the Developer Guide for the full integration recipe.

GET /menubar/items
Collect toolbar menu items registered by plugins via the `onApiMenubarItems` event. Each item declares an id, owning plugin slug, label, icon, action key, and an optional confirmation prompt. Admin2 renders them as buttons in the top toolbar and POSTs to `/menubar/actions/{plugin}/{action}` when clicked. Optional presentation keys control appearance: `variant` (default|primary|success|warning|danger), `showLabel`, and `size` (sm|md). Optional placement keys control where the button sits: `placement` (`start`, the default, is the open space on the left, away from the Clear Cache action; `end` sits beside the core actions behind a divider) and `priority` (higher renders earlier within a zone). An item may instead declare a client-side intent that runs in Admin2 without a server round-trip: `route` navigates the SPA (e.g. `/pages/new?parent=/blog&template=item` to deep-link the new-page form with a preset parent and locked template), and `modal` opens one of the plugin's own modal web components (`{component, title?, props?, size?, useStandardHeader?}` — see `GET /gpm/plugins/{slug}/modal-script/{modalId}`). When `route` or `modal` is present it takes precedence over `action`; `confirm` still runs first if set. An item may declare `authorize` (a permission string, or an array for an any-of test): items the caller fails are left out and `authorize` is stripped from the response. Items are returned in event order; Admin2 sorts them by `priority`.
JSON
{"data": [{"id": "warm-cache", "plugin": "warm-cache", "label": "Warm Cache", "icon": "fa-tachometer", "action": "warm", "confirm": "Warm the cache?"}, {"id": "new-article", "plugin": "my-plugin", "label": "New Article", "icon": "fa-plus", "route": "/pages/new?parent=/blog&template=item"}]}

Response Codes

200 Items returned.
401 Unauthorized.
403 Missing `api.access` permission.
POST /menubar/actions/{plugin}/{action}
Execute a plugin-registered menubar action. Fires `onApiMenubarAction` with the plugin/action routing keys, the posted body, and the current user. Plugins that own the `{plugin}` slug set `$event['result']`, for example `{"status": "success", "message": "..."}` or `{"status": "error", "message": "..."}`. The result is returned as-is with HTTP 200 either way, and Admin2 shows a success or error toast based on `status`. If a registered menubar item matches this plugin and action, the caller must satisfy its `authorize`. An action with no matching item only needs `api.access`, so its handler should check permissions itself.

Parameters

Name Type Description
plugin required string Owning plugin slug (from the menubar item registration).
action required string Action key (from the menubar item registration).
JSON
{}
JSON
{"data": {"status": "success", "message": "Cache warmed successfully."}}

Response Codes

200 The handler ran. Its result is returned as-is, including `status: error` results.
401 Unauthorized.
403 Missing `api.access` permission, or the caller fails the `authorize` declared on the matching menubar item.
404 No plugin handled this plugin/action pair.

Floating Widgets

GET /floating-widgets
Collect persistent UI widgets (chat assistants, notification panels) registered by plugins via `onApiFloatingWidgets`. Each widget ships a web component at `admin-next/widgets/{slug}.js` that Admin2 loads on-demand via `/gpm/plugins/{slug}/widget-script`. A widget may set `autoLoad` to load its script eagerly and `routes` to scope that autoload to specific admin views.
JSON
{"data": [{"id": "ai-pro-chat", "plugin": "ai-pro", "label": "AI Assistant", "icon": "bot", "priority": 10}, {"id": "users-enhancer", "plugin": "my-plugin", "label": "Users enhancer", "showFab": false, "autoLoad": true, "routes": ["/users"]}]}

Response Codes

200 Widgets returned.
401 Unauthorized.
403 Missing `api.access` permission.

Each widget carries an id, plugin, label, icon and optional priority. Widgets are returned in event order; Admin2 sorts them by priority. A widget may declare authorize (a permission string, or an array for an any-of test): widgets the caller fails are left out and authorize is stripped from the response. Two optional flags control loading:

  • autoLoad — load the widget's script eagerly instead of on first launcher click. Used by field/table enhancers that register behavior without showing a floating action button (usually paired with showFab: false).
  • routes — a list of admin-internal SPA routes (/users, /pages, /plugin/my-plugin) an autoloading widget applies to. Admin2 loads the script only on an exact route match; omit it to load on every route. The API normalizes each entry to a leading-slash, trailing-slash-free path and drops invalid ones. routes scopes script loading only — it is not a permission boundary; authorize remains the security check.

See the API Events page for the onApiFloatingWidgets contract and the Developer Guide for the full widget recipe.

Context Panels

GET /context-panels
Collect slide-in panel registrations. Context panels are triggered by toolbar buttons inside Admin2 editors — plugins hook `onApiContextPanels` to register them, specify which editor contexts they appear in (e.g. `pages`), and ship a web component at `admin-next/panels/{slug}.js` served via `/gpm/plugins/{slug}/panel-script`. Optional `badgeEndpoint` returns `{count: N}` to drive a badge on the toolbar button. A panel may declare `authorize` (a permission string, or an array for an any-of test): panels the caller fails are left out and `authorize` is stripped from the response. The rest are sorted by `priority`, highest first, with ties kept in registration order.
JSON
{"data": [{"id": "revisions-pro", "plugin": "revisions-pro", "label": "Revision History", "icon": "history", "contexts": ["pages"], "priority": 10, "width": 900, "badgeEndpoint": "/revisions-pro/badge"}]}

Response Codes

200 Panels returned.
401 Unauthorized.
403 Missing `api.access` permission.

Settings Panels

GET /settings/panels
Collect admin-settings panel registrations from plugins via `onApiAdminSettingsPanels`. Unlike full plugin pages (registered with `onApiSidebarItems`), settings panels render as cards inside Admin2's Settings page. Each panel follows the blueprint-mode plugin-page format: a blueprint file plus data/save endpoints. Sorted by `priority` descending (higher priority first), preserving insertion order for ties. The API does not filter panels by permission, so a plugin should check `$event['user']` before adding a panel.
JSON
{"data": [{"id": "login-settings", "plugin": "api", "label": "Login & Security", "description": "Authentication settings", "icon": "fa-shield-alt", "blueprint": "login-settings", "data_endpoint": "/login-settings/data", "save_endpoint": "/login-settings/save", "priority": 0}]}

Response Codes

200 Panels returned (sorted).
401 Unauthorized.
403 Missing `api.access` permission.

List Custom Fields

GET /custom-fields
Map every custom blueprint field type to the plugin or theme that ships it, by scanning `admin-next/fields/*.js` in every enabled plugin and installed theme. Each value is `{ slug, kind }`, where `kind` is `plugins` or `themes`, so Admin2 fetches each field script from the correct `/gpm/{kind}/{slug}/field/{type}` route. Admin2 calls this once on load to pre-populate its custom-field registry so unknown field types in blueprints render correctly on first sight. It needs only `api.access`, so plugin fields work for editors who can't manage packages.
JSON
{"data": {"codeshtheme": {"slug": "codesh", "kind": "plugins"}, "products-status": {"slug": "license-manager", "kind": "plugins"}, "tinymce": {"slug": "mytheme", "kind": "themes"}}}

Response Codes

200 Registry returned.
401 Unauthorized.
403 Missing `api.access` permission.

Editor Toolbar Buttons

GET /editor/toolbar-buttons
Collect buttons that plugins add to the Admin2 default (CodeMirror) markdown editor toolbar through the `onApiMarkdownEditorButtons` event. A button either opens one of the plugin's modal web components (`modal`, served by `/gpm/plugins/{plugin}/modal-script/{component}`) or carries an `insert` payload (`{content, mode}`, where `mode` is `insert-at-cursor`, `append` or `replace`) that the editor applies directly. A button may declare `authorize` (a permission string, or an array for an any-of test): buttons the caller fails are left out and `authorize` is stripped from the response. Buttons are returned in event order.
JSON
{"data": [{"id": "youtube", "plugin": "youtube", "label": "YouTube Video", "icon": "<svg viewBox=\"0 0 24 24\">...</svg>", "modal": {"component": "youtube-insert", "title": "Insert YouTube Video", "size": "md"}}, {"id": "hr", "plugin": "my-plugin", "label": "Horizontal Rule", "icon": "fa-minus", "insert": {"content": "---", "mode": "insert-at-cursor"}}]}

Response Codes

200 Buttons returned.
401 Unauthorized.
403 Missing `api.access` permission.

Get Admin2 Preferences

GET /admin-next/preferences
Return the full preferences payload Admin2 reads once on boot: site branding and ready-to-use logo URLs (`branding_urls`), site defaults (`site`), site-only settings (`site_settings`), media upload limits (`media_upload`), the caller's own saved overrides (`user`), the merged `effective` values, and `can_edit_site` (whether the caller is a super user who may write site preferences and branding). The response carries an `ETag` header.
JSON
{"data": {"branding": {"mode": "default", "text": "Grav", "logoLight": "", "logoDark": "", "logoHeight": 0, "title": "", "subtitle": "", "showPoweredBy": true, "favicon": ""}, "branding_urls": {"light": "", "dark": "", "favicon": ""}, "site": {"colorMode": "", "accentHue": 271, "accentSaturation": 91, "fontFamily": "google-sans", "fontSize": "normal", "editorMode": "normal", "editorKeymap": "default", "editorStickyToolbar": true, "editorFixedHeight": 0, "adminLanguage": "en", "pagesPerPage": 20, "pagesViewMode": "miller", "usersViewMode": "cards", "groupsViewMode": "cards", "pluginsViewMode": "cards", "themesViewMode": "cards", "flexAfterSave": ""}, "site_settings": {"autoSaveEnabled": false, "autoSaveToolbarUndo": true, "autoSaveBatchWindowMs": 0, "collabEnabled": true, "menubarLinks": []}, "media_upload": {"resizeWidth": 0, "resizeHeight": 0, "resizeQuality": 0.8, "minWidth": 0, "minHeight": 0, "maxWidth": 0, "maxHeight": 0}, "user": {"colorMode": "dark"}, "effective": {"colorMode": "dark", "accentHue": 271, "accentSaturation": 91, "fontFamily": "google-sans", "fontSize": "normal", "editorMode": "normal", "editorKeymap": "default", "editorStickyToolbar": true, "editorFixedHeight": 0, "adminLanguage": "en", "pagesPerPage": 20, "pagesViewMode": "miller", "usersViewMode": "cards", "groupsViewMode": "cards", "pluginsViewMode": "cards", "themesViewMode": "cards", "flexAfterSave": "", "autoSaveEnabled": false, "autoSaveToolbarUndo": true, "autoSaveBatchWindowMs": 0, "collabEnabled": true, "menubarLinks": []}, "can_edit_site": true}}

Response Codes

200 Preferences returned.
401 Unauthorized.
403 Missing `api.access` permission.

Values that a user can override resolve in this order: built-in defaults, then the site defaults (ui.defaults in user/config/admin-next.yaml), then the user's own overrides (admin_next.preferences in their account file). If the user has never picked an admin language in Admin2, a classic admin language: value on the account is used instead. Site-only settings (ui.settings: auto-save, collaboration and menubar links) are applied last and can't be overridden per user.

Update User Preferences

PATCH /admin-next/preferences/user
Save the caller's own preference overrides, stored under `admin_next.preferences` in their account file. Keys left out of the body keep their saved value, and a key set to `null` removes that override so the site default applies again. Only user-overridable keys are accepted (`colorMode`, `accentHue`, `accentSaturation`, `fontFamily`, `fontSize`, `editorMode`, `editorKeymap`, `editorStickyToolbar`, `editorFixedHeight`, `adminLanguage`, `pagesPerPage`, `pagesViewMode`, `usersViewMode`, `groupsViewMode`, `pluginsViewMode`, `themesViewMode`, `flexAfterSave`); site-only keys such as `autoSaveEnabled` or `menubarLinks` are ignored. Invalid values (an unknown option, a wrong type) are dropped rather than rejected, and numbers are clamped to their allowed range. Returns the full resolved payload, the same as `GET /admin-next/preferences`. Demo accounts are refused.
JSON
{"colorMode": "dark", "accentHue": 200, "fontSize": null}
JSON
{"data": {"branding": {"mode": "default", "text": "Grav"}, "branding_urls": {"light": "", "dark": "", "favicon": ""}, "site": {"colorMode": "", "accentHue": 271}, "site_settings": {"autoSaveEnabled": false}, "media_upload": {"resizeWidth": 0}, "user": {"colorMode": "dark", "accentHue": 200}, "effective": {"colorMode": "dark", "accentHue": 200, "fontSize": "normal"}, "can_edit_site": false}}

Response Codes

200 Overrides saved; resolved preferences returned.
401 Unauthorized.
403 Missing `api.access` permission, or a demo account.

Reset User Preferences

DELETE /admin-next/preferences/user
Remove every preference override the caller has saved (the whole `admin_next.preferences` block in their account file), so the site defaults apply again. This backs the "Reset to site defaults" action in Admin2. The dashboard layout (`admin_next.dashboard`) is not touched. Returns the full resolved payload, the same as `GET /admin-next/preferences`. Demo accounts are refused.
JSON
{"data": {"branding": {"mode": "default", "text": "Grav"}, "branding_urls": {"light": "", "dark": "", "favicon": ""}, "site": {"colorMode": "", "accentHue": 271}, "site_settings": {"autoSaveEnabled": false}, "media_upload": {"resizeWidth": 0}, "user": [], "effective": {"colorMode": "", "accentHue": 271}, "can_edit_site": false}}

Response Codes

200 Overrides cleared; resolved preferences returned.
401 Unauthorized.
403 Missing `api.access` permission, or a demo account.

Update Site Preferences

PATCH /admin-next/preferences/site
Save site-wide preferences to `user/config/admin-next.yaml`. The flat body is split in two. User-overridable keys (the same keys `PATCH /admin-next/preferences/user` accepts) are merged into `ui.defaults`: only the keys sent change, a key set to `null` removes the saved default so the built-in value applies, and invalid values are dropped. Site-only keys (`autoSaveEnabled`, `autoSaveToolbarUndo`, `autoSaveBatchWindowMs`, `collabEnabled`, `menubarLinks`) are merged into `ui.settings`, again changing only the keys sent. Any other key is ignored. Requires a super user (subject to the API key scope cap). Demo accounts are refused. Returns the full resolved payload, the same as `GET /admin-next/preferences`.
JSON
{"accentHue": 271, "fontFamily": "inter", "colorMode": null, "autoSaveEnabled": true, "menubarLinks": [{"label": "Docs", "url": "https://learn.getgrav.org", "icon": "book", "external": true}]}
JSON
{"data": {"branding": {"mode": "default", "text": "Grav"}, "branding_urls": {"light": "", "dark": "", "favicon": ""}, "site": {"colorMode": "", "accentHue": 271, "fontFamily": "inter"}, "site_settings": {"autoSaveEnabled": true, "autoSaveToolbarUndo": true, "autoSaveBatchWindowMs": 0, "collabEnabled": true, "menubarLinks": [{"label": "Docs", "url": "https://learn.getgrav.org", "icon": "book", "external": true}]}, "media_upload": {"resizeWidth": 0}, "user": [], "effective": {"colorMode": "", "accentHue": 271, "fontFamily": "inter", "autoSaveEnabled": true}, "can_edit_site": true}}

Response Codes

200 Site preferences saved; resolved preferences returned.
401 Unauthorized.
403 Not a super user, or a demo account.

Each menubarLinks entry needs a non-empty label and url, and may carry icon and external; entries missing either required field are dropped.

Update Branding

PATCH /admin-next/branding
Update the Admin2 branding stored under `ui.branding` in `user/config/admin-next.yaml`. The body is merged over the current branding, so send only the fields you want to change. Fields: `mode` (`default` shows the Grav logo, `text` shows `text`, `custom` shows the uploaded logos; unknown values fall back to `default`), `text` (up to 64 characters), `logoHeight` (height of a custom logo in the sidebar in CSS pixels, clamped to 16 to 44; `0` uses the built-in 28px), `title` (sign-in screen and browser tab title, up to 64 characters), `subtitle` (sign-in subtitle, up to 128 characters), `showPoweredBy` (show the "Powered by Grav CMS" line on the login and setup screens), and `logoLight`, `logoDark` and `favicon`, which accept only a basename inside `user/media/admin-next/`. Upload and remove those files with `POST` and `DELETE /admin-next/branding/logo`. Requires a super user (subject to the API key scope cap). Demo accounts are refused. Returns the full resolved payload, the same as `GET /admin-next/preferences`.
JSON
{"mode": "text", "text": "Acme CMS", "title": "Acme Content Admin", "logoHeight": 32, "showPoweredBy": false}
JSON
{"data": {"branding": {"mode": "text", "text": "Acme CMS", "logoLight": "", "logoDark": "", "logoHeight": 32, "title": "Acme Content Admin", "subtitle": "", "showPoweredBy": false, "favicon": ""}, "branding_urls": {"light": "", "dark": "", "favicon": ""}, "site": {"colorMode": "", "accentHue": 271}, "site_settings": {"autoSaveEnabled": false}, "media_upload": {"resizeWidth": 0}, "user": [], "effective": {"colorMode": "", "accentHue": 271}, "can_edit_site": true}}

Response Codes

200 Branding saved; resolved preferences returned.
401 Unauthorized.
403 Not a super user, or a demo account.
POST /admin-next/branding/logo
Upload a light logo, dark logo or favicon (multipart, field `file` or `logo`) to `user/media/admin-next/` and record it in the site branding. The file type is read from the file contents, never the client-declared MIME type: PNG, JPEG, WebP and ICO are accepted as images, and anything else must parse as an SVG document, which is sanitized (script and foreignObject elements, `on*` attributes, and `javascript:`/`data:` URLs are removed). Maximum size is 4 MB. The file is stored as `logo-{variant}-{hash}.{ext}` (or `favicon-favicon-{hash}.{ext}`) and the previous file for that variant is deleted. Uploading a light or dark logo switches the branding `mode` to `custom` unless it is `text`. Requires a super user (subject to the API key scope cap). Demo accounts are refused. Returns the full resolved payload; `branding_urls` holds the new file's URL.

Parameters

Name Type Description
variant required string Query parameter (or multipart field): `light`, `dark` or `favicon`. The query parameter wins when both are sent.
file required file The image file (multipart field `file`, or `logo`).
JSON
{"data": {"branding": {"mode": "custom", "text": "Grav", "logoLight": "logo-light-3f9c2a7e5b.svg", "logoDark": "", "logoHeight": 0, "title": "", "subtitle": "", "showPoweredBy": true, "favicon": ""}, "branding_urls": {"light": "/user/media/admin-next/logo-light-3f9c2a7e5b.svg", "dark": "", "favicon": ""}, "site": {"colorMode": "", "accentHue": 271}, "site_settings": {"autoSaveEnabled": false}, "media_upload": {"resizeWidth": 0}, "user": [], "effective": {"colorMode": "", "accentHue": 271}, "can_edit_site": true}}

Response Codes

201 Logo stored; resolved preferences returned.
401 Unauthorized.
403 Not a super user, or a demo account.
422 Missing or invalid `variant`, no file uploaded, a file over 4 MB, or content that is neither a supported image nor a valid SVG.
DELETE /admin-next/branding/logo
Delete the stored file for one branding variant and clear its path in the site branding. If both logo variants end up empty while the mode is `custom`, the mode reverts to `default` so Admin2 shows the built-in Grav logo. Succeeds even when no file was set for the variant. Requires a super user (subject to the API key scope cap). Demo accounts are refused. Returns the full resolved payload, the same as `GET /admin-next/preferences`.

Parameters

Name Type Description
variant required string Query parameter: `light`, `dark` or `favicon`.
JSON
{"data": {"branding": {"mode": "default", "text": "Grav", "logoLight": "", "logoDark": "", "logoHeight": 0, "title": "", "subtitle": "", "showPoweredBy": true, "favicon": ""}, "branding_urls": {"light": "", "dark": "", "favicon": ""}, "site": {"colorMode": "", "accentHue": 271}, "site_settings": {"autoSaveEnabled": false}, "media_upload": {"resizeWidth": 0}, "user": [], "effective": {"colorMode": "", "accentHue": 271}, "can_edit_site": true}}

Response Codes

200 Variant cleared; resolved preferences returned.
401 Unauthorized.
403 Not a super user, or a demo account.
422 Missing or invalid `variant`.