Query reference
One endpoint answers nearly every question:
What it returns depends on dimensions. Leave it out and you get totals. Ask for a time dimension and you get a series. Ask for anything else and you get a ranked breakdown.
Three shapes
dimensions | Shape | Rows |
|---|---|---|
| Omitted | Aggregate | One, no dimensions key |
| One time dimension | Time series | One per bucket, oldest first |
| One other dimension | Breakdown | Top N, most visitors first, plus meta |
At most one dimension per query. More returns 400 too_many_dimensions.
Request body
{
"site_id": 3093477,
"metrics": ["visitors", "pageviews"],
"date_range": "7d",
"dimensions": ["time:day"],
"filters": [
{ "field": "country", "operator": "is", "values": ["US"] }
],
"limit": 100,
"offset": 0
}
| Field | Type | Required | Notes |
|---|---|---|---|
site_id | integer | Conditional | Required on an all-sites key. Optional on a single-site key |
metrics | string[] | Yes | At least one |
date_range | string or [string, string] | Yes | Preset, Nd, or an explicit pair |
dimensions | string[] | No | Zero or one element |
filters | object[] | No | Narrow the query |
limit | integer | No | Breakdowns only. Default 100, clamped to 1 to 1000 |
offset | integer | No | Breakdowns only. Default 0 |
Sending limit or offset on an aggregate or time series returns 400 limit_offset_misuse.
Date ranges
| Value | Window |
|---|---|
"7d" | Last 7 days |
"30d" | Last 30 days |
"14d", any "Nd" | Last N days, 1 to 90 |
"month" | Current month to date |
"realtime" | Last 30 minutes or so. Pair with time:minute |
["2026-06-01","2026-06-30"] | Explicit range, inclusive, up to 366 days |
Anything else is 400 invalid date_range. For a window longer than 90 days, use an explicit pair.
Windows follow the site's own timezone. The response echoes the range it actually used as concrete dates, so you never have to reproduce the calendar math yourself.
Comparing periods
Add compare to get the previous value and the percent change alongside each number:
"compare": "previous_period"uses the equal window immediately beforedate_range."compare": ["2026-05-01","2026-05-31"]uses a window you name.
An explicit compare range must be the same length as date_range, or the request is 400 compare_length_mismatch.
Compare works on all three shapes. Aggregates get a compare block per metric, time series get one per bucket, breakdowns get one per row. The per-row case is the valuable one: each row is compared against its own true previous value, even if it ranked differently then, which two separate top-N queries cannot reconstruct.
Not emitted on the event:status_code and event:goal breakdowns. The request still succeeds and the echo still shows the compared range, so check for the compare key rather than expecting an error.
Metrics
| Metric | Meaning |
|---|---|
visitors | Unique visitors |
pageviews | Total pageviews |
visits | Sessions |
visit_duration | Average visit length in seconds |
bounce_rate | Percent |
views_per_visit | Pageviews per visit |
engagement_time | Average active seconds, not the same as visit_duration |
events | Raw event count. Breakdowns only |
conversion_rate | Converters as a percent of visitors. event:goal only |
time_on_page | Average seconds on a page. Breakdowns only |
scroll_depth | Average furthest scroll, 0 to 100. Breakdowns only |
exit_rate | Exits as a percent of pageviews. visit:exit_page only |
The first seven work on aggregates and time series. The last five exist only in breakdowns, and only on dimensions that compute them. Ask for one elsewhere and the request fails with 400 rather than returning a null column.
Dimensions
Time
| Dimension | Bucket |
|---|---|
time | Day, alias of time:day |
time:minute | Minute. Realtime only |
time:hour | Hour |
time:day | Day |
time:week | Week |
time:month | Month |
The bucket has to suit the range. Hourly buckets across a long custom range return 400 dimension not valid for date_range.
Breakdowns
Each dimension computes only the metrics listed:
| Dimension | Metrics |
|---|---|
visit:source, visit:referrer, visit:channel | visitors, visit_duration, bounce_rate |
visit:utm_source, visit:utm_medium, visit:utm_campaign, visit:utm_content, visit:utm_term | visitors, visit_duration, bounce_rate |
visit:country, visit:region, visit:city | visitors, visit_duration, bounce_rate |
visit:browser, visit:browser_version, visit:os, visit:os_version, visit:device | visitors, visit_duration, bounce_rate |
visit:entry_page | visitors, visits, visit_duration |
visit:exit_page | visitors, visits, exit_rate |
event:page, event:folder | visitors, pageviews, bounce_rate, time_on_page, scroll_depth |
event:hostname | visitors, visits, pageviews, visit_duration, bounce_rate, views_per_visit |
event:status_code | pageviews |
event:name | visitors, events |
event:props:<key> | visitors, events |
event:goal | visitors, events, conversion_rate |
There is no property dimension beyond event:props:<key>, and no dimension outside this list.
event:name lists custom events fired with data-statable-event. Automatic pageview and engagement events are excluded.
event:folder groups pages by first path segment, matching the Folders report.
event:status_code ranks HTTP status codes, value as a string such as "404".
event:goal ranks the site's configured goals. visitors is unique converters, events is total conversions, conversion_rate is converters as a percent of all visitors in the window. Add a filter to measure conversions inside a segment.
event:props:<key> is dynamic. Replace <key> with a property name to rank that property's values, (none) where unset. It belongs to a single custom event, so it requires an event filter naming that event, otherwise 400 event_filter_required:
{
"site_id": 3093477,
"metrics": ["visitors", "events"],
"date_range": "30d",
"dimensions": ["event:props:plan"],
"filters": [{ "field": "event", "operator": "is", "values": ["Signup"] }]
}
Discover which keys a site records with GET /api/v1/props.
Geo values
visit:country, visit:region and visit:city return a code as the dimension value and the readable name in a sibling labels map. Country is ISO alpha-2, city is a numeric geoname id.
The code is exactly what the matching filter accepts, so a breakdown value drops straight into the next query. No other breakdown carries labels, because its value is already both.
Filters
Fields: browser, browser_version, os, os_version, device, country, city, region, hostname, utm_source, utm_medium, utm_campaign, utm_content, utm_term, entry_page, exit_page, source, channel, referrer, page, code, event.
Note the field name drops the visit: or event: prefix the dimension carries.
Operators: is, is_not, contains, does_not_contain.
Values: at least one, OR'd together. A value may not contain a comma.
Geo fields take codes, never display names. country wants "US", city wants a geoname id like "5128581". Run the matching breakdown first to collect them.
The event filter is special. It restricts the whole query to sessions in which that custom event fired, so a sources breakdown filtered by event answers "where did the people who signed up come from". It accepts only is, only one value, and rejects pageview and engagement.
Filters on arbitrary custom properties are not supported.
Responses
Every response is {"results": [...], "query": {...}}. The query block echoes what the server resolved, including date_range as concrete dates. A top-level has_import flag is true when the window overlaps GA4-imported data, meaning the numbers blend imported and native traffic.
Aggregate:
{
"results": [
{ "metrics": { "visitors": 1234, "pageviews": 5678 } }
],
"query": {
"site_id": 3093477,
"metrics": ["visitors", "pageviews"],
"date_range": ["2026-07-17", "2026-07-23"]
}
}
Time series, oldest bucket first. Day, week and month labels are YYYY-MM-DD; hour labels are YYYY-MM-DD HH:00.
{
"results": [
{ "dimensions": { "time:day": "2026-07-14" }, "metrics": { "visitors": 210 } },
{ "dimensions": { "time:day": "2026-07-15" }, "metrics": { "visitors": 198 } }
]
}
Breakdown, with meta:
{
"results": [
{ "dimensions": { "visit:country": "US" }, "labels": { "visit:country": "United States" }, "metrics": { "visitors": 500 } },
{ "dimensions": { "visit:country": "DE" }, "labels": { "visit:country": "Germany" }, "metrics": { "visitors": 180 } }
],
"meta": { "total": 57, "limit": 100, "offset": 0, "has_more": false }
}
meta | Meaning |
|---|---|
total | Distinct values in the window, across all pages |
limit | The limit actually applied, after clamping |
offset | The offset actually applied |
has_more | More rows exist. Page on with offset += limit |
meta appears on breakdowns only.
Examples
Totals for last week:
curl -s -X POST https://statable.com/api/v1/query \
-H "Authorization: Bearer stbl_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id":3093477,"metrics":["visitors","pageviews"],"date_range":"7d"}'
Daily visitors for one country, over a named month:
curl -s -X POST https://statable.com/api/v1/query \
-H "Authorization: Bearer stbl_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"site_id": 3093477,
"metrics": ["visitors"],
"date_range": ["2026-06-01","2026-06-30"],
"dimensions": ["time:day"],
"filters": [{"field":"country","operator":"is","values":["US"]}]
}'
Top 10 pages this month, with last month beside them:
curl -s -X POST https://statable.com/api/v1/query \
-H "Authorization: Bearer stbl_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"site_id": 3093477,
"metrics": ["visitors","pageviews","time_on_page"],
"date_range": "month",
"dimensions": ["event:page"],
"compare": "previous_period",
"limit": 10
}'
Next steps
- Endpoints: sites, realtime, properties, funnels
- Errors: what each
400is telling you - Rate limits: fetching more per request
Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.