Stats API
Query visitors, sources, pages, goals and revenue for any of your sites, with the same query model the dashboard uses.
The Stats API answers the same questions as the dashboard, from your own code: a monthly report, a number on your admin screen, or a sheet that fills itself. It needs a key with the Read stats permission (stats:read); see API keys.
Run a query
POST https://api.traffic.cx/v1/query
bash
curl -X POST https://api.traffic.cx/v1/query \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"site": "tcx_yoursite",
"date_range": "30d",
"metrics": ["visitors", "conversion_rate", "revenue"],
"dimensions": ["visit:channel"],
"order_by": [["visitors", "desc"]],
"pagination": { "limit": 10 }
}'json
{
"results": [
{ "dimensions": ["Organic Search"], "metrics": [5120, 2.4, 3480] },
{ "dimensions": ["Direct"], "metrics": [2210, 1.1, 940] }
],
"meta": {
"site": "tcx_yoursite",
"timezone": "Europe/Berlin",
"currency": "USD",
"date_range": { "first_day": "2026-09-17", "last_day": "2026-10-16", "from": "…", "to": "…" },
"sampled": false,
"query_ms": 41
}
}Each row lists its dimension values and then its metric values, in the order you asked for them. A value that can't be computed, such as a rate with nothing to divide by, is null. Every answer is exact: sampled is always false.
Query fields
| Field | Required | Meaning |
|---|---|---|
site | yes | The site's ID (tcx_…, in Settings → General) |
date_range | yes | A preset, or ["2026-09-01", "2026-09-30"] for the first and last day in the site's timezone |
metrics | yes | One or more metrics, below |
dimensions | no | Up to 3, to break the numbers down; none gives one total row |
filters | no | [operator, dimension, values] triples, all of which must match |
order_by | no | [[field, "asc" or "desc"]]. Default: the first metric, descending (time ascending) |
include | no | comparison ("previous_period", "year_over_year" or a custom range), match_day_of_week, total_rows |
pagination | no | limit (up to 1,000) and offset |
attribution | no | Which visit revenue credits: "last_non_direct" (default) or "first_touch" |
Date presets: realtime (last 30 minutes), today, yesterday, 7d, 28d, 30d, 91d, month (to date), last_month, year (to date), 12mo, all.
Metrics
- Traffic:
visitors,visits,pageviews,views_per_visit,bounce_rate,visit_duration - Events and goals:
events,conversions,conversion_rate - Pages:
time_on_page,scroll_depth,exit_rate - Revenue:
revenue(net of refunds),refunds,customers,purchase_rate,rpv(revenue per visitor),aov(average order value),ltv - Share:
percentage, each row's share of the total
Revenue amounts are in the site's reporting currency, named in meta.currency.
Dimensions
| Kind | Dimensions |
|---|---|
| Visit | visit:entry_page, visit:exit_page, visit:source, visit:channel, visit:referrer, visit:utm_source, visit:utm_medium, visit:utm_campaign, visit:utm_content, visit:utm_term, visit:click_id, visit:country, visit:region, visit:city, visit:browser, visit:browser_version, visit:os, visit:os_version, visit:device |
| Event | event:page, event:hostname, event:goal, event:props:<key> for a custom property |
| Time | time:minute, time:hour, time:day, time:week, time:month, bucketed in the site's timezone |
Revenue metrics combine with the visit dimensions, event:goal and time. A query that mixes them with other dimensions answers 400 and says which one.
Filters
json
"filters": [
["is", "visit:country", ["DE", "AT"]],
["contains", "event:page", ["/blog"]]
]Operators are is, is_not, contains, contains_not, matches and matches_not (a regular expression, up to 256 characters). A filter matches when any of its values matches, and the _not forms when none does. In page values, * matches one path segment and ** any depth.
A visit filter selects visits. An event filter selects events: visitors and visits then count those with at least one matching event, so ["is", "event:page", ["/pricing"]] reads as "visits that viewed /pricing".
Several queries at once
POST /v1/query/batch takes { "queries": [ … ] } with up to 12 queries and answers { "results": [ … ] } in the same order. A query that fails has error and status in its place, and the others still answer. A batch counts as one request towards the hourly limit.
Other reads
| Request | Answers |
|---|---|
GET /v1/sites | The team's sites, with the first and last day that has data |
GET /v1/sites/{site} | One site's settings |
GET /v1/sites/{site}/realtime | Visitors in the last 5 minutes and the latest events (?limit= up to 50) |
GET /v1/sites/{site}/goals | The site's goals |
GET /v1/sites/{site}/funnels | Saved funnels |
POST /v1/sites/{site}/funnels/{id}/run | A saved funnel's steps for date_range and filters |
GET /v1/sites/{site}/annotations | Chart notes, optionally ?from= and ?to= |
Verify and troubleshoot
- Create a key with only Read stats, then call
GET /v1/sitesand check that your site is listed. - Run a
30dquery forvisitorsand compare it with the dashboard's Visitors for the last 30 days. - Add
"dimensions": ["visit:channel"]and check the rows against the Sources panel.
For 400, read path: it names the metric, dimension or filter at fault. For 404, check the site ID and that the site belongs to the key's team. For 402, the team needs a plan before its numbers can be read.
The routes and permission checks are covered by local tests. No request in this guide has been sent to a real customer account as part of publication.