Skip to content

Stats API

Query visitors, sources, pages, goals and revenue for any of your sites, with the same query model the dashboard uses.

Updated 14 October 20264 min read

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

FieldRequiredMeaning
siteyesThe site's ID (tcx_…, in Settings → General)
date_rangeyesA preset, or ["2026-09-01", "2026-09-30"] for the first and last day in the site's timezone
metricsyesOne or more metrics, below
dimensionsnoUp to 3, to break the numbers down; none gives one total row
filtersno[operator, dimension, values] triples, all of which must match
order_byno[[field, "asc" or "desc"]]. Default: the first metric, descending (time ascending)
includenocomparison ("previous_period", "year_over_year" or a custom range), match_day_of_week, total_rows
paginationnolimit (up to 1,000) and offset
attributionnoWhich 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

KindDimensions
Visitvisit: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
Eventevent:page, event:hostname, event:goal, event:props:<key> for a custom property
Timetime: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

RequestAnswers
GET /v1/sitesThe team's sites, with the first and last day that has data
GET /v1/sites/{site}One site's settings
GET /v1/sites/{site}/realtimeVisitors in the last 5 minutes and the latest events (?limit= up to 50)
GET /v1/sites/{site}/goalsThe site's goals
GET /v1/sites/{site}/funnelsSaved funnels
POST /v1/sites/{site}/funnels/{id}/runA saved funnel's steps for date_range and filters
GET /v1/sites/{site}/annotationsChart notes, optionally ?from= and ?to=

Verify and troubleshoot

  1. Create a key with only Read stats, then call GET /v1/sites and check that your site is listed.
  2. Run a 30d query for visitors and compare it with the dashboard's Visitors for the last 30 days.
  3. 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.