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
curl --fail-with-body --silent --show-error --max-time 20 \
  'https://analytics.bitgate.dev/api/sites/demo.bitgate.dev/overview?tz=UTC'
typescript
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:

json
{
  "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 unique flag does not determine these visitor counts.
  • Overview visitors use an approximate distinct aggregate. Breakdown visitors and live use exact distinct identifiers. These are identifiers, not a count of identified people or sessions.
  • live is 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 1100. 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
curl --fail-with-body --silent --show-error --max-time 20 \
  'https://analytics.bitgate.dev/api/sites/demo.bitgate.dev/breakdown?dim=pages&limit=10'
typescript
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
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'
typescript
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.