Read API
Fetch your statistics programmatically: internal dashboards, reports, exports. Restricted to the Pro and Agency plans (and to active trials).
Authentication
Create a token in Dashboard → API tokens, then pass it as a Bearer:
curl -H "Authorization: Bearer YOUR_TOKEN" \ "https://quietmetrics.dev/api/v1/stats/qm_pub_XXXXXXXXXXXXXXXXX/summary?period=30d"
All routes are prefixed with /api/v1/stats/{site_public_key}. A token only grants access to the sites on your account; a site that does not belong to you returns 404 (no existence leak).
Endpoints
| Endpoint | Content |
|---|---|
GET /summary |
KPIs for the period + previous period (comparison) |
GET /timeseries |
Time series, one point per day (chart) |
GET /breakdown |
Top values for a dimension ({label, value}) |
GET /realtime |
Active visitors over the last 5 minutes ({"visitors": N}) |
Common parameters
period accepts the same presets as the dashboard: today, yesterday, 7d, 30d (default), month, last_month, 12m. Dates are computed in the site's timezone. An unknown preset falls back to 30d.
filters takes the same filters as dashboard clicks, in dimension:value format, separated by ;:
?filters=country:FR;device:mobile ?filters=page:/pricing ?filters=channel:organic;browser:Firefox
dimension (breakdown): page, entry (entry pages), referrer, channel, campaign, country, region, browser, os, device, lang, event. Unknown dimension → 422.
limit (breakdown): number of rows, 10 by default, 1,000 maximum.
Response examples
GET /summary?period=30d:
{ "site": "mysite.com", "period": { "preset": "30d", "from": "2026-06-16", "to": "2026-07-16" }, "filters": {}, "kpis": { "visitors": 4210, "pageviews": 9873, "bounce_rate": 0.47, "avg_duration": 74 }, "previous": { "visitors": 3877, "pageviews": 9145, "bounce_rate": 0.49, "avg_duration": 71 } }
GET /breakdown?dimension=page&limit=3:
{ "dimension": "page", "period": { "from": "2026-06-16", "to": "2026-07-16" }, "data": [ { "label": "/", "value": 3120 }, { "label": "/pricing", "value": 1284 }, { "label": "/docs", "value": 977 } ], "capped": { "any": true, "dimensions": [ { "dimension": "page", "days": 2, "days_repaired": 2, "folded_values": 2812, "cap": 2000 }, { "dimension": "referrer", "days": 1, "days_repaired": 0, "folded_values": null, "cap": 500 } ] } }
Cardinality cap (capped)
Each site measures a bounded number of distinct values per day and per dimension. Beyond that, further values are counted together under a single label: the detail is grouped, no visit is lost and the totals remain exact. breakdown reports this state in the capped key:
anyis always present anddimensionsis always a list: the schema does not vary with the state of the site, so no branching is needed to read it.dimensionis the capped dimension, not the one you asked for.entryandexitare grouped by thepagecap;sourceandmediumare grouped by thecampaigncap.dayscounts the capped local days in the period;capis the highest threshold actually applied (it can vary if a raise is applied mid-period).folded_values: nulldoes NOT mean zero. The number of grouped values is only known after the nightly rebuild, which processes a day no earlier than D+2.nullmeans "cap reached, count not known yet";0means "day rebuilt, no value grouped". Showing0instead ofnullwould tell your users they are losing no detail, which is false.days_repairedsays on how many of thedaysdays the count is known. Ifdays_repaired < days,folded_valuesis a partial sum.
summary and timeseries do not carry this key: they read no dimension and are insensitive to capping.
The dashboard CSV export carries the same indicator, but in the X-Cardinality-Capped HTTP header rather than in the file body: {"capped":false}, or {"capped":true,"dimension":"page","days":1,"days_repaired":1,"folded_values":2812,"cap":2000} scoped to the exported dimension only. The CSV body stays pure machine data, with no metadata row: no synthetic row can skew a total or break a fixed-arity parser.
This cap can be raised for your site: write to support.
PHP example
$response = json_decode(file_get_contents( 'https://quietmetrics.dev/api/v1/stats/qm_pub_XXXX/summary?period=7d', false, stream_context_create(['http' => ['header' => "Authorization: Bearer YOUR_TOKEN\r\nAccept: application/json"]]) ), true); echo $response['kpis']['visitors'];
Errors
| Code | Meaning |
|---|---|
401 |
Token missing, invalid or revoked |
403 |
The site is yours but your plan does not include the API (or the trial has expired) |
404 |
Unknown site, or one belonging to another account |
422 |
Unknown breakdown dimension |
Best practices
- Cache on your side: querying once an hour is more than enough. Be aware, though, that a closed day is not frozen for good: the nightly rebuild revisits capped days and can recompute their aggregates up to two days later. So plan for a short cache, or a re-query, on the last two days; beyond that, the values no longer move.
- One token per use case (one for the internal dashboard, one for the monthly report): revocation stays surgical.
- For a one-off export, every dashboard view can also be exported as CSV from the dashboard; no need to write code.
Can't find your answer?
Support replies on business days.