Developers
API reference
Every resource, the parameters it takes, the shape it answers with — plus errors, limits and what v1 promises.
Snippets use YOUR_SITE_ID as a placeholder. Sign in and they're filled in with your real site id.
Every endpoint here is a GET on /api/v1/<resource>, authenticated with Authorization: Bearer <your key>, and read-only. A key is scoped to the single site it was created under, so no request needs to name a site. Start at the API guide if you haven't made a key yet. Sending events instead of reading them is a different endpoint — see /docs/server-events.
The response envelope
Every successful JSON response has the same three keys. Only data changes shape per resource; site and range echo what the request actually resolved to, which is the quickest way to check a range parameter did what you meant.
{
"site": { "id": "YOUR_SITE_ID", "domain": "acme.com" },
"range": { "since": "2026-08-24T09:12:00.000Z", "until": "2026-09-23T09:12:00.000Z" },
"data": { … }
}Parameters every resource accepts
- days=N — how far back from now to report on. Defaults to 30, clamped to 1–365, and ignored if you also send from.
- from=&to= — an explicit range, any format Date can parse (ISO is safest). from wins over days; to defaults to now. An unparseable value is ignored rather than rejected, so a typo silently gives you the default — check the range in the response if a number looks wrong.
- format=csv — the same data as a CSV file, for resources made of rows. See each resource below for what one row is.
Three resources ignore the range
revenue and goals always report on the last 30 days, and live always on the last 5 minutes. They take days/from/to without complaining and then ignore them — the range in the envelope is what you asked for, not what those three answered with.
Resources
GET /api/v1/site
Details of the site this API key belongs to.
format=csv— not available: this resource isn't rows, so it answers 400.
{
"id": "YOUR_SITE_ID",
"name": "Acme",
"domain": "acme.com",
"timezone": "Europe/London"
}one row shown where a list is returned
GET /api/v1/stats
Headline numbers: visitors, pageviews, bounce rate, average session time, visitors online right now.
format=csv— not available: this resource isn't rows, so it answers 400.
{
"visitors": 1284,
"pageviews": 3190,
"liveNow": 7,
"avgSessionSeconds": 96,
"bounceRatePct": 41,
"primaryGoal": { "id": "cl…", "name": "Signup", "hits": 38, "ratePct": 3 }
}one row shown where a list is returned
GET /api/v1/timeseries
Visitors and pageviews per bucket over the range. Optional granularity: day (default), hour or minute.
granularity— day (default), hour or minute. Anything else falls back to day.format=csv— one row per bucket.
{
"granularity": "day",
"points": [
{ "bucket": "2026-09-01T00:00:00.000Z", "visitors": 210, "pageviews": 540 }
]
}one row shown where a list is returned
GET /api/v1/pages
Most-viewed pages (top 8) with their pageview counts.
format=csv— one row per page.
{
"pages": [{ "path": "/pricing", "views": 812 }]
}one row shown where a list is returned
GET /api/v1/referrers
Traffic sources (top 10) with their channel (direct, search, social, ai, referral) and visitor counts.
format=csv— one row per source.
{
"referrers": [{ "channel": "search", "source": "google.com", "visitors": 214 }]
}one row shown where a list is returned
GET /api/v1/countries
Visitors broken down by country.
format=csv— one row per country.
{
"countries": [{ "countryCode": "US", "country": "United States", "visitors": 96 }]
}one row shown where a list is returned
GET /api/v1/devices
Visitors broken down by device type.
format=csv— one row per device type.
{
"devices": [{ "device": "desktop", "count": 742, "pct": 61 }]
}one row shown where a list is returned
GET /api/v1/revenue
Attributed revenue totals plus breakdowns by referrer, country, device and UTM campaign.
- Ignores days/from/to — always reports on the last 30 days.
format=csv— the byReferrer breakdown — the first row-shaped field in the response.
{
"totalRevenueCents": 184900,
"refundedCents": 0,
"netRevenueCents": 184900,
"conversions": 23,
"avgOrderValueCents": 8039,
"avgDaysToConvert": 4.2,
"revenuePerVisitorCents": 144.1,
"currency": "usd",
"byReferrer": [{ "referrer": "google.com", "revenueCents": 74900, "conversions": 9 }],
"byCountry": [{ "country": "United States", "revenueCents": 99900, "conversions": 12 }],
"byDevice": [{ "device": "desktop", "os": "macOS", "browser": "Chrome", "revenueCents": 120000 }],
"byCampaign": [{ "campaign": "spring_launch", "revenueCents": 62000, "conversions": 7 }]
}one row shown where a list is returned
GET /api/v1/goals
Configured goals with their completion counts and conversion rates.
- Ignores days/from/to — completions are counted over the last 30 days.
format=csv— one row per goal.
{
"goals": [
{ "id": "cl…", "name": "Signup", "matchType": "event", "matchValue": "signup", "hits": 38, "ratePct": 3 }
]
}one row shown where a list is returned
GET /api/v1/live
Visitors active in the last 5 minutes, with the page each is on right now.
- Ignores days/from/to — always the last 5 minutes, 20 visitors at most.
format=csv— one row per live visitor.
{
"liveNow": 7,
"visitors": [
{
"id": "v_9f2…",
"path": "/pricing",
"country": "United States",
"countryCode": "US",
"device": "desktop",
"browser": "Chrome",
"os": "macOS",
"referrer": "google.com",
"secondsAgo": 12
}
]
}one row shown where a list is returned
GET /api/v1/funnels
Every funnel you've built in the dashboard, with visitor counts and drop-off percentage at each step.
- Ignores days/from/to — steps are counted over the last 30 days, same window the dashboard's Funnels page uses.
format=csv— one row per funnel — the steps column is nested JSON, not one CSV row per step.
{
"funnels": [
{
"id": "cl…",
"name": "Signup to paid",
"windowMinutes": null,
"steps": [
{ "order": 0, "matchType": "event", "matchValue": "landing_view", "count": 900, "dropoffPct": 0 },
{ "order": 1, "matchType": "event", "matchValue": "signup", "count": 210, "dropoffPct": 76.7 },
{ "order": 2, "matchType": "event", "matchValue": "payment_completed", "count": 38, "dropoffPct": 81.9 }
]
}
]
}one row shown where a list is returned
GET /api/v1/visitor
One visitor's full event timeline, in order — look them up by our visitorId or by the externalId you passed to identify().
visitorId— VisitTrack's own visitor id (the 'id' field from the live resource, or your dashboard URL).externalId— Your own user id, for a visitor you've already called identify(visitorId, externalId) on.- Ignores days/from/to — always returns the visitor's whole history. Pass exactly one of visitorId or externalId.
format=csv— not available: this resource isn't rows, so it answers 400.
{
"id": "cl…",
"externalId": "user_482",
"country": "United States",
"firstSeenAt": "2026-08-01T10:00:00.000Z",
"isReturning": true,
"totalSessions": 4,
"totalEvents": 19,
"totalPageviews": 11,
"firstVisit": { "path": "/pricing", "createdAt": "2026-08-01T10:00:00.000Z", "referrer": "google.com" },
"timeline": [
{
"sessionId": "cl…",
"startedAt": "2026-08-01T10:00:00.000Z",
"referrer": "google.com",
"steps": [{ "id": "cl…", "type": "event", "name": "signup", "path": "/signup", "createdAt": "2026-08-01T10:02:00.000Z" }]
}
]
}one row shown where a list is returned
Errors
Every error is JSON with an error key, and nothing else is ever added to a successful response to signal failure — status code and error are the whole protocol.
{ "error": "Missing Authorization: Bearer <api key> header" }401 — no Authorization header at all
{ "error": "Invalid API key" }401 — the key doesn't exist, or it was revoked
Those two are deliberately not distinguishable from outside: a revoked key and a key that never existed answer identically, so nobody can use the API to test which old keys were real.
{
"error": "Unknown resource 'visits'",
"available": ["site", "stats", "timeseries", "pages", "referrers", "countries", "devices", "revenue", "goals", "live"]
}404 — no such resource. Checked before the key is, so a 404 here says nothing about your key.
{ "error": "'stats' has no rows to tabulate — drop format=csv for this resource." }400 — format=csv on something that isn't rows
A row-shaped resource that happens to have no rows in the range answers the same 400 — an empty CSV has no header to write, so there's nothing honest to return. Widen the range or drop format=csv to get an empty list back as JSON instead.
Rate limit
- 120 requests per minute, counted per key rather than per IP — two scripts sharing a key share the budget.
- Over the limit answers 429 with {"error":"Too many requests"} and a Retry-After header, in whole seconds. Wait that long rather than retrying immediately; a retry inside the window just spends another request.
- The window is a rolling one per key, so a burst clears on its own — no daily cap sitting behind it.
- Polling every endpoint once a minute uses a tenth of this. If you're anywhere near the limit you're probably polling for something you could ask for once with a wider range.
Versioning
The v1 in the path is a real promise, kept deliberately narrow so it can be kept. While v1 exists:
- Resources keep their names and their URLs. One won't be renamed or removed under v1.
- A field that exists keeps its name, its type and its meaning. Amounts stay in cents, timestamps stay ISO strings in UTC, percentages stay percentages.
- The envelope stays site / range / data.
- New fields and new resources can appear at any time. Parse tolerantly — ignore keys you don't recognise, and don't assume a list's length or order is part of the contract.
A change that would break a client reading v1 the way above describes — removing or renaming a field, changing a unit, changing how authentication works — is what makes a v2. It would ship at /api/v2 alongside v1 rather than replacing it in place.
What we're not promising
There's no deprecation window written down, because we haven't decided one — inventing a number here would be a promise nobody has committed to. If v1 is ever going away you'll hear it from us before it does, and this page will say the date. The rate limit above is operational, not part of the version contract, and can change.
Something missing? Tell us.
AI agent or LLM? Read this page as markdown.