# nitur — privacy-first web analytics Page views, referrers, countries and custom events, counted in aggregate. No cookies, no IP logging, no fingerprinting, and so no consent banner. Home: https://nitur.dev MCP server: https://nitur.dev/api/mcp REST base: https://nitur.dev/api/v1 OpenAPI: https://nitur.dev/api/v1/openapi.json ## What is collected Per page view: the path, the referring domain, a browser family, an operating system, a device type, a country code, and the calendar day. Nothing else. Never collected: IP addresses (the country comes from a header the host has already resolved, and the address is never read into a variable), cookies or any browser storage, visitor ids, screen size, canvas or font fingerprints, language, and time zone. Visitors are counted with a hash of a secret that is destroyed every night, so the same person on two days counts twice and cannot be linked. This is why there are no funnels, no sessions, and no per-visitor endpoint: the identity those need does not exist. ## Install One script tag. No package, no build step — 1.9 KB gzipped, loaded asynchronously so it cannot slow your page down. Fastest path, if you have a terminal open: ``` npx nitur init ``` It detects the framework, creates the site, writes the tag into the right file, and prints the token. Otherwise, replace `YOUR_SITE_TOKEN` below with the token shown on your site's page in the dashboard. **Plain HTML** — `every page, inside ` ``` … ``` - Views from localhost are ignored. Add data-track-localhost="true" to count them while developing. - The script honours Do Not Track and Global Privacy Control, and sends nothing when either is set. - Client-side route changes are counted automatically — there is nothing to call on navigation. **Next.js (App Router)** — `app/layout.tsx` ``` export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` - Use a plain
``` - Goes in index.html rather than a component: the tag only needs to load once, and putting it in a component would re-run it on every mount. - Views from localhost are ignored. Add data-track-localhost="true" to count them while developing. - The script honours Do Not Track and Global Privacy Control, and sends nothing when either is set. - Client-side route changes are counted automatically — there is nothing to call on navigation. ## Recording events Once the script is on the page, `window.track` is available anywhere: ``` window.track("signup"); window.track("purchase", { plan: "pro", seats: 3 }); ``` It is always safe to call. Before the script loads — and on localhost, and for anyone with Do Not Track on — it is a no-op rather than undefined, so a call in a click handler cannot throw. In TypeScript, add this once so `window.track` type-checks: ``` // types/track.d.ts declare global { interface Window { track: ( name: string, props?: Record, ) => void; } } export {}; ``` Names are normalised, so `Sign Up` and `sign up` are one event. Props take up to 20 string, number or boolean values. **Props are stored exactly as you send them.** Nothing is stripped, so keep personal data out — your dashboard warns when a value looks like an email address or an id, but treat that as a safety net rather than a filter. Events appear in `GET /api/v1/sites/{id}/stats` and through `get_site_stats`, with a count, a share of views, and a breakdown of the props sent with them. There are no funnels: sequencing one person's actions needs an identity that outlives the day, and none exists. ## Get a key Create one on your [account page](https://nitur.dev/dashboard/account). It is shown once and stored only as a hash, so it cannot be recovered — if you lose it, revoke it and make another. A key can read every site on your account, and create new ones. It cannot rename or delete anything. Send it as a bearer token: ``` curl https://nitur.dev/api/v1/me \ -H "Authorization: Bearer trk_sk_your_key_here" ``` ## MCP The MCP endpoint is `https://nitur.dev/api/mcp`, over streamable HTTP. Add it to Claude Code with: ``` claude mcp add --transport http nitur https://nitur.dev/api/mcp \ --header "Authorization: Bearer trk_sk_your_key_here" ``` Or, for any client that reads a JSON config: ``` { "mcpServers": { "nitur": { "type": "http", "url": "https://nitur.dev/api/mcp", "headers": { "Authorization": "Bearer trk_sk_your_key_here" } } } } ``` Six tools are exposed: | | | | --- | --- | | `list_sites` | Your sites and their ids. Start here — the other tools need an id. | | `get_site_stats` | Total views, a daily series, and breakdowns by page, referrer, browser, OS, device and country. | | `get_tracking_snippet` | The HTML snippet that installs tracking on a site. | | `create_site` | Start tracking a new project, and get its snippet back. | | `set_allowed_domains` | Restrict a site's token to specific domains, or clear the restriction. | | `get_usage` | Page views received this month and over the last twelve months. | No tool accepts a user id: the account is taken from the key, so an agent cannot reach another account's data by asking. ## REST | | | | --- | --- | | `GET /api/v1/me` | Verify a key. No side effects. | | `GET /api/v1/usage` | Page views received, this month and by month. | | `GET /api/v1/sites` | List your sites. | | `POST /api/v1/sites` | Create a site. Body: { name }. Returns 201. | | `GET /api/v1/sites/{id}` | One site. | | `PUT /api/v1/sites/{id}/domains` | Replace the domain allowlist. Body: { domains: [...] }. Empty clears it. | | `GET /api/v1/sites/{id}/stats?period=` | Aggregates. period is one of today · 7d · 30d · 12m · all; defaults to 7d. | ``` # Create a site and get its snippet curl -s -X POST https://nitur.dev/api/v1/sites \ -H "Authorization: Bearer $TRACK_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"My new project"}' | jq ``` ``` # List sites, then read stats for the first one SITE=$(curl -s https://nitur.dev/api/v1/sites \ -H "Authorization: Bearer $TRACK_KEY" | jq -r '.sites[0].id') curl -s "https://nitur.dev/api/v1/sites/$SITE/stats?period=30d" \ -H "Authorization: Bearer $TRACK_KEY" | jq ``` Errors carry a stable `error` code alongside a human-readable `message`: ``` { "error": "not_found", "message": "No site with that id on this account." } ``` ## TypeScript There is no published SDK yet. The API is small enough that a typed client is a few lines, and the [OpenAPI 3.1 spec](https://nitur.dev/api/v1/openapi.json) is served live — point any generator at it for a full client. ``` const nitur = async (path: string): Promise => { const res = await fetch(`https://nitur.dev/api/v1${path}`, { headers: { Authorization: `Bearer ${process.env.TRACK_KEY!}` }, }); if (!res.ok) throw new Error(`nitur: ${res.status} ${await res.text()}`); return res.json() as Promise; }; const { sites } = await nitur<{ sites: { id: string; name: string }[] }>("/sites"); ``` ## What the API deliberately cannot do Creating a site is the only write. Renaming, changing visibility, and deletion are dashboard-only: deleting a site cascades to every page view it collected and cannot be undone, and flipping `is_public` could quietly publish a private project. Those belong behind a person pressing a button that says so, rather than one misread instruction away from an agent. Ownership is never taken from a request body — a site is always created on the account the key belongs to, so there is no way to write into someone else's account. There is also no endpoint for per-visitor data, because none is collected. See [what is and is not stored](https://nitur.dev/privacy). ## Rules for coding agents This project uses nitur for analytics (https://nitur.dev) — privacy-first, no cookies, no IP logging, no consent banner. **Installing it on a project that has none yet:** run `npx nitur init`. It creates the site, writes the script tag into the right file, and stores the token. Do not hand-write the tag if the CLI is available. **Recording an event:** call `window.track(name, props?)` anywhere on the client. It is a no-op until the script loads, so it is safe to call from any handler without a guard. ```js window.track("signup"); window.track("purchase", { plan: "pro", seats: 3 }); ``` **Never put personal data in props.** They are stored exactly as sent — no email addresses, names, user ids, or anything else that identifies a person. Send a plan, a variant, a count, a category. **Reading the numbers:** the MCP server at `https://nitur.dev/api/mcp` exposes `list_sites`, `get_site_stats`, `get_usage` and `create_site`. Prefer it over scraping the dashboard. Failing that, the REST API is documented at https://nitur.dev/docs.md and specified at https://nitur.dev/api/v1/openapi.json. **What it cannot do, by design:** no funnels, no per-visitor data, no session replay, no cross-day identity. Do not offer to build those on top of it — the data to do so is never collected.