# 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.

## 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 <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](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 <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](https://nitur.dev/privacy).
