GraphQL API Pro
Two queries, behind a schema component of their own.
Turn it on first
The queries don't exist until a schema grants them. Not "exist and return an error" - the field genuinely isn't on the schema, so an unauthorised token gets Cannot query field "craftAnalyticsTotals" on type "Query".
The numbers expose nobody, but they're your business rather than the internet's, so the default is no.
Settings → GraphQL → Schemas, pick a schema, and tick Analytics → Read traffic reports. In project config that's:
scope:
- 'craftAnalytics.read:read'
- 'sites.054f4ae9-…:read'You need the site scope too, and it is enforced per query: a schema may only read analytics for sites it has sites.<uid>:read on. Asking for a site it doesn't have returns Analytics for that site are not in this schema - the same answer a site that doesn't exist gets, so the endpoint can't be used to enumerate them.
craftAnalyticsTotals
{
craftAnalyticsTotals(period: "30d") {
views
uniques
sessions
bounceRate
avgDurationMs
avgViewsPerSession
}
}{
"data": {
"craftAnalyticsTotals": {
"views": 4812,
"uniques": 1203,
"sessions": 1580,
"bounceRate": 45.61
}
}
}Arguments: siteId (must be a site this schema can read; defaults to the primary site, or the first readable one), period (today, yesterday, 7d, 30d, 90d, 12mo - defaults to 30d). period also takes an absolute window as two dates, "2026-01-01:2026-03-31", spanning at most 400 days. Anything it cannot read is treated as 30d rather than erroring.
craftAnalyticsTopPages
{
craftAnalyticsTopPages(period: "7d", limit: 10) {
path
views
elementId
entrances
exits
bounceRate
avgDwellMs
}
}limit is capped at 200. elementId is null for pages that aren't entries - a template-only route, say - which is exactly the hook you need to join this to an entries query in the same request.
Read the field descriptions
They carry the caveats, and the caveats matter:
uniquesis on a daily-unique basis and estimated to about ±1.6%. If you're piping this into a dashboard that someone will make decisions from, read how visitors are counted first.avgDwellMsis always 0 in server-only tracking mode, because nothing on the server knows when someone left.pathcan be__other__, which is where values past the daily cardinality cap are folded. No views are lost to it, but it isn't a page you can visit.
They're in the schema itself, so anything that introspects - Insomnia, Altair, your codegen - shows them without anyone reading this page.