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:

  • any is always present and dimensions is always a list: the schema does not vary with the state of the site, so no branching is needed to read it.
  • dimension is the capped dimension, not the one you asked for. entry and exit are grouped by the page cap; source and medium are grouped by the campaign cap.
  • days counts the capped local days in the period; cap is the highest threshold actually applied (it can vary if a raise is applied mid-period).
  • folded_values: null does NOT mean zero. The number of grouped values is only known after the nightly rebuild, which processes a day no earlier than D+2. null means "cap reached, count not known yet"; 0 means "day rebuilt, no value grouped". Showing 0 instead of null would tell your users they are losing no detail, which is false.
  • days_repaired says on how many of the days days the count is known. If days_repaired < days, folded_values is 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.