The quietstats Stats API
Pull any number from your dashboard into your own tools: spreadsheets, Looker Studio, internal dashboards or scripts. Read-only, simple Bearer keys, still cookie-free.
What you can pull
| Endpoint | Returns | Parameters |
|---|---|---|
GET /sites | Your sites (IDs and domains). | format |
GET /aggregate | Visitors, pageviews, visits, views per visit, bounce rate and visit duration, each with previous value and % change. | site, period, from, to, filters, compare, format |
GET /timeseries | The same metrics per hour, day, week or month, with no gaps. | site, period, from, to, interval, metrics, filters, format |
GET /breakdown | Every value of a property (the full list, not just the top 10) with visitors and pageviews (entry/exit pages: visits, bounce rate, duration). Search, sort and page through it. | site, property, period, from, to, filters, search, sort, limit, page or offset, format |
GET /realtime | Visitors in the last 5 minutes, the pages and countries they are on, and the latest pageviews (page, country, referrer, device; no visitor IDs). | site, format |
Base URL https://quietstats.co.uk/api/v1. Breakdown properties: page entry_page exit_page referrer channel ai utm_source utm_medium utm_campaign country device browser os screen. The channel property includes AI (ChatGPT, Perplexity, Gemini, Grok, Claude, Copilot).
Get a key in 30 seconds
- Create a free account and add your site.
- Open Settings → API keys and press Create read-only key. You can limit a key to one site.
- Copy the key straight away: we only keep a hash of it, so it is shown once. You can revoke it at any time.
Quick start
curl -H "Authorization: Bearer qs_your_key_here" \
"https://quietstats.co.uk/api/v1/aggregate?site=YOUR_SITE_ID&period=30d"const res = await fetch(
"https://quietstats.co.uk/api/v1/timeseries?site=YOUR_SITE_ID&period=7d&interval=day",
{ headers: { Authorization: "Bearer " + process.env.QUIETSTATS_KEY } }
);
if (!res.ok) throw new Error((await res.json()).error.message);
const { results } = await res.json();
console.table(results); // [{ date, visitors, pageviews, visits, ... }]import os, requests
r = requests.get(
"https://quietstats.co.uk/api/v1/breakdown",
params={"site": "YOUR_SITE_ID", "property": "channel", "period": "30d"},
headers={"Authorization": f"Bearer {os.environ['QUIETSTATS_KEY']}"},
timeout=10,
)
r.raise_for_status()
for row in r.json()["results"]:
print(row["value"], row["visitors"])curl -H "Authorization: Bearer qs_your_key_here" \
"https://quietstats.co.uk/api/v1/breakdown?site=YOUR_SITE_ID&property=page&limit=500&format=csv" \
-o top-pages.csvSample responses
GET /aggregate
{
"site": "YOUR_SITE_ID",
"period": { "from": "2026-09-21", "to": "2026-10-20", "days": 30, "timezone": "UTC" },
"compare": { "previous_from": "2026-08-22", "previous_to": "2026-09-20" },
"filters": [],
"results": {
"visitors": { "value": 14210, "previous": 12133, "change": 0.171 },
"pageviews": { "value": 34870, "previous": 29512, "change": 0.182 },
"visits": { "value": 16304, "previous": 13811, "change": 0.181 },
"views_per_visit": { "value": 2.139, "previous": 2.137, "change": 0.001 },
"bounce_rate": { "value": 0.418, "previous": 0.42, "change": -0.005 },
"visit_duration": { "value": 102, "previous": 100, "change": 0.02 }
}
}GET /breakdown?property=channel
{
"site": "YOUR_SITE_ID",
"property": "channel",
"pagination": { "page": 1, "limit": 100, "total": 6, "has_more": false },
"results": [
{ "value": "Direct", "visitors": 5702, "pageviews": 13960 },
{ "value": "Search", "visitors": 4980, "pageviews": 12140 },
{ "value": "Social", "visitors": 1890, "pageviews": 4410 },
{ "value": "AI", "visitors": 640, "pageviews": 1720 }
]
}Parameters
| Parameter | Meaning |
|---|---|
site | Site ID from your Sites page (required except /sites). |
period | today · 7d · 30d (default) · 90d · 12mo · all (all time) · custom |
from, to | YYYY-MM-DD (UTC, inclusive) with period=custom. Up to 400 days; use period=all for everything. |
filters | key==value or key!=value, joined with ;. Use | for OR. Keys: page, referrer, country, device, browser, os, screen, utm_source, utm_medium, utm_campaign. referrer==(direct) = no referrer. |
interval | hour (up to 31 days) · day · week (Mon start) · month |
metrics | visitors, pageviews, visits, views_per_visit, bounce_rate, visit_duration |
property | page, entry_page, exit_page, referrer, channel, ai, utm_source, utm_medium, utm_campaign, country, device, browser, os, screen |
limit, page, offset | Pagination for /breakdown. limit 1–1000 (default 100); page from 1, or offset from 0. The response has pagination.total and has_more, so you can walk the full list. |
search | Case-insensitive text match on the value for /breakdown (country also matches names, e.g. united → GB, US). |
sort | For /breakdown: visitors (default) or pageviews; for entry_page/exit_page: visits (default) or visitors. |
compare | true (default) or false |
format | json (default) or csv |
Fields
| Field | Meaning |
|---|---|
visitors | Unique visitors. Counted with a daily-rotating hash, so multi-day totals are the sum of daily uniques. |
pageviews | Page loads and single-page-app route changes. |
visits | Pageviews from one visitor no more than 30 minutes apart. |
views_per_visit | pageviews ÷ visits |
bounce_rate | Share (0–1) of visits with one pageview. |
visit_duration | Average seconds from first to last pageview in a visit. |
change | Fractional change against the previous period of the same length (0.171 = +17.1%). |
All dates and hours are UTC. Rows with no data are returned as zeros, so charts have no gaps.
Rate limits
| Plan | Requests per hour, per key |
|---|---|
Free | 300 |
Starter | 2,000 |
Growth | 6,000 |
Business | 20,000 |
Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Up to 10 active keys per account.
Errors
| Status | Code | What it means |
|---|---|---|
400 | invalid_param / missing_param | A parameter is missing or wrong. The message says which. |
401 | unauthorized / invalid_key | No key, a malformed key, or a revoked key. |
403 | forbidden | The key is limited to a different site. |
404 | site_not_found / not_found | The site is not on your account, or the endpoint does not exist. |
405 | method_not_allowed | The API is read-only: use GET. |
429 | rate_limited | Hourly limit reached. Wait for Retry-After seconds. |
{
"error": {
"code": "rate_limited",
"message": "Rate limit of 300 requests per hour reached for your plan.",
"status": 429,
"docs": "/api#errors"
}
}Ideas: what people build
📊 Google Sheets
Paste the Apps Script below, add your key under Project Settings → Script properties as QUIETSTATS_KEY, and set a daily trigger.
📈 Looker Studio
Point Looker Studio’s Google Sheets connector at that sheet for scheduled reports and client dashboards.
🖥️ Your own dashboard
Call /aggregate and /timeseries from your server and draw your own charts. CORS is open, but keep keys server-side.
🤖 Scripts and bots
Post yesterday’s numbers to Slack or Discord, or check /realtime during a launch.
// Google Sheets → Extensions → Apps Script. Run daily with a time-driven trigger.
function quietstatsDaily() {
const key = PropertiesService.getScriptProperties().getProperty('QUIETSTATS_KEY');
const url = 'https://quietstats.co.uk/api/v1/timeseries?site=YOUR_SITE_ID&period=90d&interval=day&format=csv';
const res = UrlFetchApp.fetch(url, { headers: { Authorization: 'Bearer ' + key } });
const rows = Utilities.parseCsv(res.getContentText());
const sheet = SpreadsheetApp.getActive().getSheetByName('quietstats') || SpreadsheetApp.getActive().insertSheet('quietstats');
sheet.clearContents().getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}Privacy stays the same
The API only returns aggregate numbers: totals, time buckets and top values. It never returns visitor hashes, IP addresses, user agents or single visits, because we don’t store the first three and don’t expose the last. Keys are read-only and can be limited to one site.
Your numbers, wherever you need them
The API is included on every plan, including Free.