Skip to content

Powered by Grav + Helios

Dashboard

Dashboard

Endpoints for dashboard data including statistics, notifications, news feed, page view popularity, the storage exposure check, and the customizable widget layout.

Endpoints for dashboard data including statistics, notifications, news feed, page view popularity, the storage exposure check, and the customizable widget layout.

Dashboard Stats

GET /dashboard/stats
Get a summary of site statistics for the dashboard, including available GPM updates. Requires `api.system.read`.
JSON
{"data": {"pages": {"total": 42, "published": 40}, "users": {"total": 5}, "plugins": {"total": 12, "active": 10, "updatable": 2}, "themes": {"total": 2, "updatable": 0, "active_updatable": false}, "grav": {"updatable": false}, "media": {"total": 120}, "theme": "quark", "grav_version": "2.1.0", "php_version": "8.3.14", "last_backup": "2026-03-26T03:00:00+00:00"}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.system.read` permission

Returns a snapshot of site statistics including page counts, user counts, plugin and theme counts with available updates, whether a Grav update is available, the media file count, the active theme, Grav/PHP versions, and the last backup date. The update counts fall back to zero when GPM can't be reached. last_backup only counts archives made by Grav's backup tool (<name>--<timestamp>.zip in backup://), and is null when there are none.

Page View Statistics

GET /dashboard/popularity
Get page view statistics from the API popularity tracker. Requires `api.system.read`.
JSON
{"data": {"summary": {"today": 34, "week": 212, "month": 804}, "chart": [{"date": "Sep 8", "views": 25}, {"date": "Sep 9", "views": 31}], "top_pages": [{"route": "/blog", "views": 1204}, {"route": "/", "views": 980}]}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.system.read` permission

Returns page view statistics including daily chart data for the last 14 days (oldest first, with M j date labels), summary counters (today, the last 7 days, this calendar month), and the top 10 pages by total views. Data comes from the API plugin's own popularity store (plugins.api.popularity); on the first read after an upgrade from the classic admin, its older popularity files are imported automatically.

Dashboard Notifications

GET /dashboard/notifications
Get system notifications grouped by display location (`top`, `dashboard`, `feed`), with a `last_checked` timestamp. Requires `api.system.read`.

Parameters

Name Type Description
location optional string Return only one group: `top`, `dashboard`, or `feed`.
force optional boolean Bypass the cached getgrav.org notifications and refetch. The cache otherwise lasts `system.session.timeout` seconds per user, and a failed fetch silently returns the cached copy.
JSON
{"data": {"notifications": {"feed": [{"id": 214, "date": "2026-07-31 12:00", "type": "info", "location": ["feed"], "message": "Run a real community on your own site with **Forum Pro** for Grav 2, now available.", "link": "https://getgrav.org/premium/forum-pro"}]}, "last_checked": "2026-09-21T08:00:00+00:00"}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.system.read` permission

Returns system notifications grouped by location (feed, dashboard, top), plus a last_checked timestamp. The response combines the getgrav.org feed with notices contributed by plugins through the onApiDashboardNotifications event, then drops any the current user has dismissed — honoring each notice's optional reappear_after interval.

Each notification item carries id, message, and date, plus optional icon (a Lucide name or emoji), title, action ({label, url}), link, type (info, notice, warning, or promo), and reappear_after. The top group renders as a rotating banner above the dashboard; dashboard and feed render in the Notifications widget. Dismiss a notice with POST /dashboard/notifications/{id}/hide.

For the plugin-side recipe — raising a persistent, dismissible banner and the difference between notifications and transient toasts — see Notifications & Toasts in the developer guide.

News Feed

GET /dashboard/feed
Get the latest news feed from getgrav.org. Requires `api.system.read`.

Parameters

Name Type Description
force optional boolean Bypass the cached feed and refetch.
JSON
{"data": {"feed": [{"title": "Grav 2.1 Released", "url": "https://getgrav.org/blog/grav-2-1-released", "date": "2026-09-11T12:00:00+00:00", "summary": ""}], "last_checked": "2026-09-21T08:00:00+00:00"}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.system.read` permission

Returns the latest blog posts from the getgrav.org Atom feed: up to 10 of the most recent articles, each with title, url, date and summary, plus a last_checked timestamp. The feed is cached per user for system.session.timeout seconds, and a failed fetch silently returns the cached copy.

Hide Notification

POST /dashboard/notifications/{id}/hide
Dismiss a dashboard notification for the current user. Records a timestamp in `user://data/notifications/{username}.yaml` so that notification is filtered out of future `GET /dashboard/notifications` responses.

Parameters

Name Type Description
id required string Notification id (from the `GET /dashboard/notifications` response). It is not checked against known notifications, but it must be 1-64 letters, digits, `.`, `_` or `-`, starting with a letter or digit. getgrav.org uses numbers; plugins use slugs such as `login-lockout`.

Response Codes

204 Notification dismissed.
401 Unauthorized.
403 Missing `api.system.write` permission.
422 The id is not a valid notification id.

Storage Exposure Probe

GET /dashboard/security/exposure-probe
Get the probe URLs Admin2 uses to check whether private storage folders can be reached over HTTP. Requires `api.system.read`.
JSON
{"data": {"url": "https://example.com/user/data/grav-security-probe.dat", "token": "3f9c2a7e5b1d4c8a9e0f6b2d7c4a1e8f", "available": true, "probes": [{"location": "user/data", "extension": "dat", "url": "https://example.com/user/data/grav-security-probe.dat", "token": "3f9c2a7e5b1d4c8a9e0f6b2d7c4a1e8f", "available": true}, {"location": "tmp", "extension": "txt", "url": "https://example.com/tmp/grav-security-probe.txt", "token": "8b1e4d7a2c9f0e3b6a5d8c1f4e7b0a2d", "available": true}]}}

Response Codes

200 Probe list returned.
401 Unauthorized
403 Missing `api.system.read` permission

For each of user://data, backup:// and tmp:// that lives inside the web root, the server makes sure a sentinel file grav-security-probe.{dat,txt,zip,json} exists, creating it with a random hex token if it is missing (it never overwrites an existing file or follows a symlink), and returns its public URL and token. Admin2 then requests each URL through the site's real web server: if the response body matches the token, that folder and file type are publicly reachable, which usually means the web server isn't applying Grav's deny rules. Several file types are tested because a front proxy may serve some extensions itself.

Folders relocated outside the web root have no public URL and are skipped. The top-level url, token and available fields repeat the first user://data probe for older Admin2 builds. Note that this GET creates files on first call.

Dashboard Widgets

GET /dashboard/widgets
Get the resolved dashboard widget list and layouts for the current user. Requires `api.access`.
JSON
{"data": {"widgets": [{"id": "core.stats", "source": "core", "label": "ADMIN_NEXT.DASHBOARD.WIDGETS.STATS", "icon": "BarChart3", "sizes": ["md", "lg", "xl"], "defaultSize": "xl", "priority": 100, "visible": true, "size": "xl", "order": 9000}], "user_layout": {}, "site_layout": {}, "can_edit_site": true}}

Response Codes

200 Widget list and layouts returned.
401 Unauthorized
403 Missing `api.access` permission

The list merges the built-in core widgets, widgets plugins add through the onApiDashboardWidgets event, the site default layout, and the current user's layout. Widgets whose authorize permission the user lacks are left out (a super user, within their API key's scope, sees every widget), and authorize is stripped from the response. A widget the site layout hides is removed entirely, so a user can't turn it back on.

Each widget is annotated with its effective visible, size and order, and the list is sorted by order. A widget with no saved order is placed by its priority. A plugin widget that doesn't declare its sizes gets sm, md and lg, and one without a valid defaultSize gets md (or its first size), so a widget that leaves them out still renders. Plugin widgets carry source: plugin; can_edit_site tells the client whether the caller may save the site layout.

Save Dashboard Layout

PATCH /dashboard/layout
Save the current user's dashboard layout to `admin_next.dashboard` in their account YAML, and return the resolved widget list (the same as `GET /dashboard/widgets`). Requires `api.access`. Demo accounts are refused.

Parameters

Name Type Description
preset optional string One of `default`, `minimal`, `compact`, `custom`. Anything else is stored as `custom`.
widgets optional array Widget entries: `id` (string, required), `visible` (boolean, default true), `size` (`xs` to `xl`) and `order` (number).
JSON
{"preset": "custom", "widgets": [{"id": "core.stats", "visible": true, "size": "lg", "order": 10}, {"id": "core.news-feed", "visible": false, "order": 20}]}
JSON
{"data": {"widgets": [{"id": "core.stats", "source": "core", "label": "ADMIN_NEXT.DASHBOARD.WIDGETS.STATS", "icon": "BarChart3", "sizes": ["md", "lg", "xl"], "defaultSize": "xl", "priority": 100, "visible": true, "size": "lg", "order": 10}], "user_layout": {"preset": "custom", "widgets": [{"id": "core.stats", "visible": true, "size": "lg", "order": 10}, {"id": "core.news-feed", "visible": false, "size": null, "order": 20}]}, "site_layout": {}, "can_edit_site": true}}

Response Codes

200 Layout saved; resolved widget list returned.
401 Unauthorized
403 Missing `api.access` permission, or a demo account
422 The body is not a JSON object.

Despite the PATCH verb, the stored layout is replaced as a whole, so send every widget entry you want to keep. Entries without a string id are dropped, and a size outside xs to xl is stored as null. An entry sent without a numeric order is stored without one, so that widget keeps its priority-based position instead of jumping to the top.

Save Site Dashboard Layout

PATCH /dashboard/site-layout
Save the default dashboard layout for every user to `dashboard.site_layout` in `user/config/admin-next.yaml`, and return the resolved widget list for the caller. Requires a super user (subject to the API key scope cap). Demo accounts are refused.

Parameters

Name Type Description
preset optional string One of `default`, `minimal`, `compact`, `custom`. Anything else is stored as `custom`.
widgets optional array Widget entries: `id` (string, required), `visible` (boolean, default true), `size` (`xs` to `xl`) and `order` (number).
JSON
{"preset": "custom", "widgets": [{"id": "core.news-feed", "visible": false}, {"id": "core.stats", "size": "xl", "order": 10}]}
JSON
{"data": {"widgets": [{"id": "core.stats", "source": "core", "label": "ADMIN_NEXT.DASHBOARD.WIDGETS.STATS", "icon": "BarChart3", "sizes": ["md", "lg", "xl"], "defaultSize": "xl", "priority": 100, "visible": true, "size": "xl", "order": 10}], "user_layout": {}, "site_layout": {"preset": "custom", "widgets": [{"id": "core.news-feed", "visible": false, "size": null}, {"id": "core.stats", "visible": true, "size": "xl", "order": 10}]}, "can_edit_site": true}}

Response Codes

200 Site layout saved; resolved widget list returned.
401 Unauthorized
403 Not a super user, or a demo account
422 The body is not a JSON object.

The stored layout is replaced as a whole, with the same normalization as PATCH /dashboard/layout. A widget marked visible: false here is hidden for every user and can't be turned back on in a user's own layout. Sizes and orders act as defaults that each user's layout can override.