nitur.PricingSign in

API & MCP

Everything the dashboard shows is available to code and to agents, and a new site can be created without leaving the terminal. One API key works for both the REST API and the MCP server.

Reading this as an agent? /docs.md is the same page without the markup, and /llms.txt indexes everything.

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 HTMLevery page, inside <head>

<!doctype html>
<html lang="en">
  <head>
    <script src="https://nitur.dev/track.js" data-site-id="YOUR_SITE_TOKEN" async></script>
  </head>
  <body>
    …
  </body>
</html>
  • 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 (
    <html lang="en">
      <body>
        <script src="https://nitur.dev/track.js" data-site-id="YOUR_SITE_TOKEN" async></script>
        {children}
      </body>
    </html>
  );
}
  • Use a plain <script> tag, not next/script. The tracker reads its own data-site-id via document.currentScript, which is null for scripts injected at runtime — with next/script it would read the wrong tag or none at all.
  • React hoists it into <head> automatically, so it does not matter that it is written inside <body>.
  • 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.

React (Vite, CRA)index.html

<!doctype html>
<html lang="en">
  <head>
    <script src="https://nitur.dev/track.js" data-site-id="YOUR_SITE_TOKEN" async></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
  • 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<string, string | number | boolean>,
    ) => 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. 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_sitesYour sites and their ids. Start here — the other tools need an id.
get_site_statsTotal views, a daily series, and breakdowns by page, referrer, browser, OS, device and country.
get_tracking_snippetThe HTML snippet that installs tracking on a site.
create_siteStart tracking a new project, and get its snippet back.
set_allowed_domainsRestrict a site's token to specific domains, or clear the restriction.
get_usagePage 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/meVerify a key. No side effects.
GET /api/v1/usagePage views received, this month and by month.
GET /api/v1/sitesList your sites.
POST /api/v1/sitesCreate a site. Body: { name }. Returns 201.
GET /api/v1/sites/{id}One site.
PUT /api/v1/sites/{id}/domainsReplace 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 is served live — point any generator at it for a full client.

const nitur = async <T,>(path: string): Promise<T> => {
  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<T>;
};

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.