# UsherStats documentation, every page --- title: UsherStats documentation description: How to install UsherStats, read its dashboards, and use its API, MCP server, bot protection and proxy mode. nav: Overview --- # UsherStats documentation UsherStats is web analytics that counts people, not bots. It shows the readers of your site, the crawlers and AI bots that fetch it, and the AI assistants that send readers your way, without cookies or personal data. It can also protect your site from the bots you do not want. ## Start here - [Getting started](getting-started.md): your first pageview in five minutes. - [Installing](installing.md): the snippet, a first-party path on your own domain, the Worker SDK, or proxy mode. - [Concepts](concepts.md): what counts as a pageview, a session, a bot, and internal traffic. ## Use it - [Dashboards explained](dashboards.md) - [Goals and A/B tests](goals-and-experiments.md) - [Bot protection](bot-protection.md) - [Proxy mode](proxy-mode.md) - [Search Console and Bing](search-console-and-bing.md) ## Build on it - [API and tokens](api-and-tokens.md): everything an account can do, over HTTP. - [Agents and MCP](agents-and-mcp.md): give an AI agent access to your analytics. - [Exporting your data](data-export.md) ## Your account - [Privacy and compliance](privacy-and-compliance.md) - [Billing and plans](billing-and-plans.md) - [Troubleshooting](troubleshooting.md) Every page here is also available as Markdown: add `.md` to its address, or read [llms.txt](https://docs.usherstats.com/llms.txt) for the list and `llms-full.txt` for everything in one file. --- --- description: From a new account to your first counted pageview in about five minutes. --- # Getting started Five minutes from a new account to your first pageview. ## 1. Create an account Sign up at [app.usherstats.com/signup](https://app.usherstats.com/signup) with your email address and a password, and confirm the email we send you. Every feature is on every plan, including Free; you can add a card later. ## 2. Add your site In the dashboard, choose **Add site** and give: - a **name** (for you; it is not shown anywhere public); - every **hostname** the site is served on, such as `example.com` and `www.example.com`. UsherStats only accepts reports from pages on these hostnames, so a missing one means missing visits; - your **time zone**, which decides where a day begins in your charts. The site's **Install** page then shows its public key, which starts with `pk_`. ## 3. Add the snippet Paste this into the `` of every page, with your own key: ```html ``` That is the whole installation for human analytics. To serve it from your own domain, to see crawlers and AI bots, or to turn on bot protection, see [Installing](installing.md). ## 4. Visit your site Open one of your pages in an ordinary browser window, then switch to another tab or close it: the page reports when it is hidden, so time on page and scroll depth are complete. The site's Install page shows when the first report arrives, and the visit appears in the **People** view within a minute or two. If nothing arrives: - Your browser may send **Global Privacy Control** (Brave and DuckDuckGo do by default, and some extensions add it). UsherStats does not record those browsers at all. Try another. - If you have marked your browser as internal, your visit is under **Internal**, not People. That is working as intended. - If you browse through a VPN that runs on a cloud provider, your visit is counted as a hosting-network visit, under **Crawlers**. [Troubleshooting](troubleshooting.md) covers the rest. ## Next - [Concepts](concepts.md): what UsherStats counts, and what it leaves out. - [Goals and A/B tests](goals-and-experiments.md): count signups and downloads. - [Installing](installing.md): see the crawlers and AI bots reading your site. --- --- description: Install UsherStats with the snippet, a first-party path on your own domain, the Worker SDK, or proxy mode. --- # Installing There are three ways to install UsherStats, plus proxy mode, and they build on each other. | Way | What you get | What you change | |---|---|---| | [Snippet](#the-snippet) | Human analytics, goals, journeys, A/B tests | One tag in your pages | | [First-party path](#a-first-party-path) | The same, served from your own domain, so blockers aimed at third parties leave it alone | A route on your server | | [Worker SDK](#the-worker-sdk) | All of the above, plus crawler and AI-bot analytics and bot protection | Your Cloudflare Worker | | [Proxy mode](#proxy-mode) | The same as the Worker SDK, with no code | A DNS record | Your site's **public key** (`pk_…`) and **server secret** (`sk_live_…`) are on its Install page in the dashboard. The public key is public by design. Keep the server secret on your server: it is what lets UsherStats believe what your server says about its visitors. ## The snippet ```html ``` Put it in the `` of every page. Optional attributes: - `data-page-type="article"`: a short label for the kind of page, for the dashboard's page-type views. - `data-journeys="off"`: send pageviews and events only, not journeys. If your site sends a Content Security Policy, allow `https://collect.usherstats.com` in `script-src` and `connect-src`. ## A first-party path Served from your own domain, the script and its reports are first-party: nothing for a blocker to drop as a third party. Your server forwards `/_us/` to `https://collect.usherstats.com/`, and the snippet says so: ```html ``` **Your server must tell UsherStats who the visitor is.** Behind your proxy, UsherStats sees your server, not the visitor, and a server's network is a hosting network, which UsherStats counts as automated. So every forwarded request carries your server secret and the visitor's details: | Header | Value | |---|---| | `x-usher-proxy` | your server secret, `sk_live_…` | | `x-usher-client-ip` | the visitor's IP address | | `x-usher-client-country` | the visitor's two-letter country code | | `x-usher-client-asn` | the visitor's network number (AS number) | | `x-usher-client-as-org` | the visitor's network name, such as `Comcast Cable Communications, LLC` | UsherStats uses these only with a valid secret for that site. **If your server cannot supply the visitor's network (AS number and name), use the snippet directly instead**: otherwise your readers will be counted as hosting-network traffic. On Cloudflare, the Worker SDK fills all of them for you. Elsewhere, a GeoIP database with network data (for example MaxMind's GeoLite2 ASN and Country) provides them. ### Cloudflare Workers Use the [Worker SDK](#the-worker-sdk): it serves `/_us/` and adds the snippet to your pages for you. ### Next.js Most Next.js hosts do not tell your code the visitor's network, so for Next.js the simplest correct install is the direct snippet, in your root layout: ```jsx import Script from 'next/script'; export default function RootLayout({ children }) { return ( ``` Once a page with the snippet has been viewed, the install check says so (collect stamps the site at most once a minute): ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/install-status \ -H "Authorization: Bearer $US_TOKEN" ``` ## Hostnames Bare hostnames only (no scheme, path or port); they are lower-cased. Up to 50 per site. A hostname may be on sites of more than one workspace: collect only counts a page for a site whose key the page carries. Proxy mode is stricter: a proxied hostname belongs to exactly one site, and is switched on only after its ownership is verified (`verifiedAt`), through its Cloudflare custom hostname's validation or a DNS TXT record. ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/hostnames \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"hostname": "blog.fieldnotes.example"}' ``` Each hostname carries `verification`: the DNS record that proves the hostname is yours, a TXT record `_usherstats.` with the value `usherstats-verify=` (the token is this site's own for this hostname). Publish it at your DNS provider, then ask for the check. A verified hostname (`verifiedAt` set, `verification` null) is one the bot-protection challenge may send visitors back to, and one delegated Search Console access may read; an unverified one is still counted by collect. The check is limited to 30 an hour per site. ```sh curl -X POST https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/blog.fieldnotes.example/verify -H "Authorization: Bearer $US_TOKEN" ``` The answer's `verified` says whether the record was found; when it was not, `message` names the record to publish. Proxy-mode hostnames need no TXT record of ours: Cloudflare's validation of the custom hostname verifies them. One hostname: for a proxy-mode hostname, `proxy` is its live status at Cloudflare and the DNS records it still needs; for any other, `proxy` is null. ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/blog.fieldnotes.example \ -H "Authorization: Bearer $US_TOKEN" ``` To serve a hostname through [proxy mode](../proxy-mode.md), add it with `"proxy": true` and the `origin` its requests are forwarded to. The answer's `proxy.records` are the DNS records to set (a CNAME to the proxy; the TXT records let it go live before the DNS moves). Proxy hostnames count against the plan's allowance: paid plans are billed for each one past it, Free is refused with a 402; and a hostname another site already proxies is a 409. ```json { "hostname": "shop.fieldnotes.example", "proxy": true, "origin": "https://origin.fieldnotes.example" } ``` Removing a proxy-mode hostname removes it at Cloudflare as well. ```sh curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/blog.fieldnotes.example \ -H "Authorization: Bearer $US_TOKEN" ``` ## Keys A server secret, for the server SDK or proxy mode. `value` is in this answer only: ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/keys \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"kind": "secret"}' ``` Listing shows public keys in full and secrets by prefix: ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/keys \ -H "Authorization: Bearer $US_TOKEN" ``` Rotating makes a new key of the same kind and ends the old one at once, so deploy the new secret right after: ```sh curl -X POST https://api.usherstats.com/v1/sites/$SITE_ID/keys/$SECRET_ID/rotate \ -H "Authorization: Bearer $US_TOKEN" ``` ```sh curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/keys/$NEW_SECRET_ID \ -H "Authorization: Bearer $US_TOKEN" ``` ## Settings Four lists; a PATCH replaces the ones it names and keeps the rest. - `internalNetworks`: CIDRs (IPv4 or IPv6) whose traffic is your own, counted apart from readers. - `botWindows`: time windows (`from`, `to`, optional `country`, `network`, `note`) whose traffic is filed as a bot fleet after the fact. - `goals`: conversions, by event name (`"type": "event"`) or path (`"type": "path"`). - `experiments`: A/B tests by `key`, with at least two `variants` and a `status` of draft, running or stopped. ```sh curl -X PATCH https://api.usherstats.com/v1/sites/$SITE_ID/settings \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "internalNetworks": ["203.0.113.0/24", "2001:db8::/32"], "botWindows": [{"from": "2026-09-30T03:00:00Z", "to": "2026-09-30T03:10:00Z", "country": "CN", "note": "one fleet"}], "goals": [{"name": "Newsletter", "type": "event", "match": "subscribe"}], "experiments": [{"key": "hero-copy", "variants": ["control", "short"], "status": "running"}] }' ``` A value that does not fit is `400` naming the field, such as `internalNetworks[0]`: ```sh curl -X PATCH https://api.usherstats.com/v1/sites/$SITE_ID/settings \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"internalNetworks": ["10.0.0.0/33"]}' ``` ## Delete a site Its keys stop working at once. ```sh curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID \ -H "Authorization: Bearer $US_TOKEN" ``` ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID \ -H "Authorization: Bearer $US_TOKEN" ``` --- # Workspace and members A workspace holds sites and the people who work on them. Each person has a role, and the role decides their scopes: | Role | Scopes | |---|---| | owner | everything (`account:admin`) | | admin | everything except `billing:write` and deleting the account | | member | every `:read` scope, plus `sites:write`, `bot:write`, `search:write` | | viewer | `analytics:read`, `sites:read`, `bot:read`, `search:read` | Only an owner (or a token with `account:admin`) makes, changes or removes an owner, and the last owner can neither be demoted nor leave. Removing someone revokes every API token they made in the workspace; demoting them revokes those that hold scopes the new role lacks. Plans limit members, counting pending invitations: Free 2, Starter 5, Growth 15, Business unlimited (`402` past the limit). | Route | Scope | |---|---| | `GET /v1/workspace` | any member or token of the workspace | | `PATCH /v1/workspace` | `members:write` | | `GET /v1/members` | `members:read` | | `POST /v1/members/invites` | `members:write` | | `DELETE /v1/members/invites/{id}` | `members:write` | | `POST /v1/invites/accept` | the invited person, signed in | | `PATCH /v1/members/{userId}` | `members:write` | | `DELETE /v1/members/{userId}` | `members:write`, or your own id to leave | Changes to members are not available to site-restricted tokens. The examples assume `$US_TOKEN` holds a token with `account:admin`, and that Sam (`sam@example.com`) has an account and is signed in with `sam-cookies.txt`. ## The workspace ```sh curl https://api.usherstats.com/v1/workspace \ -H "Authorization: Bearer $US_TOKEN" ``` The answer includes the plan, its `limits` (`null` is unlimited) and current `usage`. ```sh curl -X PATCH https://api.usherstats.com/v1/workspace \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Field Notes Ltd"}' ``` ## Invite someone The invitation email links to `https://app.usherstats.com/invite?token=...`; it expires in 7 days. Withdraw one that is no longer wanted: ```sh curl https://api.usherstats.com/v1/members/invites \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "alex@example.com", "role": "viewer"}' ``` ```sh curl -X DELETE https://api.usherstats.com/v1/members/invites/$ALEX_INVITE \ -H "Authorization: Bearer $US_TOKEN" ``` ```sh curl https://api.usherstats.com/v1/members/invites \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "sam@example.com", "role": "member"}' ``` Sam accepts while signed in with the invited address (an invitation for another address is `404`): ```sh curl https://api.usherstats.com/v1/invites/accept -b sam-cookies.txt \ -H "Content-Type: application/json" \ --data @- < Sam now belongs to two workspaces, and picks this one per request with `X-Workspace`: ```sh curl https://api.usherstats.com/v1/me -b sam-cookies.txt \ -H "X-Workspace: $WORKSPACE_ID" ``` ## Members ```sh curl https://api.usherstats.com/v1/members \ -H "Authorization: Bearer $US_TOKEN" ``` ```sh curl -X PATCH https://api.usherstats.com/v1/members/$SAM_ID \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"role": "viewer"}' ``` The last owner cannot step down: ```sh curl -X PATCH https://api.usherstats.com/v1/members/$OWNER_ID \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"role": "admin"}' ``` Remove a member (or, with your own user id, leave): ```sh curl -X DELETE https://api.usherstats.com/v1/members/$SAM_ID \ -H "Authorization: Bearer $US_TOKEN" ``` --- --- title: Agents and MCP description: Give an AI agent access to UsherStats through the API, the OpenAPI description, the MCP server and these docs as Markdown. nav: Agents & MCP --- # Agents and MCP UsherStats treats AI agents as first-class users. An agent can do anything a person with the same scopes can, through the same API, and these docs are written to be read by agents as well as people. ## Give the agent a token Create an [API token](api-and-tokens.md#tokens) for the agent, and only for it: - the **fewest scopes** the job needs: `analytics:read` alone for an agent that reports on traffic; add `sites:write` or `bot:write` only for one that should change settings; - **limited to the sites** it works on; - with an **expiry**, if the job is temporary. Every change an agent makes is in the workspace's audit log under its token, and revoking the token stops it at once. ## The MCP server UsherStats provides a Model Context Protocol (MCP) server, so an MCP-capable assistant or agent can query your analytics and manage your sites as tools, using the same token and the same scopes as the API. Its address, the tools it offers and setup instructions for common clients are added to this page when it launches. ## The API directly Agents that call HTTP APIs can use the OpenAPI 3.1 description at `https://api.usherstats.com/v1/openapi.json`: it lists every route, its parameters, its scopes and its responses. Errors are `application/problem+json` with a `title` written to be acted on. ## Docs for agents - Every page of these docs is also served as Markdown: add `.md` to its address. - [llms.txt](https://docs.usherstats.com/llms.txt) lists every page with a one-line summary. - [llms-full.txt](https://docs.usherstats.com/llms-full.txt) is every page in one file. ## Good practice - Read before you write: have the agent show you a change (for example, new bot protection rules in `log` mode) before it applies it. - Keep tokens out of prompts and logs; give them to the agent's runtime as a secret. --- # Analytics Every figure in the dashboard comes from one query catalogue, and the API exposes it as it is: you name a **metric set**, a **dimension** and a few numbers, and UsherStats writes the query. There is no SQL to send and nothing to escape. The dashboard reads through these same routes. All of them need the `analytics:read` scope. A site in another workspace, or outside a token's site restriction, is `404`. | Route | Scope | |---|---| | `GET /v1/stats/catalogue` | `analytics:read` | | `GET /v1/sites/{id}/stats` | `analytics:read` | | `POST /v1/sites/{id}/stats/query` | `analytics:read` | | `POST /v1/sites/{id}/stats/batch` | `analytics:read` | | `GET /v1/sites/{id}/summary` | `analytics:read` | | `GET /v1/sites/{id}/top/{dimension}` | `analytics:read` | | `POST /v1/sites/{id}/internal-link` | `sites:write` | The examples assume `$US_TOKEN` holds a token with `account:admin`. First, a site to read: ```sh curl https://api.usherstats.com/v1/sites \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Field notes", "hostnames": ["fieldnotes.example"]}' ``` ## The catalogue Four metric sets: `pageviews` (confirmed pageviews in a browser), `sessions`, `requests` (everything your server or proxy answered, crawlers included) and `events`. Each lists its lanes (`human`, `bot`, `internal`), the dimensions it can be broken down by, the filters it takes and the counts it can sort by. ```sh curl https://api.usherstats.com/v1/stats/catalogue \ -H "Authorization: Bearer $US_TOKEN" ``` ## One query A spec is `metrics` and `by`, plus any of: - `lane`: `human` (People), `bot` (Crawlers) or `internal` (your own team). Each metric set has a default. - `days` (calendar days ending today; `1` is today so far, default `7`), or `from` and `to` (YYYY-MM-DD). - `compare`: `true` adds `previous`, the period before (a single day is compared with the same weekday a week earlier), or says why it cannot be compared yet. - `where`: filters, such as `{"source": "google"}`. In a query string, `where.source=google`. - `sort`: which count a table is ranked by, such as `sessions` or `pageviews`. Analytics Engine keeps 90 days; past that, UsherStats answers from the daily rollups each site keeps for as long as the account exists. Hourly series and page-to-page pairs are not rolled up, so they answer only within 90 days. ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/stats?metrics=pageviews&by=source&days=30&sort=sessions" \ -H "Authorization: Bearer $US_TOKEN" ``` The same as JSON (the form an agent's tool call takes): ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/stats/query \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"metrics": "requests", "by": "bot", "lane": "bot", "days": 7, "compare": true}' ``` A name that is not on the catalogue's lists is `400`, and `field` says which: ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/stats?metrics=pageviews&by=blob3" \ -H "Authorization: Bearer $US_TOKEN" ``` ## Many queries at once Up to 40 named specs in one call. A spec that is refused answers `{"error": {"status": 400, "title": ...}}` under its name; the others still answer. ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/stats/batch \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"queries": {"byDay": {"metrics": "pageviews", "by": "day", "days": 30}, "crawlers": {"metrics": "requests", "by": "category"}}}' ``` ## The People summary The People boxes as numbers: sessions, pageviews, from search, average session, bounce rate, single-page visits and pages per session, each with the previous period's figure and the change. A change carries a `tone`: `good` or `bad` where one direction is better (a lower bounce rate is better), `flat` when nothing moved. ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/summary?days=7" \ -H "Authorization: Bearer $US_TOKEN" ``` ## Top tables `source`, `medium`, `referrer`, `campaign`, `page`, `page_type`, `country` and `device` count People sessions (where each began) or, with `count=pageviews`, pageviews. `bot`, `category`, `kind`, `network`, `path`, `status`, `cache` and `decision` count requests; `event` counts events. ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/top/page?days=30&count=pageviews&limit=20" \ -H "Authorization: Bearer $US_TOKEN" ``` ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/top/bot?days=7" \ -H "Authorization: Bearer $US_TOKEN" ``` ## Marking your own browser Your team's visits belong in Internal, not People. Tools mark themselves with a signed header; a browser is marked by opening a link on your site's own first-party path (`/_us/`, which the Worker SDK and the first-party proxy set up). The link works for ten minutes, once per browser you open it in, and the mark lasts 90 days. `POST /v1/sites/{id}/internal-link/rotate` replaces the site's internal key, so every browser marked before stops counting as yours (mark them again). The link is made only for a hostname the site has verified (its TXT record, see [sites](sites.md)) or serves in proxy mode: it sets a cookie on that domain. Verify it first: ```sh curl -X POST https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/fieldnotes.example/verify \ -H "Authorization: Bearer $US_TOKEN" ``` ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/internal-link \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"hostname": "fieldnotes.example"}' ``` See also [goals and experiments](goals.md), [bot protection](bot.md), [search](search.md) and [export](export.md). --- # Bot protection settings A site's bot protection is a mode, what to do with declared bots no rule names, and a list of rules; how the gate uses them is in [Bot protection](../bot-protection.md). Reading needs `bot:read`, changing `bot:write`. What the gate decided also needs `analytics:read`. | Route | Scope | |---|---| | `GET /v1/sites/{id}/bot` | `bot:read` | | `PUT /v1/sites/{id}/bot` | `bot:write` | | `GET /v1/sites/{id}/bot/decisions` | `bot:read` | ```sh curl https://api.usherstats.com/v1/sites \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Shop", "hostnames": ["shop.example"]}' ``` A new site's protection is off: ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/bot \ -H "Authorization: Bearer $US_TOKEN" ``` ## Settings - `mode`: `off`, `log` (decide every request and record what it would have done, while serving everyone), `challenge` or `block`. Start in `log`. - `bots`: `allow` (the default), `challenge` or `deny`, for a declared bot no rule names. - `rules`: checked in order; the first that matches decides. Each has an `action` (`allow`, `challenge` or `deny`), an optional `label`, and a `match` naming at least one of `bot`, `category`, `ai`, `kind`, `verified`, `asn`, `country` and `path` (a prefix). Categories are `search-engine`, `ai-search`, `ai-assistant`, `ai-training`, `preview`, `seo`, `monitor`, `archiver`, `tool` and `other`. Every field named must hold. A rule applies to a verified search engine only when it names it, so a country or network rule cannot take your site out of search. `PUT` replaces all three: ```sh curl -X PUT https://api.usherstats.com/v1/sites/$SITE_ID/bot \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"mode": "log", "bots": "allow", "rules": [{"action": "deny", "label": "training crawlers", "match": {"category": "ai-training"}}, {"action": "challenge", "match": {"path": "/checkout", "asn": [16276]}}]}' ``` A field it cannot accept is `400`, with `field` naming it, such as `rules[0].match`: ```sh curl -X PUT https://api.usherstats.com/v1/sites/$SITE_ID/bot \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"mode": "log", "rules": [{"action": "deny", "match": {}}]}' ``` ## What it decided Every request but your own team's, files and refusals included, as `action` and `reason`, by crawler category and name, with totals per action. In `log` mode the reason reads `log: would challenge: ...`. ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/bot/decisions?days=7" \ -H "Authorization: Bearer $US_TOKEN" ``` --- # Export Everything UsherStats keeps for a site, on every plan; what each part holds is in [Exporting your data](../data-export.md). An export needs the `export:read` scope. | Route | Scope | |---|---| | `POST /v1/sites/{id}/export` | `export:read` | | `GET /v1/downloads/{token}` | none: the link is the credential | ```sh curl https://api.usherstats.com/v1/sites \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Shop", "hostnames": ["shop.example"]}' ``` ## Export a site The answer lists five files, each with a download link that works for an hour: - `settings.json`: the site as configured, with its hostnames, goals, experiments and bot protection rules; - `site-store.ndjson`: the daily totals, journeys, search weeks and settings UsherStats keeps per site, one JSON object per line, in the form a site can be loaded back from. Credentials (search keys, server secrets) are never exported: connect them again after a move; - `rollups.ndjson`: the daily totals alone; - `archive.json`: the site's raw records, one gzipped NDJSON object per site, kind and hour, each with its own link; - `manifest.json`: what is where, with counts. `from` and `to` (YYYY-MM-DD) limit the raw records listed; without them, all of them are. ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/export \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"from": "2026-09-01", "to": "2026-09-30"}' ``` ## Download A link needs no token: it names one file and cannot be changed into another. After an hour it answers `410`; export again for new links. ```sh curl "$DOWNLOAD_URL" ``` A link that was not made by an export is `404`: ```sh curl https://api.usherstats.com/v1/downloads/not-a-link ``` --- # Goals, experiments and behaviour Goals and A/B tests are part of a site's settings; their results come from the same data as the dashboard's Behaviour view. How to send events and mark experiments in a page is in [Goals and A/B tests](../goals-and-experiments.md). Reading the lists needs `sites:read`, changing them `sites:write`, and results `analytics:read`. | Route | Scope | |---|---| | `GET /v1/sites/{id}/goals` | `sites:read` | | `POST /v1/sites/{id}/goals` | `sites:write` | | `DELETE /v1/sites/{id}/goals/{goal}` | `sites:write` | | `GET /v1/sites/{id}/goals/results` | `analytics:read` | | `GET /v1/sites/{id}/experiments` | `sites:read` | | `POST /v1/sites/{id}/experiments` | `sites:write` | | `PATCH /v1/sites/{id}/experiments/{key}` | `sites:write` | | `DELETE /v1/sites/{id}/experiments/{key}` | `sites:write` | | `GET /v1/sites/{id}/experiments/{key}/results` | `analytics:read` | | `GET /v1/sites/{id}/journeys` | `analytics:read` | ```sh curl https://api.usherstats.com/v1/sites \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Shop", "hostnames": ["shop.example"]}' ``` ## Goals A goal is an event, by name (`"type": "event"`), or a page (`"type": "path"`; a trailing `*` matches every path that starts with the rest). Its `id`, made from its name, is what results use. ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/goals \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Signup", "type": "event", "match": "signup"}' ``` ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/goals \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Pricing page", "type": "path", "match": "/pricing*"}' ``` ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/goals \ -H "Authorization: Bearer $US_TOKEN" ``` Results count each goal's completions (the events sent, or the page's pageviews), and for an event goal the People sessions that reached it, out of all People visits: ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/goals/results?days=30" \ -H "Authorization: Bearer $US_TOKEN" ``` `DELETE` removes one by its id. A goal reads the events already stored, so one added today counts last week's sessions too. To replace every goal at once, `PATCH /v1/sites/{id}/settings` with a `goals` list. ```sh curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/goals/pricing-page -H "Authorization: Bearer $US_TOKEN" ``` ## Experiments An experiment's `key` is the `data-exp` name in your page. It needs at least two variants. ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/experiments \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"key": "hero-headline", "name": "Hero headline", "variants": ["a", "b"], "status": "running"}' ``` ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/experiments \ -H "Authorization: Bearer $US_TOKEN" ``` Results, variant by variant: the People sessions shown it, how many reached the goal (`?goal=`, a goal id; the first event goal by default), the rate, the events sent from it, and `p`, a two-proportion test between the two largest variants. A variant seen by fewer than five sessions is listed without its figures, and `p` waits until both of the two largest have five. ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/experiments/hero-headline/results?days=30&goal=signup" \ -H "Authorization: Bearer $US_TOKEN" ``` ```sh curl -X PATCH https://api.usherstats.com/v1/sites/$SITE_ID/experiments/hero-headline \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "stopped"}' ``` ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/experiments -H "Authorization: Bearer $US_TOKEN" -H "Content-Type: application/json" -d '{"key": "pricing-table", "variants": ["control", "annual-first"], "status": "draft"}' ``` ```sh curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/experiments/pricing-table \ -H "Authorization: Bearer $US_TOKEN" ``` ## Behaviour The Behaviour view's analysis: visits and goal rates, a Sankey of how sessions unfold (`?anchor=` centres it on one step), the common paths with their lift on a goal, and every A/B test. `?zoom=` is `page`, `section` (the default) or `element`; `?who=` is `human` (the default) or `internal`, your own team's sessions, for QA. A group of fewer than five sessions is never named. ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/journeys?days=7&zoom=page" \ -H "Authorization: Bearer $US_TOKEN" ``` --- # Search connections Google Search Console and Bing Webmaster Tools, connected with access you give and can take back; the setup on Google's and Bing's side is in [Search Console and Bing](../search-console-and-bing.md). Credentials are checked, encrypted, and used only by the daily pull of your site's own figures; no route shows a key again. Reading needs `search:read`, connecting `search:write`. | Route | Scope | |---|---| | `GET /v1/sites/{id}/search` | `search:read` | | `PUT /v1/sites/{id}/search/google` | `search:write` | | `PUT /v1/sites/{id}/search/bing` | `search:write` | | `DELETE /v1/sites/{id}/search/google` | `search:write` | | `DELETE /v1/sites/{id}/search/bing` | `search:write` | | `GET /v1/sites/{id}/search/weeks` | `search:read` | ```sh curl https://api.usherstats.com/v1/sites \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Shop", "hostnames": ["shop.example"]}' ``` What is connected, and `serviceAccount`, the address to add to your Search Console property for delegated access: ```sh curl https://api.usherstats.com/v1/sites/$SITE_ID/search \ -H "Authorization: Bearer $US_TOKEN" ``` ## Google Delegated: add the `serviceAccount` address to the property (Restricted permission), then connect it. Delegated access reads with UsherStats' own account, which every customer's property shares, so it is accepted only for a property whose host is a **verified** hostname of the site (`sc-domain:shop.example` needs `shop.example`; a URL-prefix property needs its own hostname). Before that, it is `400` with `"field": "property"`: ```sh curl -X PUT https://api.usherstats.com/v1/sites/$SITE_ID/search/google \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"mode": "delegated", "property": "sc-domain:shop.example"}' ``` Or with your own service account: `"mode": "service_account"` and the key file's JSON as `"key"` (text or an object). It reads only what you gave that account, so it needs no verified hostname. A file that is not a service-account key is `400` with `"field": "key"`: ```sh curl -X PUT https://api.usherstats.com/v1/sites/$SITE_ID/search/google \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"mode": "service_account", "property": "sc-domain:shop.example", "key": "{\"type\": \"authorized_user\"}"}' ``` ## Bing ```sh curl -X PUT https://api.usherstats.com/v1/sites/$SITE_ID/search/bing \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"apiKey": "your-bing-api-key", "siteUrl": "https://shop.example/"}' ``` ## The figures One row per week (starting Monday) and source: clicks, impressions, click-through rate, average position, how many pages and queries were shown, and Bing's inbound links. The current week fills in day by day. A figure that could not be collected is `null`; a zero is a measurement. ```sh curl "https://api.usherstats.com/v1/sites/$SITE_ID/search/weeks?weeks=26" \ -H "Authorization: Bearer $US_TOKEN" ``` ## Disconnecting The credentials are deleted; the weeks already collected stay. ```sh curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/search/bing \ -H "Authorization: Bearer $US_TOKEN" ``` Disconnecting a source that is not connected is `404`: ```sh curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/search/google \ -H "Authorization: Bearer $US_TOKEN" ``` --- --- title: Exporting your data description: Export everything UsherStats holds for your sites, raw records and daily totals, from the dashboard or the API. nav: Data export --- # Exporting your data Your data is yours. UsherStats keeps it for as long as your account exists and lets you take all of it out at any time, on every plan. ## What you can export - **Raw records**, as UsherStats keeps them: - **pageviews**: each confirmed pageview, with its page, source, country, device, scroll depth and time on page; - **events**: each event and goal, with its name, label and value, and A/B exposures; - **requests**: each request your server or proxy recorded, with the crawler or browser it came from and what bot protection decided. - **Daily totals** behind the dashboards. - **Settings**: sites, hostnames, goals, experiments and bot protection rules. Raw records are newline-delimited JSON, compressed with gzip, one file per site, kind and hour: the same rows the archive keeps, so an export is complete rather than a summary. ## How - In the dashboard: **Settings → Export**, choose the sites and the date range. - Through the API, with a token that has the `export:read` scope. The routes are in the API reference (`GET /v1/openapi.json`); see [API and tokens](api-and-tokens.md). ## Limits - An export answers with download links that work for an hour, and only while whoever made the export can still export the site: removing a member, revoking a token or deleting the site ends its links. - The exported files are kept for a day, then removed; export again for a fresh copy. - A site can be exported 5 times an hour, and a workspace 20 times an hour. - A date range of up to a month lists exactly its days of raw records; a longer one lists up to 20,000 files and says when it stopped short, so take a long history a range at a time. ## Deleting data A site's data can be deleted, or given a retention period after which older records are removed, in the site's settings. Closing your account deletes all of its sites' data. Export first if you want to keep a copy. --- --- description: What UsherStats records about your visitors and what it does not, cookies, Global Privacy Control, GDPR roles, and what to tell your readers. nav: Privacy & compliance --- # Privacy and compliance UsherStats is built to measure a site without following the people who read it. ## What is recorded For each confirmed pageview: the page's path; the `utm_source`, `utm_medium` and `utm_campaign` tags (and no other part of the address); the referring site's hostname (not its full address); the browser window's width, language and time zone; scroll depth, time on page and whether the reader interacted; and a page counter for the tab. From the request, UsherStats derives the country, the network operator, a device class and a summary of the connection's encryption settings. With the Worker SDK or proxy mode, each request your server answers is recorded too: its path, status, and the kind of client that made it. ## What is not - **No cookies for analytics**, and no identifier that follows a reader from one site to another, or from one day to the next. The page counter lives in the tab's session storage, which the browser clears when the tab closes. - **No IP addresses and no full browser identification strings** are stored. They are used while a request is handled, to classify it, and then dropped. - **No names, email addresses or form contents.** Never put them in event names or labels either. - **Global Privacy Control**: browsers that send it are not recorded at all. Two cookies can exist on your domain, both first-party and both outside analytics: - `us_pass`, set only on a visitor who has just completed a [bot protection](bot-protection.md) check, so they are not asked again for a day; - the internal-traffic cookie, set only in your own team members' browsers when they mark them as internal (see [Concepts](concepts.md#internal-traffic)). ## GDPR roles For your visitors' data, you are the **controller** and UsherStats is your **processor**. Our [data processing agreement](https://usherstats.com/dpa) sets out the processor terms (GDPR Article 28) and covers transfers with the Standard Contractual Clauses. The companies we use are on the [subprocessors](https://usherstats.com/subprocessors) page, and the [privacy policy](https://usherstats.com/privacy) describes everything in full. ## What to put in your own privacy policy You can describe UsherStats along these lines, adjusted to how you use it: > We use UsherStats to measure how our site is used. It sets no cookies for analytics, stores no personal data and no > IP addresses, and does not follow you across sites or days. It records the page you visited, the site that referred > you, campaign tags, your browser's language, time zone and window size, your country and network, and how long you > stayed. Browsers that send Global Privacy Control are not recorded. UsherStats processes this data on our behalf. If you use bot protection, add that requests are checked for automated traffic and some visitors may be asked to complete a short check, which sets a cookie for a day. Whether you need consent for analytics depends on where you and your readers are and how you use the data; this page is not legal advice. ## Data retention and deletion Data is kept for as long as your account exists unless you delete it or set a retention period for a site. See [Exporting your data](data-export.md). ## Contact Privacy questions: privacy@usherstats.com. --- --- description: UsherStats plans, what counts towards them, overage, and how billing works. nav: Billing & plans --- # Billing and plans Every feature is on every plan, Free included. Plans differ only by volume. | | Free | Starter | Growth | Business | |---|---|---|---|---| | Price (monthly / yearly) | $0 | $9 / $90 | $29 / $290 | $99 / $990 | | Human pageviews / month | 100k | 250k | 1M | 5M | | Server requests / month (server SDK + proxy) | 1M | 5M | 20M | 75M | | Sites | 5 | 20 | unlimited | unlimited | | Proxy-mode hostnames | 1 | 3 | 10 | 50 | | Team members | 2 | 5 | 15 | unlimited | | Every feature (analytics, crawler/AI analytics, journeys, A/B, bot protection, Search Console, API, MCP) | yes | yes | yes | yes | | Data kept | forever | forever | forever | forever | | Support | docs + community | email | email, 2 business days | priority email | ## What counts - **Human pageviews**: pageviews counted under People. Crawlers, hosting-network visits and your own team's visits do not count against this allowance. - **Server requests**: requests recorded by the [Worker SDK](installing.md#the-worker-sdk) or [proxy mode](proxy-mode.md), people and bots alike. - Allowances are per workspace, across all of its sites, and reset at the start of each billing month. ## Going over - Paid plans: **$10 per extra 1M pageviews**, **$1 per extra 1M server requests**, and **$1 per extra proxy hostname per month**, billed at the end of the month. Nothing is ever dropped for volume on a paid plan. - Free never bills. It emails the workspace's owners at 80% and 100% of its allowance. Past 150% of its server requests, server-request logging pauses until the month turns; pageviews keep counting. ## Paying Plans are paid by card through Stripe, monthly or yearly (a year costs ten months). Change or cancel your plan, update your card and download invoices under **Settings → Billing**; owners have the `billing:write` scope needed to change it. A cancelled plan runs to the end of the period you paid for; your data stays. --- --- description: Fixes for missing pageviews, missing crawler data, real people being challenged, and empty Search data. --- # Troubleshooting ## No pageviews arrive Check, in order: 1. **The hostname.** UsherStats accepts reports only from pages on the site's hostnames. If your site answers on both `example.com` and `www.example.com`, add both in the site's settings. 2. **The key.** The snippet's `data-site` must be the site's public key exactly: `pk_` and 26 letters and digits. A malformed key makes the script do nothing at all. 3. **Your browser.** Browsers that send Global Privacy Control (Brave and DuckDuckGo by default, and some extensions) are not recorded. Test with another. 4. **Leave the page.** A page reports when it is hidden or closed, not when it loads. Switch tabs or close it, then look. 5. **Content Security Policy.** If your site sends one, allow `https://collect.usherstats.com` in `script-src` and `connect-src`, or use a [first-party path](installing.md#a-first-party-path). 6. **Blockers.** Some blocking extensions drop third-party analytics. A first-party path avoids that. ## My visits show under Crawlers or Internal, not People That is usually right: - **Internal**: your browser is marked as internal, or you are on a network listed in the site's internal networks. - **Crawlers**: you are browsing from a hosting network (many VPNs run on cloud servers), your computer reports its time zone as `UTC`, or the session went past twenty pages. See [Concepts](concepts.md#what-counts-as-a-pageview). ## People is empty behind my own proxy A first-party path on your own server must send your server secret and the visitor's IP address, country and network (AS number and name). Without them UsherStats sees your server, a hosting network, and counts every reader as automated. See [A first-party path](installing.md#a-first-party-path), or use the snippet directly. ## A single-page app shows one pageview per visit The snippet reports one pageview per full page load. Navigations inside a single-page app are not reported as separate pageviews yet. ## The Crawlers view is empty Crawlers do not run scripts, so the snippet cannot see them. Install the [Worker SDK](installing.md#the-worker-sdk) or use [proxy mode](proxy-mode.md). If you have and it is still empty, check that the Worker's `USHERSTATS_SECRET` is the site's current server secret. On Free, server-request logging pauses past 150% of the month's allowance; see [Billing and plans](billing-and-plans.md). ## Real people are being challenged Switch the site to `log` mode, read the **Bot protection** view to see which rule or signal is responsible, and adjust your rules. See [Bot protection](bot-protection.md). ## Search data is missing - The service account (yours or ours) must be a user of the Search Console property, with at least Restricted access. - The property must match your site: a domain property covers every hostname; a URL-prefix property only its prefix. - Search engines publish data a few days late, so the most recent days fill in later. See [Search Console and Bing setup](search-console-and-bing.md). ## Still stuck Write to support@usherstats.com with your site's name and what you expected to see. --- # UsherStats for AI agents (MCP) UsherStats is a remote [Model Context Protocol](https://modelcontextprotocol.io) server. Point any MCP client -- Claude Code, Claude Desktop, Cursor, or your own agent -- at ```text https://api.usherstats.com/mcp ``` with an API token, and the agent can do what the token can: list and create sites, add hostnames, read the install snippet and status, manage keys, settings, tokens and team members. Every tool *is* an API route, called as your token, so scopes, site restrictions and plan limits apply exactly as they do over HTTP. Analytics tools (stats, top pages and sources, crawlers, journeys, experiments, bot rules, search data) appear here as soon as those API routes ship, with no change on your side. - **Endpoint:** `POST /mcp` (Streamable HTTP, JSON-RPC 2.0, one JSON answer per request, no sessions). - **Protocol versions:** `2026-07-28` (stateless: no `initialize`; version in each request's `_meta` and the `MCP-Protocol-Version` header) and `2025-03-26`, `2025-06-18`, `2025-11-25` (opening with `initialize`). - **Authentication:** `Authorization: Bearer us_live_...` only. Sign-in cookies are not accepted on `/mcp`. - **The HTTP API** behind the tools is described at [https://api.usherstats.com/v1/openapi.json](https://api.usherstats.com/v1/openapi.json) (OpenAPI 3.1). ## 1. Make a token for the agent Give the agent only what it needs. A token can be limited to some sites (`siteIds`) and given an expiry; it is shown once. For an agent that reports and helps with setup but changes nothing: ```sh curl https://api.usherstats.com/v1/tokens \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "claude (read only)", "scopes": ["sites:read", "analytics:read", "members:read"], "expiresAt": "2027-06-30T00:00:00Z"}' ``` For an agent that may also set sites up (create sites, add hostnames, change settings), add `sites:write`: ```sh curl https://api.usherstats.com/v1/tokens \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "claude (setup)", "scopes": ["sites:read", "sites:write", "analytics:read"], "expiresAt": "2027-06-30T00:00:00Z"}' ``` You can also make tokens in the dashboard under **Settings > API tokens**. Revoke one with `DELETE /v1/tokens/{id}`; it stops working at once. ## 2. Connect your client Keep the token out of files you commit: each snippet below reads it from the environment variable `US_TOKEN`. ### Claude Code ```sh claude mcp add --transport http usherstats https://api.usherstats.com/mcp --header "Authorization: Bearer $US_TOKEN" ``` Or share it with a project in `.mcp.json` (Claude Code expands `${US_TOKEN}` from the environment): ```json { "mcpServers": { "usherstats": { "type": "http", "url": "https://api.usherstats.com/mcp", "headers": { "Authorization": "Bearer ${US_TOKEN}" } } } } ``` ### Claude Desktop Claude Desktop starts local servers from `claude_desktop_config.json`; the `mcp-remote` bridge connects it to a remote server with a header. Put the token in `env`, not in the arguments: ```json { "mcpServers": { "usherstats": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.usherstats.com/mcp", "--header", "Authorization:${US_AUTH}"], "env": { "US_AUTH": "Bearer us_live_..." } } } } ``` ### Cursor In `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (one project): ```json { "mcpServers": { "usherstats": { "url": "https://api.usherstats.com/mcp", "headers": { "Authorization": "Bearer ${env:US_TOKEN}" } } } } ``` ### Any other MCP client Configure a **Streamable HTTP** (sometimes "HTTP" or "remote") server with the URL `https://api.usherstats.com/mcp` and the request header `Authorization: Bearer `. The server does not use OAuth; a client that only supports OAuth for remote servers can use the `mcp-remote` bridge as in the Claude Desktop example. ## 3. What to ask - "Which of my sites have the UsherStats snippet installed, and when did each last send data?" - "Create a site called Docs for docs.example.com in Europe/Bucharest and give me the snippet to paste." - "Add shop.example.com as a hostname of my Shop site." - "Mark 203.0.113.0/24 as our office network on every site so our own visits are not counted." - "Add a goal named Signup on the path /welcome for the Shop site." - "Who is on the team, and which invitations are still pending?" - "List the API tokens nobody has used in the last 30 days." The agent starts with `get_me` (what the token may do) and `list_sites`; every id it needs comes from a `list_` or `get_` tool. When a call is refused, the tool error's first line says why and what to do -- for example `This needs the "sites:write" scope, which this API token does not have` -- so the agent can tell you, rather than guess. **Retries.** Every tool that changes something takes an optional `idempotencyKey`. An agent that repeats a call with the same key within 24 hours (after a timeout, say) gets the first result back instead of creating a second site or token. The same key works over HTTP as the `Idempotency-Key` header. **Limits.** A token makes up to 600 calls a minute, and every tool call counts, including each one inside a JSON-RPC batch (protocol version 2025-03-26 only). A batch carries at most 20 messages. ## Tools One tool per API route a token can call. The list is generated from the routes, so it is always exactly what the API serves; `tools/list` returns each tool's input schema (JSON Schema) and, for tools that return data, its output schema. | Tool | Scope | |---|---| | `get_me` | none | | `get_workspace` | `members:read` | | `update_workspace` | `members:write` | | `list_members` | `members:read` | | `create_member_invite` | `members:write` | | `delete_member_invite` | `members:write` | | `update_member` | `members:write` | | `delete_member` | `members:write` | | `list_tokens` | `tokens:read` | | `create_token` | `tokens:write` | | `delete_token` | `tokens:write` | | `list_sites` | `sites:read` | | `create_site` | `sites:write` | | `get_site` | `sites:read` | | `update_site` | `sites:write` | | `delete_site` | `sites:write` | | `create_site_hostname` | `sites:write` | | `get_site_hostname` | `sites:read` | | `verify_site_hostname` | `sites:write` | | `delete_site_hostname` | `sites:write` | | `list_site_keys` | `sites:read` | | `create_site_key` | `sites:write` | | `rotate_site_key` | `sites:write` | | `delete_site_key` | `sites:write` | | `get_site_snippet` | `sites:read` | | `get_site_install_status` | `sites:read` | | `update_site_settings` | `sites:write` | | `get_stats_catalogue` | `analytics:read` | | `get_site_stats` | `analytics:read` | | `query_site_stats` | `analytics:read` | | `batch_site_stats` | `analytics:read` | | `get_site_summary` | `analytics:read` | | `get_site_top` | `analytics:read` | | `list_site_goals` | `sites:read` | | `create_site_goal` | `sites:write` | | `delete_site_goal` | `sites:write` | | `get_site_goal_results` | `analytics:read` | | `list_site_experiments` | `sites:read` | | `create_site_experiment` | `sites:write` | | `update_site_experiment` | `sites:write` | | `delete_site_experiment` | `sites:write` | | `get_site_experiment_results` | `analytics:read` | | `get_site_journeys` | `analytics:read` | | `get_site_bot` | `bot:read` | | `update_site_bot` | `bot:write` | | `get_site_bot_decisions` | `bot:read` | | `get_site_search` | `search:read` | | `update_site_search_google` | `search:write` | | `update_site_search_bing` | `search:write` | | `delete_site_search_google` | `search:write` | | `delete_site_search_bing` | `search:write` | | `get_site_search_weeks` | `search:read` | | `internal_link_site` | `sites:write` | | `rotate_site_internal_link` | `sites:write` | | `export_site` | `export:read` | | `get_billing` | `billing:read` | | `checkout_billing` | `billing:write` | | `portal_billing` | `billing:write` | | `plan_billing` | `billing:write` | Not tools: sign-up, sign-in, passwords, sessions and two-factor setup (a person does these; an agent uses a token), accepting an invitation (it joins a signed-in person to a workspace), and the OpenAPI document (fetch it over HTTP). ## The protocol, by hand What a client sends, for anyone writing their own. Each request is its own `POST /mcp`. With `2026-07-28`, the version and client capabilities travel in `params._meta`, and the method (and, for `tools/call`, the tool name) are mirrored in headers; a header that disagrees with the body is refused with `400` and error `-32020`. Discover the server: supported versions, capabilities and instructions for the model. ```sh curl https://api.usherstats.com/mcp \ -H "Authorization: Bearer $AGENT_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: server/discover" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}' ``` List the tools: ```sh curl https://api.usherstats.com/mcp \ -H "Authorization: Bearer $AGENT_TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: $VERSION" \ -H "Mcp-Method: tools/list" \ -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}' ``` Call one. The result has the route's answer as JSON text in `content` and as `structuredContent`: ```sh curl https://api.usherstats.com/mcp \ -H "Authorization: Bearer $AGENT_TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: create_site" \ -d '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "create_site", "arguments": {"name": "Docs", "hostnames": ["docs.example.com"], "idempotencyKey": "create-docs-1"}, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}' ``` ```sh curl https://api.usherstats.com/mcp \ -H "Authorization: Bearer $AGENT_TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_site_snippet" \ --data @- < A refusal is a tool result with `"isError": true` whose text starts with what to do. The read-only token cannot create sites: ```sh curl https://api.usherstats.com/mcp \ -H "Authorization: Bearer $READ_TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: create_site" \ -d '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "create_site", "arguments": {"name": "Nope"}, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}' ``` Without a token, `/mcp` answers `401` (as `application/problem+json`, with `WWW-Authenticate: Bearer`): ```sh curl https://api.usherstats.com/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "id": 6, "method": "tools/list"}' ``` A client on an earlier protocol version opens with `initialize`; no session id is issued, so every later request stands alone (send `MCP-Protocol-Version` with the negotiated version): ```sh curl https://api.usherstats.com/mcp \ -H "Authorization: Bearer $AGENT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "id": 7, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "my-agent", "version": "1.0"}}}' ``` ```sh curl https://api.usherstats.com/mcp \ -H "Authorization: Bearer $AGENT_TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: $NEGOTIATED" \ -d '{"jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": {"name": "list_sites", "arguments": {}}}' ``` `GET /mcp` and `DELETE /mcp` answer `405`: there is no standalone event stream and no session to end. --- # Plans and billing Every plan has every feature: analytics, crawler and AI-bot analytics, journeys, A/B tests, bot protection, Search Console, the API and MCP. Plans differ only by volume, and data is kept forever on all of them. | | Free | Starter | Growth | Business | |---|---|---|---|---| | Price (monthly / yearly) | $0 | $9 / $90 | $29 / $290 | $99 / $990 | | Human pageviews / month | 100k | 250k | 1M | 5M | | Server requests / month | 1M | 5M | 20M | 75M | | Sites | 5 | 20 | unlimited | unlimited | | Proxy-mode hostnames | 1 | 3 | 10 | 50 | | Team members | 2 | 5 | 15 | unlimited | Months are calendar months in UTC. Paid subscriptions renew on the 1st, so a billing period is the month that usage is counted in; the first payment covers the rest of the current month, pro rata. ## Above the allowance On a paid plan nothing is dropped. Usage above the allowance is billed at the end of the period, pro rata: - **$10 per extra 1M pageviews** (one cent per 1,000), - **$1 per extra 1M server requests** (one cent per 10,000), - **$1 per extra proxy hostname per month**, for each month it exists. Worked examples, for one month: | Plan | Pageviews | Server requests | Proxy hostnames | Overage | |---|---|---|---|---| | Starter | 250,000 | 5,000,000 | 3 | $0.00 | | Starter | 400,000 | 5,000,000 | 3 | $1.50 | | Growth | 1,500,000 | 22,000,000 | 10 | $7.00 | | Business | 5,000,000 | 80,000,000 | 52 | $7.00 | | Free | 900,000 | 3,000,000 | 1 | $0.00 | **Free never bills.** The owners get an email at 80% and at 100% of an allowance. Pageviews keep counting past 100%; past 150% of its server requests, Free stops logging server requests until the month turns. Paid plans get the 80% and 100% emails too, as a heads-up. ## Your plan and usage `GET /v1/billing` (scope `billing:read`, which owners and admins have) answers the plan, the subscription's status, this month's usage, the overage so far in cents, and a projection to the end of the month. ```sh curl https://api.usherstats.com/v1/billing \ -H "Authorization: Bearer $US_TOKEN" ``` ## Subscribe Paying is the owner's: it needs `billing:write`, which only owners have (and tokens they make with it). Tokens restricted to some sites cannot change billing. `POST /v1/billing/checkout` answers the URL of a Stripe Checkout page for the plan, monthly or yearly. The plan changes when Stripe confirms the payment, not before. ```sh curl https://api.usherstats.com/v1/billing/checkout \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan": "growth", "interval": "month"}' ``` Until the payment is confirmed there is no subscription to change, so a plan change is refused: ```sh curl https://api.usherstats.com/v1/billing/plan \ -H "Authorization: Bearer $US_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan": "starter"}' ``` ## Change plan, update the card, cancel `POST /v1/billing/plan` changes the plan of a subscription: - an **upgrade** applies once the difference for the rest of the period, invoiced immediately, is paid; if the card is declined the answer is `402` and the plan stays as it was; - a **downgrade** applies when the period you paid for ends; until then you keep the larger plan; - `{"plan": "free"}` **cancels** at the end of the period. Nothing is deleted: the workspace moves to Free and keeps its data. Choosing a plan again before the period ends withdraws the cancellation. Switching between monthly and yearly billing in place is not available yet: cancel, and subscribe again when the period ends. `POST /v1/billing/portal` answers a link to Stripe's customer portal, where you update the card, download invoices and receipts, or cancel at the end of the period. It needs a billing account, which the first subscription creates: ```sh curl -X POST https://api.usherstats.com/v1/billing/portal \ -H "Authorization: Bearer $US_TOKEN" ``` ## When a payment fails The owners get an email with a link to pay the invoice, and nothing changes for 7 days. If the invoice is still unpaid then, the workspace moves to Free (data kept, collection continues within Free's allowances) and the owners are told. Paying the invoice later restores the plan. ## Stripe's webhook `POST /v1/billing/webhook` is for Stripe only. It accepts a request only with a valid `Stripe-Signature` made in the last 5 minutes, and applies each event once: ```sh curl https://api.usherstats.com/v1/billing/webhook \ -H "Content-Type: application/json" \ -d '{"id": "evt_1", "type": "customer.subscription.deleted"}' ``` | Route | Scope | |---|---| | `GET /v1/billing` | `billing:read` | | `POST /v1/billing/checkout` | `billing:write` (not for site-restricted tokens) | | `POST /v1/billing/portal` | `billing:write` (not for site-restricted tokens) | | `POST /v1/billing/plan` | `billing:write` (not for site-restricted tokens) | | `POST /v1/billing/webhook` | none; Stripe's signature |