Stats API
One query envelope, every read endpoint, and the response shape they all share.
Every analytics read takes the same query string and returns the same envelope. Learn it once.
curl "https://analytics.example.com/api/stats/overview?site=1&range=30d&compare=previous" \
-H "Authorization: Bearer mfg_…"
The query envelope
| Parameter | Values |
|---|---|
site |
Required. The numeric site id. |
start, end |
ISO-8601 instants. |
range |
today, yesterday, 24h, 7d, 30d, 90d, 12mo, mtd, ytd, all. |
compare |
none, previous, year. Default none. |
tz |
IANA name. Defaults to the site’s timezone. |
granularity |
minute, hour, day, week, month. Chosen for you when absent. |
filters |
A JSON array or group. See filters. |
segment |
A saved segment id, merged into filters. |
audience |
human, agent, all. Default human. |
limit, page |
Pagination, on the list endpoints. |
audience is the parameter that makes this product what it is. The same question, the same
window, the same filters, asked about the other readership.
The response
{
"data": { "visitors": 1284, "sessions": 1502, "pageviews": 3310 },
"meta": { "query": {}, "rows": 3, "sampled": false, "took_ms": 18 }
}
data is the payload. meta says what was asked, how many rows came back, whether the
answer was sampled, and how long it took.
Errors carry the matching HTTP status:
{ "error": { "code": "validation_error", "message": "range is not a known window", "field": "range" } }
The code is snake_case and stable: not_found, unauthorized, forbidden,
conflict, validation_error, rate_limited, bad_request, database_error,
upstream_error, internal_error. Branch on it, never on the message.
The endpoints
Totals and time
GET /api/stats/overview visitors, sessions, pageviews, bounce, duration, views/session
GET /api/stats/timeseries one metric in buckets, plus the comparison series
GET /api/stats/realtime the last five minutes: live people, live agents, open pages
Content and acquisition
GET /api/stats/pages entries, exits, scroll depth, time on page
GET /api/stats/sources channels, then sources, then campaigns
GET /api/stats/locations country, region, city
GET /api/stats/devices browser, OS, device type, screen class
GET /api/stats/breakdown ranked rows for any dimension: ?dimension=…
Behaviour
GET /api/stats/events event names, counts and property breakdowns
GET /api/stats/goals conversions, rate and revenue per goal
GET /api/stats/funnel/:id step counts, drop-off, the sessions per step
GET /api/stats/journeys nodes and links: ?depth=4&start=/docs
GET /api/stats/retention the cohort grid: ?granularity=week
GET /api/stats/sessions the session list, and /:id for one session
GET /api/stats/visitors visitors, traits and session counts
Health
GET /api/stats/performance LCP, CLS, INP, FCP, TTFB at p50, p75, p95
GET /api/stats/errors grouped by fingerprint, with a sparkline
Machines
GET /api/agents/overview fetches, unique agents, verified share, bytes, top operators
GET /api/agents/breakdown ?dimension=agent_id|operator|purpose|pathname|status
GET /api/agents/timeseries crawl volume over time, per agent
GET /api/agents/coverage per URL: first crawl, last crawl, freshness gap, agents
GET /api/agents/catalog the known-agent catalog, with each operator's own docs link
GET /api/citation-gap per URL: fetches, arrivals, ratio, verdict
GET /api/citation-gap/summary
GET /api/policy robots.txt and llms.txt as parsed, per agent
GET /api/policy/violations fetches of a path the site disallowed
GET /api/content/decay human traffic fell, agent traffic held
/api/stats/export.csv and /api/stats/export.pdf take the same envelope and return the
same data as a file.
Worked examples
Which operators crawled the most last month:
curl "https://analytics.example.com/api/stats/breakdown?site=1&range=30d&audience=agent&dimension=operator" \
-H "Authorization: Bearer mfg_…"
Answer-engine arrivals on the documentation, week by week:
curl -G "https://analytics.example.com/api/stats/timeseries" \
-H "Authorization: Bearer mfg_…" \
--data-urlencode "site=1" \
--data-urlencode "range=90d" \
--data-urlencode "granularity=week" \
--data-urlencode 'filters=[{"dimension":"channel","op":"is","value":"answer_engine"},{"dimension":"pathname","op":"starts_with","value":"/docs"}]'
The pages being taken with nothing coming back:
curl "https://analytics.example.com/api/citation-gap?site=1&range=30d" \
-H "Authorization: Bearer mfg_…"
Practical notes
- Both audiences, one call, is not a thing.
audience=allsums where summing is honest and returns nothing where it is not: a crawler has no bounce rate, and a zero there would read as a measurement. - Comparisons run the query twice with everything else identical, so a delta can only be a change in the data.
- Absent is not zero. A metric that cannot be computed comes back absent. Render it as absent.
- Bulk belongs in the export. For a full table, use
export.csvrather than paging a breakdown ten thousand rows at a time.