API reference
Stats API
The stats API delivers traffic summaries, breakdowns and live activity for registered hostnames. Query the public demo without a token, or send an owner's Firebase ID token for a private site.
Base URL: https://analytics.bitgate.dev. OpenAPI document | Authentication
TypeScript examples run server-side in Node 20+: save a snippet as example.mts, set any referenced environment variables, then run npx tsx example.mts.
Read a public report
GET /api/sites/{hostname}/overview
These examples only read demo.bitgate.dev. The curl example uses the default trailing 30 days; the TypeScript example selects the last 24 complete UTC hours.
curl --fail-with-body --silent --show-error --max-time 20 \
'https://analytics.bitgate.dev/api/sites/demo.bitgate.dev/overview?tz=UTC'const to = Math.floor(Date.now() / 3_600_000) * 3_600;
const query = new URLSearchParams({
from: String(to - 86_400),
to: String(to),
tz: "UTC",
});
const response = await fetch(
`https://analytics.bitgate.dev/api/sites/demo.bitgate.dev/overview?${query}`,
{ signal: AbortSignal.timeout(20_000) },
);
if (!response.ok) {
throw new Error(`Overview ${response.status}: ${await response.text()}`);
}
console.log(await response.json());Private reads add Authorization: Bearer <Firebase ID token>. Product API responses have no cross-origin CORS headers: use a server-side client or the Totallytics origin, even for public reports.
Date ranges
The overview and breakdown endpoints share the same range parser.
| Parameter | Type | Default and bounds |
|---|---|---|
from |
number | Unix seconds, inclusive; rounded down. Defaults to to - 30 * 86400. |
to |
number | Unix seconds, exclusive; rounded up. Defaults to current Unix seconds. |
tz |
string | Overview only. Defaults to UTC; affects series grouping, not the UTC range. |
The rounded range must satisfy from < to and span no more than 400 days. Send seconds, not JavaScript milliseconds. Missing, empty, zero or non-numeric from/to values select their defaults rather than returning a validation error. Non-finite values or an invalid range return 400 invalid range.
Timezone handling
Use UTC or a recognized zone such as Europe/Amsterdam. The parser accepts at most two slash-separated components, each 1–32 characters of letters, digits, _, + or -. Other syntax silently becomes UTC; this includes multi-slash names such as America/Argentina/Buenos_Aires. A syntactically accepted but unknown zone can return 500 internal error.
Daily series timestamps are generated by converting a grouped calendar date to Unix seconds. They are not guaranteed to represent midnight in the requested timezone. Use tz=UTC for unambiguous date labels; the account summary returns explicit YYYY-MM-DD day strings.
Hour boundaries
Overview pageviews, visitors and series filter complete hourly aggregates by their hour's start. A range starting at 10:30 excludes the 10:00 bucket; an end at 11:30 includes the whole 11:00 bucket. Use UTC hour-aligned bounds when comparing these numbers with breakdowns, which filter individual timestamps.
Duration and scroll averages use individual append timestamps. The previous period applies the same rules to the immediately preceding interval of equal length.
Overview response
| Field | Type | Meaning |
|---|---|---|
totals |
object | Metrics for the requested range |
previous |
object | Same metrics for [from - (to - from), from) |
totals.pageviews, previous.pageviews |
number | Non-bot pageview count from hourly aggregates |
totals.visitors, previous.visitors |
number | Approximate distinct pageview visitor identifiers across the range |
totals.avg_duration_s, previous.avg_duration_s |
number | Rounded mean duration in seconds across non-bot append rows; zero if none |
totals.avg_scroll, previous.avg_scroll |
number | Rounded mean scroll percentage across those append rows; zero if none |
series |
array | Time-ordered buckets; missing buckets are not zero-filled |
series[].t |
number | Bucket timestamp in Unix seconds |
series[].pageviews |
number | Non-bot pageviews in the bucket |
series[].visitors |
number | Approximate distinct pageview visitor identifiers in the bucket |
live |
number | Exact distinct visitor identifiers with non-bot activity in the last five minutes |
granularity |
hour or day |
hour for ranges up to four days; day for longer ranges |
Example response:
{
"totals": {
"pageviews": 3,
"visitors": 2,
"avg_duration_s": 42,
"avg_scroll": 75
},
"previous": {
"pageviews": 2,
"visitors": 2,
"avg_duration_s": 30,
"avg_scroll": 50
},
"series": [
{ "t": 1789516800, "pageviews": 1, "visitors": 1 },
{ "t": 1789520400, "pageviews": 2, "visitors": 2 }
],
"live": 0,
"granularity": "hour"
}What the counts mean
- Visitor identifiers depend on the UTC day, site, request IP and user agent. They rotate daily, so a person returning tomorrow is not deduplicated across days. The supplied collector
uniqueflag does not determine these visitor counts. - Overview visitors use an approximate distinct aggregate. Breakdown visitors and
liveuse exact distinct identifiers. These are identifiers, not a count of identified people or sessions. liveis independent of the selected date range and includes pageview, event, append and error activity. Event-only visitors can appear live without increasing pageview visitors.- Append averages are per append row, not per pageview or session. Appends are not joined back to pageviews for these calculations; repeated appends and zero-valued rows contribute to the mean in their own ingestion-time range.
- Series may be empty, omit quiet intervals, or contain a zero-pageview bucket created by append activity. Zero-fill in your client only if your chart needs it. Do not add bucket visitor counts to obtain a deduplicated range total.
Breakdowns
GET /api/sites/{hostname}/breakdown
| Parameter | Type | Default and bounds |
|---|---|---|
dim |
string | Required; one of the ten dimensions below |
from, to |
number | Same UTC range rules as overview |
limit |
integer | Default 10; numeric values clamp to 1–100. Zero or non-numeric input uses 10. Send integers: fractional values are not rounded and can fail. |
tz has no effect on this endpoint. Rows are ordered by count descending. There is no cursor, offset, total-row count or metadata filter.
| Dimension | Groups by | value counts |
|---|---|---|
pages |
Page path | Pageviews |
referrers |
Referrer hostname | Pageviews |
countries |
Country | Pageviews |
devices |
Parsed device category | Pageviews |
browsers |
Parsed browser name | Pageviews |
os |
Operating system name | Pageviews |
utm_sources |
utm_source |
Pageviews |
utm_mediums |
utm_medium |
Pageviews |
utm_campaigns |
utm_campaign |
Pageviews |
events |
Event name | Event rows |
All breakdowns exclude bot rows. Blank values are omitted except for pages and referrers, where an empty value is named Direct / none. Event metadata is stored by the collector but cannot be retrieved, grouped or filtered through this API. utm_term and utm_content are not breakdown dimensions.
curl --fail-with-body --silent --show-error --max-time 20 \
'https://analytics.bitgate.dev/api/sites/demo.bitgate.dev/breakdown?dim=pages&limit=10'const query = new URLSearchParams({ dim: "pages", limit: "10" });
const response = await fetch(
`https://analytics.bitgate.dev/api/sites/demo.bitgate.dev/breakdown?${query}`,
{ signal: AbortSignal.timeout(20_000) },
);
if (!response.ok) {
throw new Error(`Breakdown ${response.status}: ${await response.text()}`);
}
console.log(await response.json());Success is 200 with { "rows": [...] }. Each row has name (string), value (number of matching rows) and visitors (exact distinct visitor identifiers within that group). An empty report returns { "rows": [] }. A visitor can appear in several groups; group visitors must not be summed as a unique total.
All-sites summary
GET /api/sites/summary?tz=UTC
Requires a Firebase ID token even when some owned sites are public. Returns all sites owned by that account, ordered by creation time, with a rolling window beginning 31 days ago and an independent five-minute live count. Only tz is read; from, to and limit do not customize this endpoint.
curl --fail-with-body --silent --show-error --max-time 20 \
-H "Authorization: Bearer ${TT_TOKEN:?Copy a token from Authentication}" \
'https://analytics.bitgate.dev/api/sites/summary?tz=UTC'const token = process.env.TT_TOKEN;
if (!token) throw new Error("Set TT_TOKEN from the Authentication page");
const response = await fetch(
"https://analytics.bitgate.dev/api/sites/summary?tz=UTC",
{
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(20_000),
},
);
if (!response.ok) {
throw new Error(`Summary ${response.status}: ${await response.text()}`);
}
console.log(await response.json());| Field | Type | Meaning |
|---|---|---|
sites |
array | All owned sites; empty when none are registered |
sites[].hostname |
string | Registered hostname |
sites[].live |
number | Same five-minute live definition as overview |
sites[].days |
array | Available days, ascending; not zero-filled |
sites[].days[].day |
string | YYYY-MM-DD calendar date in tz |
sites[].days[].pageviews |
number | Non-bot pageview count from hourly aggregates |
sites[].days[].visitors |
number | Approximate distinct pageview visitor identifiers for the day |
Sites without activity remain present with days: [] and live: 0. The rolling cutoff can give a partial first day; this is not 31 complete calendar days. The same timezone parser and hourly-boundary caveats apply.
Errors
Errors are JSON: { "error": "message" }. Successful JSON responses and handled errors carry Cache-Control: no-store.
| Status | Message | Meaning |
|---|---|---|
| 400 | invalid range |
Reversed, empty, non-finite or over-400-day rounded range |
| 400 | unknown dimension |
Missing or unsupported dim |
| 401 | sign in required |
Private report or account summary needs a valid token |
| 403 | not your site |
Valid token belongs to someone other than the private site's owner |
| 404 | unknown site |
Hostname is not registered |
| 500 | internal error |
Query failed; also possible with an unknown timezone or fractional limit |
Site access is checked before range or dimension validation. Public reports ignore a missing or invalid token because public access is sufficient. Site lookup caching can delay visibility changes.