How Pure Analytics works
Pure Analytics is a privacy-friendly web analytics service. You add one script tag to your site, and you get real-time traffic, referrer, geography, device, campaign and revenue data in a dashboard and through a REST API.
This page is written to be read by both people and AI agents. Every fact here reflects the running system: the tracking snippet, the API contract, the plan limits and the quota behaviour.
In one paragraph
A small JavaScript file on your pages sends events to https://pure-analytics.com/api/track. A Cloudflare Worker validates each event, filters bots, stores it in PostgreSQL, and counts page views against your monthly quota. You read the aggregated numbers either in the dashboard at https://pure-analytics.com/dashboard or through the public API at https://pure-analytics.com/api/v1/analytics with an API key.
Architecture
Four pieces: the script on your pages, the ingest Worker, the database, and the two readers on top of it.
| Layer | What runs there |
|---|---|
| Tracking script | https://pure-analytics.com/tracking/utilities/script.js, loaded with defer on your site |
| Ingest | Cloudflare Worker, POST /api/track |
| Storage | PostgreSQL (Supabase), row-level security per account |
| Dashboard | React single-page app on Cloudflare Pages |
| Public API | Cloudflare Worker, GET /api/v1/analytics |
Aggregation happens in the database. The dashboard and the public API call the same aggregation functions, so a number you read through the API is the number you see in the dashboard.
Install the tracking script
Paste this before the closing </body> tag of every page. Replace the data-ea-website-id value with the tracking ID shown when you add a website in the dashboard.
<!-- PureAnalytics Tracking Code -->
<script
defer
data-ea-website-id="YOUR_TRACKING_ID"
data-ea-domain="example.com"
src="https://pure-analytics.com/tracking/utilities/script.js">
</script>
<!-- End PureAnalytics Tracking Code -->Script attributes
| Attribute | Required | Meaning |
|---|---|---|
data-ea-website-id | yes | Tracking ID of the website, from the dashboard |
data-ea-domain | no | Your domain, used to tell internal from external links |
data-ea-track-accuracy | no | Cookie mode. most accurate sets a first-party cookie; any other value, or no attribute, keeps the script cookieless. Default: cookieless |
data-ea-stripe-key | no | Stripe publishable key, enables client-side revenue tracking |
data-ea-options | no | JSON object that turns single event types on or off, see below |
The script does not run on localhost, on 127.0.0.1, on file:// pages or inside an iframe. That is intentional: your development traffic never reaches your production numbers.
What the script collects
By default the script collects every event type. Turn single types off with data-ea-options, for example data-ea-options='{"clicks": false, "exits": false}'.
| Option | Default | Events |
|---|---|---|
pageviews | true | Page views |
externalLinks | true | Clicks on links to other domains |
forms | true | Form submissions |
clicks | true | Clicks on elements with an id or a class |
exits | true | Page exits |
respectDNT | true | Honour the browser's Do Not Track setting |
Only page views count toward your monthly quota. Clicks, form submissions, external links, exits, heartbeats and custom events are stored and reported, but they never consume it.
The accuracy level you pick in the dashboard (Basic, Standard, Enhanced, Professional, Most Accurate) goes into the snippet as data-ea-track-accuracy. In the current script it decides only the cookie: Most Accurate is the only level that sets one; every other level, and a snippet without the attribute, is cookieless.
Custom events
Beyond the automatic events, you can record your own through the global object the script installs.
window.pureAnalytics.track("signup_completed", { plan: "pro" });The first argument is the event name, the second is an optional object of metadata. window.easyAnalytics is a backwards-compatible alias for the same object and points at the same instance.
What is collected and what is not
Collected: page URL and title, referrer, UTM parameters, country derived from the request, browser, operating system, device type, screen size, and the timing of the events you enabled.
Not collected: no IP address is stored, no cross-site identifier, no behavioural profile, no fingerprint sold to a third party. IP anonymisation is on by default, and Do Not Track is respected by default.
Because the default configuration stores no personal data and sets no cookie, most sites can run Pure Analytics without a cookie banner. That is a description of what the software does, not legal advice for your jurisdiction.
Bot filtering
Events are dropped before storage when the request looks automated. Three checks run in order on the ingest Worker: known bot signatures in the user agent, a Chrome user agent arriving without Client Hints, and a source network that belongs to a hosting or datacenter provider. The network check is the one that catches headless browser farms, because the network an event arrives from cannot be spoofed by the page.
Traffic from Apple iCloud Private Relay is deliberately allowed through: those are real visitors on iOS.
Public API
Base URL: https://pure-analytics.com/api/v1/analytics
Authenticate with an API key created in the dashboard, sent in the X-API-Key header. A key is scoped to one or more of your websites; asking for a website the key does not cover returns 403.
Query parameters
| Parameter | Default | Meaning |
|---|---|---|
website_id | all websites on the key | Website UUID or tracking ID |
start | 30 days before end | ISO 8601 start of the period |
end | now | ISO 8601 end of the period |
metrics | visitors,pageviews,bounce_rate | Comma-separated list, see below |
groupBy | none | Adds a breakdown by one dimension |
limit | 100 | Rows returned in the breakdown, at most 10000 |
granularity | day | Bucket size of the timeseries: hour, day, week, month |
| filters | none | One or more of page, entry_page, exit_page, referrer, channel, ai_assistant, utm_source, utm_medium, utm_campaign, browser, country, device, os, button |
Metrics
visitors, sessions, pageviews, unique_pageviews, bounce_rate, engagement_rate, avg_session_seconds, views_per_session, plus two that return their own array: timeseries and revenue.
avg_session_duration and avg_session_time are accepted as aliases of avg_session_seconds.
Breakdown dimensions
Valid values for groupBy: page, entry_page, exit_page, country, browser, device, os, referrer, channel, ai_assistant, source, medium, campaign.
Every breakdown row reports visitors, sessions and pageviews separately, so there is never an ambiguous single count.
Filters
Any filter parameter restricts the whole response — summary, timeseries and breakdown alike. Filters combine with groupBy to cross two dimensions: groupBy=entry_page&referrer=google.com returns the pages Google search sends people to, groupBy=referrer&page=/pricing returns who sends people to a given page.
Attribution is per session and first touch: referrer, channel and the utm_* filters describe where the session started, so the page they pair with is entry_page. Grouping by page under a referrer filter answers a different question — every page seen by sessions from that source, not the page it landed on.
The referrer filter takes the same label the referrer breakdown returns: a hostname, or Direct.
ai_assistant exists because AI products strip the Referer on roughly two thirds of their traffic: neither referrer nor utm_source alone sees all of it. Filter or group by ai_assistant (ChatGPT, Perplexity, Copilot, …) to get the whole picture, and pair it with groupBy=entry_page for the pages each assistant sends people to.
Example request
curl -s "https://pure-analytics.com/api/v1/analytics?metrics=visitors,pageviews,bounce_rate&groupBy=country&limit=5" \
-H "X-API-Key: YOUR_API_KEY"Landing pages of the traffic coming from Google:
curl -s "https://pure-analytics.com/api/v1/analytics?metrics=visitors,sessions&groupBy=entry_page&referrer=google.com&limit=10" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"website_ids": ["8f3ca1d2-....-...."],
"period": {
"start": "2026-08-02T00:00:00.000Z",
"end": "2026-09-01T00:00:00.000Z",
"timezone": "Europe/Rome"
},
"visitors": 4821,
"pageviews": 11204,
"bounce_rate": 42.7,
"breakdown": [
{ "country": "IT", "visitors": 1902, "sessions": 2210, "pageviews": 5104 }
]
}The reporting timezone is the one configured on the website. When a single call spans several websites the response is in UTC, because mixing timezones in one bucket would produce a series that matches none of the sites.
Revenue
Ask for metrics=revenue and the response carries one row per currency with gross, refunded, net, transactions and refunds. Amounts in different currencies are never summed together.
Rate limits
60 requests per 60 seconds per API key, or per client IP for requests without a key. The limit covers /api/v1/*, /api/health and the MCP server; it never applies to the tracking ingest.
Every response carries the IETF rate limit headers:
RateLimit-Policy: "api";q=60;w=60
RateLimit: "api";r=57;t=42r is the number of requests left in the current window and t the seconds until it resets, as seen by the edge node that answered. Past the limit the API answers 429 with a Retry-After header in seconds: wait that long, then retry.
Errors
Errors are application/problem+json (RFC 9457). Every error body has the same fields, so an agent can branch on code and show resolution to a person.
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://pure-analytics.com/docs#errors",
"title": "Unauthorized",
"status": 401,
"detail": "Missing X-API-Key header",
"code": "missing_api_key",
"resolution": "Send a valid API key in the X-API-Key header. Create one in the dashboard under Settings > API keys.",
"instance": "/api/v1/analytics"
}| Status | code | Cause |
|---|---|---|
400 | invalid_group_by, invalid_granularity, no_websites | Unsupported parameter value, or no website on the key |
401 | missing_api_key, invalid_api_key, inactive_api_key, expired_api_key | The API key is missing, unknown, deactivated or expired |
403 | website_forbidden | The requested website is not associated with the key |
404 | not_found | No endpoint at that path |
429 | rate_limited | More than 60 requests in 60 seconds |
500 | query_failed, internal_error | The query failed; retry later |
The body also repeats detail in an error field, for clients written against the first version of the API.
OpenAPI
The full contract of the API, with every parameter, schema, error and header, is published as OpenAPI 3.1 at https://pure-analytics.com/openapi.json. The API catalog at https://pure-analytics.com/.well-known/api-catalog (RFC 9727) links to it.
MCP server
AI agents can read the same data through a Model Context Protocol server, without writing HTTP calls.
| Endpoint | https://pure-analytics.com/mcp |
| Transport | Streamable HTTP, JSON responses, no session |
| Authentication | X-API-Key header on the connection, or Authorization: Bearer <key> |
| Server card | https://pure-analytics.com/.well-known/mcp/server-card.json |
Tools
Both tools are read-only.
| Tool | Needs a key | What it returns |
|---|---|---|
get_analytics | yes | The same report as GET /api/v1/analytics: summary metrics, an optional breakdown (group_by), timeseries and revenue, with the same filters |
get_api_reference | no | Accepted metrics, dimensions, filters, limits, rate limit and plan prices |
Without a key, initialize, tools/list, get_api_reference and the resources still work, so an agent can discover the server before the user hands over a key.
Resources
| URI | Type |
|---|---|
https://pure-analytics.com/docs.md | This documentation, text/markdown |
https://pure-analytics.com/openapi.json | The OpenAPI specification, application/json |
pure-analytics://reference | The API reference as JSON |
Connect a client
Any MCP client that supports remote servers over HTTP can connect. For example, in Claude Code:
claude mcp add --transport http pure-analytics https://pure-analytics.com/mcp --header "X-API-Key: YOUR_API_KEY"Command line
There is no separate CLI to install: the API is one GET request, so curl and jq are the command line client.
export PA_KEY=YOUR_API_KEY
# Visitors and bounce rate for the last 30 days
curl -s "https://pure-analytics.com/api/v1/analytics?metrics=visitors,bounce_rate" -H "X-API-Key: $PA_KEY" | jq
# Top 10 landing pages of the traffic sent by ChatGPT
curl -s "https://pure-analytics.com/api/v1/analytics?groupBy=entry_page&ai_assistant=ChatGPT&limit=10" -H "X-API-Key: $PA_KEY" | jq '.breakdown'
# Daily visitors as CSV
curl -s "https://pure-analytics.com/api/v1/analytics?metrics=timeseries" -H "X-API-Key: $PA_KEY" | jq -r '.timeseries[] | [.date, .visitors] | @csv'To explore the MCP server from a terminal, the official inspector works without installation:
npx -y @modelcontextprotocol/inspectorPlans and limits
Limits are page views per month, counted across all your websites. Clicks, form submissions, exits, heartbeats and custom events are stored but do not count.
| Plan | Pageviews per month | Price per month |
|---|---|---|
| Free | 1,000 | €0 |
| Starter | 5,000 | €19 |
| Pro | 25,000 | €49 |
| Enterprise | 75,000 | €89 |
| Scale | 150,000 | €149 |
| Unlimited | 300,000 | €249 |
What happens when you go over
Going over your plan limit does not drop your data. You are notified once per month inside the app, and collection continues. Losing a customer's data to enforce a price tier is not a trade we make.
A separate ceiling, ten times the plan limit, exists to protect the infrastructure against abuse. Past that point the ingest returns 429. If the quota counter itself fails, tracking proceeds anyway: a quota problem must never break collection.
An account whose payment is late keeps its plan limit during the grace period. An account with no active subscription falls back to the Free limit of 1,000 pageviews.
Machine-readable resources
These URLs exist so an agent can read the product without rendering a page.
| Resource | URL |
|---|---|
| Short summary for language models | https://pure-analytics.com/llms.txt |
| This page, full text | https://pure-analytics.com/llms-full.txt |
| This page, Markdown | https://pure-analytics.com/docs.md |
| Sitemap | https://pure-analytics.com/sitemap.xml |
| OpenAPI 3.1 specification | https://pure-analytics.com/openapi.json |
| API catalog (RFC 9727) | https://pure-analytics.com/.well-known/api-catalog |
| MCP server | https://pure-analytics.com/mcp |
| MCP server card | https://pure-analytics.com/.well-known/mcp/server-card.json |
| MCP registry entry | https://pure-analytics.com/server.json |
| Tracking script | https://pure-analytics.com/tracking/utilities/script.js |
Every public page of this site is served as complete server-rendered HTML, so a crawler that does not execute JavaScript still reads the content. Ask for any public page with Accept: text/markdown and you get its Markdown version instead of the HTML, from the same URL.
Where to go next
The rest of the site, for a human reader.
- Comparisons:
/compare/google-analytics,/compare/plausible,/compare/fathom,/compare/simple-analytics - About the product and who builds it:
/about - Contact:
/contact - Blog:
/blog - Privacy policy:
/privacy - Sign up or sign in:
/login