API reference

Collector reference

Send pageviews and events to Totallytics with the browser tracker, an image beacon or a JSON request. Collector routes do not use Firebase tokens or API keys; register the hostname before sending traffic.

Base URL: https://analytics.bitgate.dev. OpenAPI document | Install the tracker

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.

Routes

Method Route Input Success
GET /latest.js Tracker configuration lives on the script tag JavaScript
GET /simple.gif Query parameters 1×1 GIF
GET /noscript.gif Query parameters, with page details from Referer 1×1 GIF
POST /events One JSON object Plain text ok
POST /append Same JSON object format as /events Plain text ok
GET /healthz None Plain text ok

/events and /append are the same ingestion handler. Both default to type: "pageview"; the route name does not choose a row type. Set type: "event" for events and type: "append" for duration or scroll updates. There is no batch-array API.

Send JSON

The examples use a placeholder hostname. Replace it with your registered hostname; never use the public demo as a write target.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  -X POST https://analytics.bitgate.dev/events \
  -H 'Content-Type: application/json' \
  --data '{"hostname":"your-domain.example","type":"event","event":"signup","metadata":{"plan":"pro"}}'
typescript
const hostname = process.env.EA_HOSTNAME;
if (!hostname) throw new Error("Set EA_HOSTNAME to your registered hostname");

const response = await fetch("https://analytics.bitgate.dev/events", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    hostname,
    type: "event",
    event: "signup",
    metadata: { plan: "pro" },
  }),
  signal: AbortSignal.timeout(20_000),
});
if (!response.ok) {
  throw new Error(`Collector ${response.status}: ${await response.text()}`);
}
console.log(await response.text());

A successful JSON request returns 200 with the literal body ok, not JSON. Send JSON text: application/json is recommended, and JSON sent as text/plain by sendBeacon is also accepted; the handler does not enforce Content-Type. Curl's default user agent is classified as a bot, so a curl test can succeed without appearing in reports. Server-side visitor attribution uses the sending request's IP and effective user agent, not an end user's identity supplied in JSON.

Append duration and scroll

Appends create additional rows; they do not update an existing pageview. Use the original pageview's id or page_id as original_id for correlation. The collector does not require that ID or check that a matching pageview exists.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  -X POST https://analytics.bitgate.dev/append \
  -H 'Content-Type: application/json' \
  --data '{"hostname":"your-domain.example","type":"append","original_id":"pageview-id","duration":42,"scrolled":75}'

Duration is in seconds and scroll is a percentage. Appends are counted in their own ingestion-time range, not the original pageview's time. Overview averages are calculated per append row, not per pageview.

Payload fields

The same fields work as query parameters on the pixels and as properties of the single JSON body on either POST route. Query values are strings. In JSON, scalar strings, numbers and booleans are converted to strings before most field processing; use the types below for predictable results. Unrecognized fields are ignored.

Text limits below are truncation limits, not validation errors. Missing optional text defaults to an empty string unless noted. Numeric measurements are rounded to the nearest integer and clamped to the stated maximum; missing, non-finite or non-positive values become zero.

Required and routing fields

Field Type Processing
hostname string Required and non-empty. Lowercased and truncated to 253 characters; not whitespace-trimmed. Use the exact registered hostname, without scheme, port or path.
type string pageview, event, append or error; default pageview when missing or empty. Case-sensitive; truncated to 16 characters before validation.
event string Required for type: "event"; otherwise ignored. Truncated to 256 characters, then non-ASCII-letter/digit runs become _ and edge underscores are removed. Case is preserved; Buy now! becomes Buy_now. Empty after cleaning is an error.
path string 2,048 characters. Defaults to / for a pageview and empty for other types.
query string Page query string without the leading ?, up to 2,048 characters. Parsed for UTM parameters.
referrer string 2,048 characters. Use hostname/path without a URL scheme: the first slash-separated segment becomes the referrer hostname.
metadata object, array or string Objects and arrays are JSON-serialized; strings are kept as supplied. Stored text is truncated to 4,096 characters and can therefore cease to be valid JSON. Not exposed by the stats API.

IDs and measurements

Field Type Processing
id string Row ID, up to 64 characters
page_id string Page correlation ID, up to 64 characters
session_id string Session correlation ID, up to 64 characters; not used for visitor counting
original_id string Original pageview correlation ID, up to 64 characters
duration number Seconds; integer output from 0 to 86400
scrolled number Percentage; integer output from 0 to 100
viewport_width, viewport_height number Viewport dimensions; integer output from 0 to 65535
screen_width, screen_height number Screen dimensions; integer output from 0 to 65535
error string Error text, up to 2,048 characters; error rows have no dedicated stats breakdown

IDs do not provide deduplication or idempotency. Reusing an ID can create another row.

Client context

Field Type Processing
ua string User agent, truncated to 512 characters when supplied. Falls back to the request's User-Agent header. Drives browser, OS, device, visitor and bot classification.
timezone string Client timezone label, up to 64 characters; does not set row timestamps
language string Language label, up to 16 characters
os_name, os_version string Up to 64 characters each; non-empty supplied values override parsed OS values
brands string Brand information serialized as a string, up to 256 characters
version string Script version label, up to 32 characters
hostname_original string Original hostname before an override, lowercased and truncated to 253 characters

Flags

Flags are true for boolean true, number 1, string "true" or string "1"; other values are false. Defaults are false except https, which defaults to true when absent.

Field Meaning
unique Stored uniqueness hint; does not control stats visitor counts
mobile Mobile hint; parsed device type also contributes to the stored mobile flag
bot Marks a bot. False does not override bot detection from the user agent.
brave, duck Browser-specific flags; not available as breakdown dimensions
https Whether the tracked page used HTTPS
collect-dnt Separate override, not a normal flag: only true or "true" bypasses a DNT skip; 1 does not.

Campaign fields

Field Type Processing
utm_source string Up to 256 characters; available as utm_sources in stats
utm_medium string Up to 256 characters; available as utm_mediums in stats
utm_campaign string Up to 256 characters; available as utm_campaigns in stats
utm_term string Up to 256 characters; no stats breakdown
utm_content string Up to 256 characters; no stats breakdown

For each field, a non-empty UTM value parsed from query takes precedence over the explicit top-level value. Other query parameters are stored within query but do not create report dimensions.

Server-derived fields

Row timestamps are assigned during ingestion. Country comes from the incoming request's network context. Visitor identifiers are derived from the UTC day, site, incoming IP and effective user agent, and rotate daily. Payload fields such as timestamp, ts, ip, country and visitor_id do not override them. The tracker's time cache-buster and sri field are also ignored by the collector.

Pixels and script

Pageview pixel

GET /simple.gif reads the payload fields from its query string. A valid request returns a 1×1 GIF, including for event, append or error types. An invalid payload returns a text error instead of an image.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  --get https://analytics.bitgate.dev/simple.gif \
  --data-urlencode 'hostname=your-domain.example' \
  --data-urlencode 'type=pageview' \
  --data-urlencode 'path=/pricing' \
  --output /dev/null

For metadata on a pixel, send a URL-encoded JSON string rather than an object. A GET pixel is a collection request, not a read-only stats request.

No-JavaScript pixel

GET /noscript.gif has the same payload format and GIF response. When absent from the query, hostname, path, https and query are derived from the page URL in the Referer header. Explicit parameters take precedence, even when empty. type defaults to pageview.

The incoming Referer is the page being tracked, not that page's acquisition referrer. The collector does not infer the acquisition referrer field from it. Browser referrer policies may omit the header or reduce it to an origin; then the hostname must be supplied explicitly or the path may become /.

Browser script

GET /latest.js serves JavaScript with Cache-Control: public, max-age=86400, stale-while-revalidate=604800. Install it with the configuration described in Installation and use Events for browser event calls. Fetching the script itself does not record a pageview.

Receipt and filtering

A collector 200 acknowledges the handler's response, not a durable insert. Storage runs in the background; a later storage failure is not returned to the caller. Normally, unregistered hostnames are discarded. Registration lookup failures can bypass that check, so it is not an authorization boundary. Site caching can delay registration changes.

There is no idempotency guarantee. Do not automatically retry event or append requests after an ambiguous network failure: the first request may already have been processed. Inspect the response and reports separately when validating an integration.

If DNT: 1 or X-Do-Not-Track: 1 is present, collection is skipped and the normal success response is returned, unless collect-dnt is true. This check occurs before payload-field validation; POST bodies must still parse as JSON. The browser script also checks browser Do Not Track before sending.

Bot rows can be stored but are excluded from reports. Detection checks the bot flag and user-agent matches for bot, spider, crawl, monit, curl, wget, python-requests, node-fetch, axios or headless, case-insensitively.

Responses and CORS

Status Body Meaning
200 ok JSON collector receipt or /healthz liveness response
200 GIF bytes Pixel receipt
400 POST required /events or /append was called with a method other than POST or OPTIONS
400 invalid JSON POST body did not parse as a non-null JSON object; arrays are not a batch format
400 hostname is required No usable hostname
400 unsupported type: <type> Row type is not supported
400 event name is required Event name is missing or empty after cleaning
204 Empty OPTIONS preflight

An array is not processed as a batch: it ordinarily reaches hostname validation and returns hostname is required. Collector errors are plain text, not the product API's JSON error envelope. Pixel responses use image/gif and Cache-Control: no-store, no-cache, must-revalidate; JSON ingestion responses use text/plain; charset=utf-8 and Cache-Control: no-store.

Collector responses and OPTIONS preflights provide Access-Control-Allow-Origin: *, methods GET, POST, OPTIONS, allowed headers Content-Type, and a preflight max age of 86400 seconds. They do not allow credentials or arbitrary custom request headers. This does not enable cross-origin reads of /api/*.

GET /healthz returns ok without checking storage or database readiness, and its response has no collector CORS headers. Use it to check that the HTTP handler responds, not that events have been stored.