Every read endpoint parses the same window, the same filters and the same paging. They are described once here rather than repeated on two dozen reference pages.
Each endpoint's own page lists the parameters it adds on top, and notes where it deliberately ignores one of these.
Choosing a window
Two ways, and passing both is a 400 — which one wins would be a guess you
did not make.
| Parameter | Contract |
period | A named window: today, yesterday, last24h, last7d, last30d, last12m, month, year, all. Resolved in tz. |
from, to | ISO 8601 with an offset. Must be given together — one alone is a 400. |
tz | IANA timezone for day boundaries. Defaults to UTC. |
tz is validated as a real zone, so Mars/Olympus is a 400 naming the
problem rather than a failure further in. Aliases work: Asia/Calcutta is
accepted alongside Asia/Kolkata.
period=today&tz=Europe/Istanbul means your today, not the server's. Pass
the zone your reader is in — a site's own timezone is on
its settings, and the API does not apply it
for you.
Where the buckets fall
/timeseries and /heatmap cut their buckets in tz. The others that bucket
— crawler series and
engagement — resolve the window in tz
but cut at UTC boundaries, and say so on their own pages.
The live endpoints take no window at all. They take a lookback in minutes;
see Online now.
Filters
Flat query parameters, one per dimension — 21 of them:
filter_path filter_path_pattern filter_hostname
filter_entry_path filter_exit_link filter_file_download
filter_goal filter_browser filter_os
filter_device filter_country filter_region
filter_city filter_referrer filter_channel
filter_utm_source filter_utm_medium filter_utm_campaign
filter_gad_campaign_id filter_utm_term filter_bot_kindEquality only. There is no is_not, no contains and no numeric
comparison. The one exception is filter_path_pattern, which takes * as a
wildcard:
?filter_path_pattern=/blog/*Unknown query parameters are ignored, not refused — a query string
collects analytics tags, cache-busters and proxy additions that have nothing
to do with us. A misspelled filter_ name therefore does nothing silently;
check the spelling against the list above if a filter seems to have no effect.
Two endpoints ignore filters entirely and explain why on their own pages: channel quality compares channels, so narrowing to one leaves nothing to compare, and engagement reads sessions rather than events.
Crawler traffic
bots=excluded is the default everywhere. A report about people that quietly
counts machines is wrong.
The AI endpoints ignore the parameter and always include crawlers — there the machines are the report.
Retention clamping
If your window reaches further back than your plan retains, you get the data that exists and the response says so — not an error, and not a silent truncation:
"meta": { "range": { "from": "…", "to": "…", "clamped": true, "tz": "UTC" } }A caller that plots the result without reading clamped draws a chart missing
the years it asked for, with nothing in the response to indicate it. It is the
difference between a short answer and a wrong one.
Your retention window is on usage and, for the figure actually applied to a given site's reads, on its settings.
The response envelope
One shape, on every endpoint:
{
"data": { },
"meta": {
"site_id": "6fed8980-…",
"range": { "from": "…", "to": "…", "clamped": false, "tz": "UTC" },
"page": { "limit": 100, "offset": 0, "has_more": true }
}
}meta.range appears on endpoints that take a window, meta.page on those that
return rows, and meta.minutes on the live ones. Nothing carries a field it
cannot fill.
Adding a field to data is not a breaking change. Read the fields you need
and ignore the rest.
Errors use one shape too, with a symbolic code rather than a repeated HTTP
status — see Errors.
Paging
limit (default 100, maximum 1000) and offset. There is no cursor.
meta.page carries has_more and not a total. Counting every matching row
costs as much as the query itself on a wide window, and the number is stale by
the time you read it on a live dataset. has_more is measured rather than
guessed: the query asks for one row more than you did and reports whether it
came back.
One endpoint ignores paging and returns everything: traffic split. In a ratio, a row cut off at the bottom does not shorten the answer — it silently changes every percentage above it.
What no endpoint will do
| Why | |
/visitors, /visitors/{id} | Needs cross-day person identity TrueStat is built without. Not deferred — not this product. |
| Write a pageview | Use the tracking script. Conversions are the exception: POST /goals. |
| Post a payment | Revenue comes from the payment provider and reconciles against it. Connect one with the integrations endpoints. |
| Transfer ownership | An account changes hands deliberately, in the dashboard. |
| Mint an API key | A credential that issues credentials turns one leak into permanent access. Dashboard only. |
| Delete a site | Irreversible, and dashboard only. |
| Return any secret | Site keys, integration credentials and key plaintexts are never in a response. |
A fields projection | Rows are a handful of columns. It would buy nothing. |