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
/dashboard/stats
{"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
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
/dashboard/popularity
{"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
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
/dashboard/notifications
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. |
{"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
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
/dashboard/feed
Parameters
| Name | Type | Description |
|---|---|---|
| force optional | boolean | Bypass the cached feed and refetch. |
{"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
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
/dashboard/notifications/{id}/hide
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
Storage Exposure Probe
/dashboard/security/exposure-probe
{"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
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
/dashboard/widgets
{"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
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
/dashboard/layout
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). |
{"preset": "custom", "widgets": [{"id": "core.stats", "visible": true, "size": "lg", "order": 10}, {"id": "core.news-feed", "visible": false, "order": 20}]}
{"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
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
/dashboard/site-layout
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). |
{"preset": "custom", "widgets": [{"id": "core.news-feed", "visible": false}, {"id": "core.stats", "size": "xl", "order": 10}]}
{"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
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.