# pvuv.ai API reference — REST API and MCP server

Two ways to work with pvuv.ai from code, sharing one credential, a personal API token:

- **MCP server**: your AI assistant (Claude Code, Cursor, Codex, VS Code…) reads your analytics, finds where visitors drop off and writes tracking code.
- **REST API**: for dashboards, scripts, spreadsheets, anything that speaks HTTP.

## 1. Tokens: scope and permissions

Create one in the dashboard: **⚙ Settings → API tokens** on the sites page (https://www.pvuv.ai/sites), or on a site page **⋯ → Connect AI** (a token for that site plus ready-to-paste MCP configs).

A token has a **scope**, which sites it covers:

| Scope | Sees |
|---|---|
| All my sites | every site you own, including ones added later |
| A single site | only that site; it cannot see that you have others |

and a **purpose**, which sets its permissions:

| Purpose | Permissions | May |
|---|---|---|
| AI assistant / CLI — read only (default) | `read` | every `GET`, the MCP server |
| AI assistant — read, edit funnels, create sites | `read`, `write` | also save/delete funnels and tracking plans, mint server-reporting tokens, create sites (all-sites scope only) |
| Server reporting | `ingest` | only `POST …/events` and `POST …/crawler_hits`; reads nothing, so it may sit in a server's environment |

The token is shown **once** and only a hash of it is stored: if you lose it, revoke it and create another. Tokens start with `pvuv_`, so secret scanners recognise one that gets committed by mistake. Revoking takes effect immediately.

## 2. REST API

Base URL: `https://api.pvuv.ai`

```bash
curl -H "Authorization: Bearer pvuv_…" "https://api.pvuv.ai/v1/sites"
```

`X-API-Key: pvuv_…` works as an alias where setting an `Authorization` header is awkward (some no-code and spreadsheet tools).

### Reading (`read`)

| Endpoint | Returns |
|---|---|
| `GET /v1/sites` | sites this token can read; **call first**, ids are not guessable |
| `GET /v1/sites/lookup?domain=example.com` | your sites for a domain (`www.` and the bare host match), and whether another account tracks it (`registered_elsewhere`, all-sites tokens only) |
| `GET /v1/sites/{id}/overview` | pageviews, visitors (`uv`: humans; `uv_all`: everyone), sessions, bounce rates, dwell, conversions, revenue, verdict split; `compare=previous` adds the previous period |
| `GET /v1/sites/{id}/realtime` | visitors active in the last `window` minutes (5 default, 10 or 30; bots excluded) and a per-minute sparkline of the last 30 minutes |
| `GET /v1/sites/{id}/timeseries` | a metric over time; each bucket also carries `pv` and `invalid` |
| `GET /v1/sites/{id}/breakdown` | top values for one dimension |
| `GET /v1/sites/{id}/quality` | verdict totals and which detection signals fired |
| `GET /v1/sites/{id}/alerts` | rule checks, their ratios and thresholds |
| `GET /v1/sites/{id}/vitals` | real-user Web Vitals per page |
| `GET /v1/sites/{id}/ctit` | time from arrival to each goal |
| `GET /v1/sites/{id}/adguard` | ad protection: per-tier block / false-positive estimates, block reasons |
| `GET /v1/sites/{id}/crawler_names` | crawlers that ran the tracking script, per category and name, with verified / fake pageviews |
| `GET /v1/sites/{id}/crawlers` | reported AI-assistant and search-engine crawler hits per crawler, day and path, with IP verification |
| `GET /v1/sites/{id}/ai_referrals` | per page: an assistant's index / live fetches vs the clicks it sent, fetch → click lag, answer bursts |
| `GET /v1/sites/{id}/traffic` | individual suspicious visitors with their evidence |
| `GET /v1/sites/{id}/anomalies` | anomaly reports against the site's baseline |
| `GET /v1/sites/{id}/funnels` | saved funnels |
| `GET /v1/sites/{id}/funnel` | conversion funnel for `funnel_id` or `steps` (see Funnels below) |
| `GET /v1/sites/{id}/funnel_dropoff` | who dropped after `step` and what they did |
| `GET /v1/sites/{id}/visitors` | visitor list; `/visitors/{vid}/profile` for one journey |
| `GET /v1/sites/{id}/event_inventory` | every event name received (bots excluded): count, visitors, first/last time, server share, properties |
| `GET /v1/sites/{id}/tracking_plan` | the site's tracking plan |
| `GET /v1/sites/{id}/tracking_health` | the plan read against what arrived: never seen, unplanned, missing properties, wrong source, funnel steps that never fire |
| `GET /v1/sites/{id}/funnel_suggestions` | funnel templates filled with this site's pages and events, plus what is missing |

### Writing (`write`, bearer token only)

| Endpoint | Does |
|---|---|
| `POST /v1/sites` `{domain \| domains[], name?, timezone?}` | create a site (all-sites tokens only); returns your existing site for the domain instead of a duplicate (`created: false`); at most 100 sites per account |
| `POST /v1/sites/{id}/funnels` `{name, steps, funnel_id?}` | save (or update) a funnel |
| `DELETE /v1/sites/{id}/funnels/{funnel_id}` | delete a funnel |
| `POST /v1/sites/{id}/tokens` `{name?}` | mint an `ingest`-only token for that site (shown once) |
| `POST /v1/sites/{id}/tracking_plan` `{events: [{name, description, where, source: browser\|server, props, goal}]}` | replace the tracking plan (up to 100 events) |

### Reporting (`ingest`, bearer token only)

`POST /v1/sites/{id}/events`: **server-side events** from the site's backend (signup, purchase, subscription…), up to 100 per request:

```json
{ "events": [ { "name": "purchase", "user_id": "u_123", "visitor_id": "<_pv_id cookie>",
  "revenue": 29.0, "currency": "USD", "props": { "plan": "pro" }, "id": "evt_1Q2w3e", "ts": 1761465600000 } ] }
```

Each needs `visitor_id` (the `_pv_id` cookie the tracking script sets) or `user_id` (the id the page passed to `pvuv('identify', …)`). A server event counts as a goal (with revenue), is a funnel step and appears in the visitor's journey; it never counts as a pageview. `id` (8–64 chars) makes retries safe; `ts` may be up to 7 days back. Response `{accepted, duplicate, linked, unlinked, rejected}`. Guide with code: [server-events.md](https://api.pvuv.ai/docs/server-events.md).

`POST /v1/sites/{id}/crawler_hits`: report crawler fetches from your server. Guide with code: [crawler-sync.md](https://api.pvuv.ai/docs/crawler-sync.md).

### Common parameters

- `period`: `24h` `today` `yesterday` `7d` `30d` `90d` (any `Nd` up to 730) `this_week` `last_week` `this_month` `last_month` `this_year`, or `YYYY-MM-DD..YYYY-MM-DD` (inclusive, up to 730 days), in the site's own timezone
- `metric`: `pv` `uv` `sessions` `bounce_rate` `bounce_rate_single` `avg_duration_ms` `visit_duration_ms`
- `interval`: `minute` `hour` `day` `week` `month`
- `dim`: `page` `entry_page` `exit_page` `channel` `source` `referrer` `country` `region` `city` `browser` `os` `device` `size` `utm_medium` `utm_campaign` `utm_term` `utm_content` `goal` `bot_category` `not_found` `release` and `prop:<key>` (any event property)
- `limit`, `filters` (JSON array of `{dim, op, value}`, `op` = `is` | `is_not` | `contains`)

Errors are `{"error":"…"}` with a matching status: `401` bad or missing token, `403` token not valid for that site or lacking the permission, `404` unknown route or site.

### Funnels

`steps` is a JSON array (or `{"steps": [...], "options": {...}}`) of 2–8 steps plus up to 4 side steps:

| step field | |
|---|---|
| `type`, `value` | `page` + exact path, or a prefix ending in `*` (`/blog/*`); `event` + event name |
| `any` | other values that count as the same step |
| `props` | event property conditions, e.g. `{"kind": "google"}` |
| `side` | a step beside the path, reported as a share of those who entered |
| `label` | display name |

Options (in `options`, or as query parameters): `mode` = `ordered` (default) or `reached`; `window_days` = count a step only within that many days of entering; `base` = `all` (default) or `new`. `breakdown` = `source` `medium` `campaign` `term` `landing` `country` `device` `browser` `os` `release` or `prop:<key>`.

Each step returns `visitors`, `dropped`, `conv_prev` / `conv_first` (`{rate, low, high}`: a 90% range), `low_sample`, `median_gap_ms` and `friction` (what the people who dropped did).

## 3. MCP server

`POST https://api.pvuv.ai/mcp`: Streamable HTTP, stateless. Nothing to install. On a site page, **⋯ → Connect AI** creates the token and shows each client's config; by hand:

```bash
# Claude Code (in the project folder)
claude mcp add --transport http pvuv https://api.pvuv.ai/mcp --header "Authorization: Bearer pvuv_…"
```

```json
// Cursor: ~/.cursor/mcp.json   (VS Code: user mcp.json, "servers" with "type": "http")
{ "mcpServers": { "pvuv": { "url": "https://api.pvuv.ai/mcp", "headers": { "Authorization": "Bearer pvuv_…" } } } }
```

```toml
# Codex: ~/.codex/config.toml, with PVUV_TOKEN exported in the shell
[mcp_servers.pvuv]
url = "https://api.pvuv.ai/mcp"
bearer_token_env_var = "PVUV_TOKEN"
```

Keep the token out of repositories: use the global config files above, not a project's `.cursor/` or `.vscode/` folder.

**What the assistant can do is what the token can do:**

| Token | Tools |
|---|---|
| single site, `read` | the site's analytics; other sites are invisible |
| single site, `read` + `write` | also `save_funnel`, `delete_funnel`, `save_tracking_plan`, `create_ingest_token` |
| all sites, `read` | every own site (`site_id` required) and `find_site_by_domain` |
| all sites, `read` + `write` | also `create_site` and the write tools above |
| `ingest` only | refused |

Every tool is a call to the REST API above with your own token, so MCP can never do more than the token could do with `curl`. Prompts for common jobs: `weekly_review`, `find_funnel_leak`, `did_change_work`, `audit_tracking`, and with an all-sites token `setup_site`.

## Guides for AI assistants

- [tracking.md](https://api.pvuv.ai/docs/tracking.md): install the snippet, send goal events, define funnels
- [server-events.md](https://api.pvuv.ai/docs/server-events.md): report signups, payments and other backend milestones
- [crawler-sync.md](https://api.pvuv.ai/docs/crawler-sync.md): report AI-assistant and search-engine crawler fetches
- [llms.txt](https://api.pvuv.ai/llms.txt): index
