Query API
Beta — subject to change
The Query API is available during the Early Adopter Program and may change without notice. It is not yet covered by versioning or deprecation guarantees.
Three read-only resources answer the three questions the dashboards answer. All share the same request shape and the same response shape.
| Resource | Endpoint | Answers |
|---|---|---|
| Events | GET /api/v1/analytics/events | How many times did X happen? |
| Sessions | GET /api/v1/analytics/sessions | How engaged are visitors? |
| Content | GET /api/v1/analytics/content | Which content performs best, or drives conversions? |
Authentication is not the Site Auth
These endpoints require an authenticated dotCMS back-end user with READ permission on the site being queried. The Site Auth is only for sending events, never for querying them.
How a Query Is Built#
Every request combines three kinds of parameter. Getting these three straight is most of what you need to know, because all three resources work the same way.
| Kind | What it does | Examples |
|---|---|---|
| Metric | The number you want measured — a count, a rate or an average. Always numeric, and the only thing that appears in totals. | totalEvents, uniqueVisitors, engagementRate |
| Dimension | How to group the metric. Always a label, never summed. Adds one row per distinct value. | day, device, browser |
| Filter | Narrows which rows are counted at all, before any grouping. Never appears as a column. | siteId, eventType, conversionName |
Think of it as a pivot table: dimensions are the row labels, metrics are the numeric columns. Request no dimensions and everything collapses into a single row — the total for the whole window, which is what a KPI tile needs. Request one or more and you get a row per combination, which is what a chart or table needs.
Filter or dimension? The same word can be both
eventType=pageview as a filter restricts the query to page views, so every number returned counts only those. dimensions=eventType does the opposite — it keeps every event type in scope and adds a column breaking the results out by type, so you can compare them side by side.
Response Shape#
Responses come wrapped in the standard dotCMS envelope. The query result is under entity:
{
"entity": {
"params": { "from": "2026-08-06", "to": "2026-08-12", "project": "demo", … },
"columns": [ { "name": "day", "type": "DIMENSION" },
{ "name": "totalEvents", "type": "METRIC" } ],
"rows": [ { "day": "2026-08-06", "totalEvents": 1240 } ],
"totals": { "totalEvents": 9423 },
"pagination": { "page": 1, "pageSize": 20, "totalPages": 3, "totalItems": 60 }
},
"errors": [], "messages": [], "pagination": null, "permissions": []
}| Field | Notes |
|---|---|
params | Echoes the request as it actually ran, with range resolved to absolute from/to. Parameters you did not send are omitted rather than returned as null. |
columns | The schema of each row, in order, each tagged DIMENSION or METRIC. Build your table headers and chart axes from this instead of hardcoding field names. |
rows | Always an array. One entry for a scalar query, one per combination for a grouped one, and an empty array — not an error — when there is no data in the window. |
totals | Present only when you requested at least one dimension. For a scalar query rows[0] already is the total. Rates and averages here are recomputed from the underlying sums across the whole window, never averaged from the per-row rates. |
pagination | Present only when you send page or pageSize. |
Read pagination from inside entity
The envelope has its own top-level pagination field which is always null for these endpoints. The real pagination block is entity.pagination.
Common Parameters#
| Parameter | Required | Notes |
|---|---|---|
siteId | No | Restricts to one site. Falls back to the current site. |
from / to | No* | Absolute window, YYYY-MM-DD. to may not be before from. |
range | No* | Relative window, last_N_days. Resolves to the N complete days ending yesterday — today is always excluded. Capped at 90 days. |
metrics | No | Comma-separated. Each resource has its own default set. |
dimensions | No | Comma-separated. Omit for a whole-window scalar. |
orderBy / orderDir | No | Sort by any requested metric or dimension; asc or desc. |
page / pageSize | No | Sending either one activates pagination. |
project | No | Injected automatically from the instance configuration when omitted. |
*Send exactly one of from+to or range. Sending both, or half of one, returns 400.
Events#
Answers "how many times did this happen." The three metrics fall into two groups, and a metric's group decides which dimensions you may group it by:
| Metric group | Metrics | Dimensions you can pair with it |
|---|---|---|
| Event totals | totalEvents, uniqueVisitors | day, month, eventType |
| Pageview breakdown | pageviews | day, month, device, browser |
Grouping by device or browser is only possible with pageviews; grouping by event type is only possible with the other two, since a page view is already a single event type.
| Metric | Meaning | Group |
|---|---|---|
totalEvents (default) | Count of all matching events. | Event totals |
uniqueVisitors | Distinct visitor count. | Event totals |
pageviews | Count of page-view events. | Pageview breakdown |
| Dimension | Meaning | Valid with |
|---|---|---|
day | Calendar-day bucket. | any metric |
month | Calendar-month bucket. | any metric — mutually exclusive with day |
eventType | Group by event type. | totalEvents, uniqueVisitors |
device | Desktop / Mobile / Tablet / Other. | pageviews only |
browser | Chrome / Safari / Firefox / Edge / Other. | pageviews only |
Events is the only resource that accepts more than one dimension per request. You can also request several metrics at once, as long as every dimension you ask for is valid for all of them: metrics=totalEvents,uniqueVisitors&dimensions=day works, but metrics=totalEvents,pageviews&dimensions=device returns 400, because device pairs only with pageviews. The eventType filter is rejected outright alongside pageviews, which is already a single event type.
Example: Daily Trend#
curl -s "$BASE_URL/api/v1/analytics/events\
?range=last_14_days&metrics=totalEvents,uniqueVisitors&dimensions=day""entity": {
"columns": [ { "name": "day", "type": "DIMENSION" },
{ "name": "totalEvents", "type": "METRIC" },
{ "name": "uniqueVisitors", "type": "METRIC" } ],
"rows": [ { "day": "2026-07-25", "totalEvents": 3210, "uniqueVisitors": 1180 },
{ "day": "2026-07-26", "totalEvents": 3450, "uniqueVisitors": 1225 } ],
"totals": { "totalEvents": 48213, "uniqueVisitors": 16720 }
}Sessions#
Answers "how engaged are visitors." Eight metrics in two tiers — and the dimension you pick decides how many of them you can ask for:
| If you group by… | Metrics available |
|---|---|
| nothing — a whole-window total | all 8 (Basic + Extended) |
day | all 8 (Basic + Extended) |
device, browser or language | the 4 Basic only |
The whole-window total and the day-by-day breakdown are the two detailed views; the other three are lighter and do not track the Extended metrics at all.
| Metric | Meaning | Tier |
|---|---|---|
totalSessions | Total session count. | Basic |
engagedSessions | Sessions that meet the engagement rule. | Basic |
engagementRate | engagedSessions / totalSessions × 100 | Basic |
avgEngagedSessionTimeSeconds | Average duration of engaged sessions. | Basic |
engagedConversionSessions | Engaged sessions that also converted. | Extended |
conversionRate | engagedConversionSessions / totalSessions × 100 — the site conversion rate. | Extended |
avgInteractionsPerEngagedSession | Average event count per engaged session. | Extended |
avgSessionTimeSeconds | Average duration across all sessions. | Extended |
| Dimension | Meaning | Metrics available |
|---|---|---|
| (none) | Whole-window scalar. | all eight |
day | Calendar-day bucket. | all eight |
device | Device category. | Basic only |
browser | Browser family. | Basic only |
language | Content locale — blank shows as "n/a". | Basic only |
Sessions accepts at most one dimension, and there is no month bucket. Crossing the tiers — an Extended metric with device, browser or language — returns 400.
Omitting metrics follows the same split: all eight for a scalar query or grouped by day, the four Basic ones otherwise. Default sort order varies too — day ascends chronologically, device and browser ascend alphabetically, and language sorts by engagedSessions descending.
Example: Engagement by Device#
curl -s "$BASE_URL/api/v1/analytics/sessions?range=last_30_days&dimensions=device""rows": [
{ "device": "Desktop", "totalSessions": 5210, "engagedSessions": 3312,
"engagementRate": 63.57, "avgEngagedSessionTimeSeconds": 248.1 },
{ "device": "Mobile", "totalSessions": 3450, "engagedSessions": 1890,
"engagementRate": 54.78, "avgEngagedSessionTimeSeconds": 198.4 }
]Content#
Answers "which content performs best." One endpoint, two modes — and which one you get is inferred from the parameters you send rather than chosen explicitly.
| Mode | Ranks content by | Metrics | Dimensions |
|---|---|---|---|
| Top content | Raw event volume | totalEvents only | identifier, title |
| Attribution (default) | How often it preceded a conversion | totalEvents, attributionCount, attributionRate | identifier, title, eventType |
Attribution is the richer view and the default, so you have to narrow deliberately to metrics=totalEvents — with none of the other triggers below — to get the simpler ranking instead.
| If you… | You get |
|---|---|
Omit metrics entirely | Attribution (the default) |
Send metrics=totalEvents and nothing below | Top content |
Request attributionCount or attributionRate | Attribution |
Request the eventType dimension | Attribution |
Send conversionName | Attribution |
attributionCount is the number of conversions the content preceded, and attributionRate is attributionCount / totalEvents × 100. Watch out for totalEvents: it means raw event volume in top-content mode but impressions that preceded a conversion in attribution mode — the same field name, two different numbers.
Omit dimensions and you get identifier,title in top-content mode, identifier,title,eventType in attribution mode.
identifier and title are both a filter and a dimension
As dimensions they group the response by content item, in either mode. As filters they narrow the query to one specific item — and they only take effect in attribution mode. Send identifier=abc-123 alongside metrics=totalEvents, which resolves to top-content mode, and the filter is dropped silently: no error, no indication in the response, and a result set that looks correct but covers all your content.
conversionName restricts results to content that preceded that specific conversion, and selects attribution mode on its own. A request with no page/pageSize returns every matching row, which on a large catalogue can be hundreds — paginate.
Example: Conversion Drivers#
curl -s "$BASE_URL/api/v1/analytics/content\
?range=last_30_days&conversionName=form_submit&orderBy=attributionRate&orderDir=desc&pageSize=20""rows": [
{ "identifier": "abc-123", "title": "Spring Sale Landing Page", "eventType": "pageview",
"totalEvents": 4210, "attributionCount": 512, "attributionRate": 12.16 },
{ "identifier": "def-456", "title": "Product Launch Blog Post", "eventType": "pageview",
"totalEvents": 3185, "attributionCount": 201, "attributionRate": 6.31 }
]Errors#
| Status | When |
|---|---|
400 | Malformed dates, range over 90 days, unsupported metric or dimension, incompatible metric + dimension pair. |
401 | Not authenticated as a dotCMS back-end user. |
403 | SITE_ACCESS_DENIED — the user lacks READ permission on the requested site. |
502 | dotCMS reached the analytics service but could not authenticate to it — a service configuration problem, not a session problem. |