# Analytics Platform > A privacy-focused alternative to Google Analytics. Its full capability — traffic, acquisition, revenue, retention, journeys, Core Web Vitals, visitor profiles, plus management of sites, goals, funnels, segments, alerts, webhooks, annotations and scheduled reports — is available through a REST API and a remote MCP server that share one authentication and permission model. Live app: https://analytics.appfor.you REST base URL: https://analytics.appfor.you/api/v2 MCP endpoint: https://analytics.appfor.you/api/mcp ## Documentation - [Human-readable API & MCP reference](https://analytics.appfor.you/docs): every endpoint with parameters, curl examples and response samples, plus MCP client setup. - [OpenAPI 3.1 specification](https://analytics.appfor.you/openapi.json): machine-readable schema for all 56 REST endpoints — feed this to a client generator. - [MCP setup instructions](https://analytics.appfor.you/docs#mcp-connect): copy-pasteable configuration for Claude Code, Claude Desktop and Cursor. - [Destructive-operation rules](https://analytics.appfor.you/docs#destructive): read this before calling any DELETE. ## Authentication Send an API key as a bearer token on every request, REST and MCP alike: ``` Authorization: Bearer ``` Users create keys at Dashboard → Settings → API Keys. Keys start with `ap_`, are displayed once at creation and stored only as a SHA-256 hash. A key carries scopes, an optional restriction to specific sites, and an optional hard expiry. Scopes are hierarchical — admin implies write implies read: - `read` — Every GET endpoint. Read-only access to sites and analytics. This is the default: API keys created before scopes existed, or created without an explicit scope list, behave exactly as read-only keys. - `write` — Everything `read` grants, plus POST and PATCH. No DELETE at all. Create and modify sites, goals, funnels, segments, alerts, webhooks, annotations and scheduled reports. Every DELETE in this API needs `admin` — a `write` key can build and edit, but can never remove. - `admin` — Everything `write` grants, plus every DELETE. Required to delete a site, goal, funnel, segment, alert, webhook, annotation or scheduled report. Every one of those also requires an explicit `confirm` value repeating the resource id — the scope alone is never enough. Keys issued before scopes existed normalise to `read` at read time, so read-only integrations keep working unchanged. A site restriction may name any site its creator can reach — sites they own and sites they joined as an accepted team member. An unrestricted key covers everything its owner can access. ## Conventions - `{siteId}` accepts either the Mongo `_id` returned as `siteId`, or the public `trackingId` from the tracking snippet. - List endpoints take `limit` (default 50, max 500) and `offset`, and return `total`. - Time windows use a `range` preset plus optional `from`/`to` overrides. The accepted presets differ per endpoint: the goal and funnel endpoints accept either a full ISO 8601 instant or a plain date, while the acquisition, revenue, journey, vitals and profile endpoints take YYYY-MM-DD and only honour `from` and `to` when BOTH are supplied. - Date-only bounds cover WHOLE days: `from=YYYY-MM-DD` starts at `T00:00:00.000Z` and `to=YYYY-MM-DD` runs through `T23:59:59.999Z`, so the final day is included and `from=X&to=X` is a full 24 hours. (It previously produced a zero-width window that always returned nothing — do not rely on that any more.) A value carrying a time is an instant and is used verbatim. - Anchoring: neither bound → `[now - range, now]`; `from` only → `[from, now]`; `to` only → `[to - range, to]`; both → `[from, to]`. An upper bound in the future is clamped to now, except where that would invert the window. A window lying WHOLLY in the future is rejected with 400 everywhere, because it can never contain data. - Most windowed endpoints echo the resolved window back as an OBJECT: `range: { since, until }`, or `range: { preset, since, until }` on `GET /stats` and `GET /goals`. Two endpoints still emit a flat `range` STRING alongside top-level `since`/`until` — goal detail and funnel stats. A few endpoints echo no window at all (`/pageviews`, `/events`, `/realtime`, `/retention`); `/profiles` echoes one only when a date parameter was supplied, and adds `field: "lastSeenAt"`. - An unusable window (an unparseable `from`/`to`, or one that ends before it starts) is a 400 on ALL 11 endpoints that accept one — paths relative to `/api/v2/sites/{siteId}`: `GET /goals`, `GET /goals/{goalId}`, `GET /funnels/{funnelId}/stats`, `GET /channels`, `GET /journeys`, `GET /vitals`, `GET /profiles`, `GET /revenue`, `GET /revenue/sources`, `GET /revenue/products`, `GET /revenue/campaigns`. Nothing silently falls back to its `range` preset any more, so a successful response always describes the window you asked for. - GET /retention and GET /annotations validate their own from/to and answer 400 with their own wording. - GET /stats, GET /pageviews and GET /events accept only a range preset, and GET /realtime has a fixed 5-minute window, so none of them has a from/to to reject. - An unrecognised range preset is a 400 everywhere; no endpoint silently substitutes its default any more. - Errors are always `{ "error": string }` with a meaningful HTTP status. - PATCH bodies take only the fields to change and require at least one. Array/object fields (funnel `steps`, webhook `events`, segment `filters`) are REPLACED, not merged. - Team members with the `viewer` role are read-only (403 on writes). Only the site owner may delete a site, and only the owner may change `isPublic` — a `PATCH /api/v2/sites/{siteId}` carrying that field from a team member, admin role included, returns 403 before any field is applied, because publishing exposes the site on an unauthenticated `/share/` URL. - Alerts, webhooks, reports and segments are per-user: a key never lists another user’s. Annotations are readable by everyone with site access but editable only by their author. - Rate limit: 100 requests per minute per API-key owner, shared across all of that account's keys and across REST + MCP. Successful responses and the 429 carry X-RateLimit-* headers; other errors do not. See the Rate limits section. ## Rate limits - 100 requests per minute, counted per API-KEY OWNER rather than per key: every key on an account draws from one budget, and REST and MCP traffic share it. - Counters live in MongoDB — one document per (bucket, one-minute window), incremented with a single atomic upsert and removed by a TTL index. The budget is therefore shared by every application replica and survives a redeploy; it is not a per-process counter. - If the database is unreachable or slow the limiter degrades to an in-process counter instead of failing the request. While degraded the limit is still enforced, but per replica — so the effective ceiling across a multi-replica deployment can briefly be higher than the documented number. It never fails open entirely. - Every SUCCESSFUL response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, and so does the 429 you get once the budget is gone — which additionally carries `Retry-After` in seconds. The other error responses (400, 401, 403, 404, 500, 503) are raised outside the limiter and deliberately carry none of them, so track your budget from a success or from the 429 itself. `X-RateLimit-Reset` is a Unix timestamp in MILLISECONDS. - Failed authentication has its own, much tighter budget keyed on the client IP: 20 failures per minute, or 200 for the shared `unknown` bucket used when no usable client address reaches the app. The budget is CONSUMED only once a credential has been looked up and found bad, so a request presenting a valid key is never charged for a neighbour behind the same address; a request carrying no credential at all is rejected up front as soon as that bucket is empty, with zero I/O. What this budget does NOT do: it does not eliminate the per-guess key lookup, and once an address is over budget a wrong key still answers 429 while a correct one still authenticates. It is not a defence against a determined guesser — API-key entropy is, backed by the owner-level budget that applies the moment a key is valid. - The address a failure is charged to is taken from `x-forwarded-for` counting TRUSTED_PROXY_HOPS entries from the END (default 1 — the address written by the reverse proxy directly in front of the app), and clamping to the leftmost entry (with a one-time warning logged) when the chain is shorter than the configured hop count. `x-real-ip` is consulted only when the deployment sets `TRUST_X_REAL_IP=true` (default false) — it is otherwise plain client input that would let a caller pin its bucket to a victim address. Anything that is not a valid IP address is discarded into the shared `unknown` bucket instead of becoming a key, and IPv6 addresses are canonicalized (RFC 5952) before keying so alternative spellings of one address share one bucket. This governs failed-auth accounting only; the client address is never an access-control decision. - An MCP `tools/call` consumes TWO units — one at the MCP transport and one inside the /api/v2 route it re-enters. `initialize`, `ping` and `tools/list` consume one. In a JSON-RPC batch the FIRST element rides on the unit the transport already charged and every further element charges one more, on top of whatever its own method costs — so a batch of ten tool calls still costs twenty. The `X-RateLimit-*` headers on an MCP response report the budget observed AFTER all of that work, not the snapshot taken before it. ## Destructive operations Calls that permanently destroy data require BOTH the `admin` scope AND an explicit confirmation. Repeat the resource identifier from the URL as `confirm`, either as a `?confirm=` query parameter or as a `confirm` field in the JSON body. Omitting it returns 400 naming the exact value to send. The scope alone is never sufficient — this exists specifically so an autonomous agent cannot delete something on its own initiative. Only supply a `confirm` value after a human has explicitly asked for the deletion. EVERY DELETE in this API is on the list below — goals, funnels and segments included. There is no delete a `write` key can perform. The expected value is the identifier exactly as it appears in the path (the 24-character id, not a name or slug); `DELETE /api/v2/sites/{siteId}` additionally accepts the site’s `trackingId` or Mongo `_id`. The confirmation is checked after the scope and site-access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400, never 404: an unconfirmed call never reveals whether the resource exists. - `DELETE /api/v2/sites/{siteId}` — requires `confirm={siteId}`. Permanently delete a site and its collected analytics. Irreversible. - `DELETE /api/v2/sites/{siteId}/goals/{goalId}` — requires `confirm={goalId}`. Permanently delete a goal. Requires the admin scope and a confirmation. - `DELETE /api/v2/sites/{siteId}/funnels/{funnelId}` — requires `confirm={funnelId}`. Permanently delete a funnel. Requires the admin scope and a confirmation. - `DELETE /api/v2/sites/{siteId}/segments/{segmentId}` — requires `confirm={segmentId}`. Permanently delete a saved segment. Requires the admin scope and a confirmation. - `DELETE /api/v2/sites/{siteId}/alerts/{alertId}` — requires `confirm={alertId}`. Permanently delete an alert. Requires the admin scope and a confirmation. - `DELETE /api/v2/sites/{siteId}/webhooks/{webhookId}` — requires `confirm={webhookId}`. Permanently remove a webhook. Deliveries stop immediately. - `DELETE /api/v2/sites/{siteId}/annotations/{annotationId}` — requires `confirm={annotationId}`. Permanently delete an annotation you authored. - `DELETE /api/v2/sites/{siteId}/reports/{reportId}` — requires `confirm={reportId}`. Permanently delete a scheduled report; no further emails are sent. Deleting a site cascades across 23 collections — RecordingChunk, PageView, Event, Goal, Funnel, TeamMember, UserProfile, SessionRecording, SessionSummary, RevenueEvent, WebVital, BotEvent, BotRule, Segment, Alert, Webhook, Dashboard, Annotation, ReportConfig, Insight, Integration, CustomDimension, CustomDimensionValue — all removed before the Site document itself, so no visitor PII is left orphaned. The response reports `cascaded` (the collection names), `deletedCounts` (documents removed per collection) and `totalDeleted`. API keys restricted to a deleted site are deliberately left alone: an empty site list means "every site the owner has", so pulling the last id out of a key would widen it rather than narrow it. Alerts, webhooks and scheduled reports can be paused reversibly with `PATCH … {"enabled": false}` instead of being deleted. Prefer that. ## REST API 56 endpoints across 13 resources. All paths are relative to `https://analytics.appfor.you`. The bracketed value is the required scope. ### Sites Create, inspect, configure and delete the websites you track. Every other resource hangs off a site, so start here to obtain a siteId. - `GET /api/v2/sites` [read] — List every site the API key can access. - `POST /api/v2/sites` [write] — Create a new site owned by the API key holder. - `GET /api/v2/sites/{siteId}` [read] — Full configuration for one site, including the tracking snippet. - `PATCH /api/v2/sites/{siteId}` [write] — Partially update site settings; returns the complete updated site. - `DELETE /api/v2/sites/{siteId}` [admin] — Permanently delete a site and its collected analytics. Irreversible. **DESTRUCTIVE, requires `confirm`.** ### Core analytics Headline traffic metrics: aggregate stats, live visitors, pageview series and custom events. - `GET /api/v2/sites/{siteId}/stats` [read] — Visitors, pageviews, bounce rate, average duration and top-10 breakdowns. - `GET /api/v2/sites/{siteId}/realtime` [read] — Visitors active in the last 5 minutes and the pages they are on. - `GET /api/v2/sites/{siteId}/pageviews` [read] — Pageview total, unique sessions and a daily chart series. - `GET /api/v2/sites/{siteId}/events` [read] — Top 50 custom event names by volume. ### Acquisition Where your traffic comes from, grouped into GA4-style marketing channels. - `GET /api/v2/sites/{siteId}/channels` [read] — Visitors and pageviews per acquisition channel. ### Revenue E-commerce reporting built on the revenue events sent by the tracker. All monetary amounts are integers in the smallest currency unit (cents). - `GET /api/v2/sites/{siteId}/revenue` [read] — Gross, refunds, net revenue, orders, conversion rate and a daily trend. - `GET /api/v2/sites/{siteId}/revenue/sources` [read] — Revenue broken down by acquisition source and medium. - `GET /api/v2/sites/{siteId}/revenue/campaigns` [read] — Revenue broken down by utm_campaign, with source, medium and conversion rate. - `GET /api/v2/sites/{siteId}/revenue/products` [read] — Revenue broken down by product name and category. ### Behaviour & performance Cohort retention, navigation paths and Core Web Vitals. - `GET /api/v2/sites/{siteId}/retention` [read] — Retention matrix plus the averaged retention curve. - `GET /api/v2/sites/{siteId}/journeys` [read] — Top navigation paths, entry pages, exit pages and pages per session. - `GET /api/v2/sites/{siteId}/vitals` [read] — p50/p75/p99 for LCP, FID, CLS, FCP, TTFB and INP with per-page and per-device breakdowns. - `GET /api/v2/sites/{siteId}/bot-events` [read] — Beacons this site refused before storing them, broken down by reason, by ingestion path and by day. ### Visitor profiles Per-visitor records with lifetime pageview, event and revenue totals, plus an activity timeline. - `GET /api/v2/sites/{siteId}/profiles` [read] — Search, filter, sort and page through visitor profiles. - `GET /api/v2/sites/{siteId}/profiles/{profileId}` [read] — One visitor profile plus an interleaved recent-activity timeline. ### Goals Conversion goals. A `pageview` goal converts when a visitor loads a pathname; an `event` goal converts when a named custom event fires. - `GET /api/v2/sites/{siteId}/goals` [read] — Goals for a site, newest first, with conversions in the window. - `POST /api/v2/sites/{siteId}/goals` [write] — Define a conversion goal. - `GET /api/v2/sites/{siteId}/goals/{goalId}` [read] — One goal with its conversion count for the window. - `PATCH /api/v2/sites/{siteId}/goals/{goalId}` [write] — Change a goal's name, type and/or target. - `DELETE /api/v2/sites/{siteId}/goals/{goalId}` [admin] — Permanently delete a goal. Requires the admin scope and a confirmation. **DESTRUCTIVE, requires `confirm`.** ### Funnels Ordered multi-step paths through the site, plus their computed drop-off statistics. - `GET /api/v2/sites/{siteId}/funnels` [read] — Funnel definitions for a site, newest first. - `POST /api/v2/sites/{siteId}/funnels` [write] — Define an ordered funnel with at least two steps. - `GET /api/v2/sites/{siteId}/funnels/{funnelId}` [read] — One funnel definition with its ordered steps. - `PATCH /api/v2/sites/{siteId}/funnels/{funnelId}` [write] — Change a funnel's name and/or steps. - `DELETE /api/v2/sites/{siteId}/funnels/{funnelId}` [admin] — Permanently delete a funnel. Requires the admin scope and a confirmation. **DESTRUCTIVE, requires `confirm`.** - `GET /api/v2/sites/{siteId}/funnels/{funnelId}/stats` [read] — Sessions per step, drop-off and conversion rates. ### Segments Saved audience filters. Segments are PERSONAL to the API key owner — a team member never sees the site owner’s segments, and vice versa. - `GET /api/v2/sites/{siteId}/segments` [read] — Saved segments for this site that belong to the key's own user. - `POST /api/v2/sites/{siteId}/segments` [write] — Save a reusable audience filter owned by the key holder. - `GET /api/v2/sites/{siteId}/segments/{segmentId}` [read] — One saved segment belonging to the key holder. - `PATCH /api/v2/sites/{siteId}/segments/{segmentId}` [write] — Rename a segment and/or replace its filters. - `DELETE /api/v2/sites/{siteId}/segments/{segmentId}` [admin] — Permanently delete a saved segment. Requires the admin scope and a confirmation. **DESTRUCTIVE, requires `confirm`.** ### Alerts Email alerts on traffic and goal conditions. Alerts are per-user: a team member only ever sees and manages their own. - `GET /api/v2/sites/{siteId}/alerts` [read] — Alerts the key's owner has configured for the site. - `POST /api/v2/sites/{siteId}/alerts` [write] — Create an email alert owned by the key holder. - `GET /api/v2/sites/{siteId}/alerts/{alertId}` [read] — One alert owned by the key's owner. - `PATCH /api/v2/sites/{siteId}/alerts/{alertId}` [write] — Change an alert's threshold, comparison, recipient or enabled state. - `DELETE /api/v2/sites/{siteId}/alerts/{alertId}` [admin] — Permanently delete an alert. Requires the admin scope and a confirmation. **DESTRUCTIVE, requires `confirm`.** ### Webhooks HTTP callbacks for pageview, goal and alert events. Webhooks are per-user. Signing secrets are shown in full exactly once. - `GET /api/v2/sites/{siteId}/webhooks` [read] — Webhooks the key's owner registered for the site. - `POST /api/v2/sites/{siteId}/webhooks` [write] — Register a delivery endpoint. The signing secret is returned in full exactly once. - `GET /api/v2/sites/{siteId}/webhooks/{webhookId}` [read] — One webhook owned by the key holder. - `PATCH /api/v2/sites/{siteId}/webhooks/{webhookId}` [write] — Change a webhook's url, subscribed events or enabled state. - `DELETE /api/v2/sites/{siteId}/webhooks/{webhookId}` [admin] — Permanently remove a webhook. Deliveries stop immediately. **DESTRUCTIVE, requires `confirm`.** ### Annotations Notes pinned to the analytics timeline — releases, campaigns, incidents, milestones. SITE-LEVEL for reading: everyone with site access sees all annotations. AUTHOR-ONLY for writing. - `GET /api/v2/sites/{siteId}/annotations` [read] — Timeline annotations for the site, newest first. - `POST /api/v2/sites/{siteId}/annotations` [write] — Mark a point on the timeline, authored by the key holder. - `GET /api/v2/sites/{siteId}/annotations/{annotationId}` [read] — One annotation, readable by anyone with site access. - `PATCH /api/v2/sites/{siteId}/annotations/{annotationId}` [write] — Edit an annotation's date, text, colour or category. - `DELETE /api/v2/sites/{siteId}/annotations/{annotationId}` [admin] — Permanently delete an annotation you authored. **DESTRUCTIVE, requires `confirm`.** ### Scheduled reports Recurring email summaries. Report configs are per-user subscriptions. - `GET /api/v2/sites/{siteId}/reports` [read] — Scheduled report subscriptions the key's owner has for the site. - `POST /api/v2/sites/{siteId}/reports` [write] — Schedule a recurring analytics summary email. - `GET /api/v2/sites/{siteId}/reports/{reportId}` [read] — One report configuration owned by the key's owner. - `PATCH /api/v2/sites/{siteId}/reports/{reportId}` [write] — Change frequency, recipient, or pause/resume the schedule. - `DELETE /api/v2/sites/{siteId}/reports/{reportId}` [admin] — Permanently delete a scheduled report; no further emails are sent. **DESTRUCTIVE, requires `confirm`.** ## MCP server Endpoint: `POST https://analytics.appfor.you/api/mcp` Streamable HTTP transport speaking JSON-RPC 2.0, stateless: no `Mcp-Session-Id` is issued and no SSE stream is opened, so every request must carry its own `Authorization: Bearer ` header. Supported methods: `initialize`, `notifications/*`, `tools/list`, `tools/call`, `ping`. Protocol revisions 2025-06-18, 2025-03-26 and 2024-11-05 are accepted. `GET /api/mcp` returns 405; `OPTIONS` returns 204. Authentication is a static API key with the same scopes as REST — there is no OAuth flow and no protected-resource metadata document. `tools/list` WITHOUT a `params.cursor` returns ALL 55 tools in one response and no `nextCursor` — pagination is only a SHOULD in the MCP spec, so a client that ignores cursors is never left believing a first page is the whole server. Supply a cursor and the listing pages deterministically at 20 tools per response: pass each `nextCursor` back verbatim until a response arrives without one. A cursor this server did not issue is rejected with JSON-RPC -32602 rather than silently restarting the listing. JSON-RPC BATCHES are capped at 20 messages; a longer array is rejected with -32600 before any element is dispatched. That check runs AFTER transport authentication, so an oversized batch has still cost one API-key lookup and one unit of the rate-limit budget — cheap, but not free. Elements execute at most 4 at a time and responses are returned in request order. The first element rides on the unit the transport already charged and every further element charges its own before it runs; an element denied mid-batch returns error code -32000 with a retry timestamp while earlier elements still succeed. TOOL RESULTS ARE SIZE-CAPPED, and never silently: successful result text is truncated at 24000 bytes and an echoed upstream error body at 2000 bytes. A truncated result ends with the marker `--- MCP_RESULT_TRUNCATED ---`. Treat that as "there is more": re-run the tool with a smaller `limit`, an `offset` or a narrower date window instead of acting on the partial payload. A 401 carries `WWW-Authenticate: Bearer realm="analytics-platform", error="invalid_token"`. Every response from the protocol layer — results, JSON-RPC errors and the 202 for a notification — carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (epoch milliseconds), as does the 429; the 401 and 503 raised before that layer runs do not. `Access-Control-Expose-Headers` lists those headers plus `Retry-After`, `WWW-Authenticate`, `Mcp-Session-Id` and `MCP-Protocol-Version`. Every tool re-enters its own REST endpoint, so the MCP surface can never be more permissive than the API. A `tools/call` consumes two units of the 100 req/min budget (one at the transport, one inside the v2 route); `initialize`, `ping` and `tools/list` consume one. The `X-RateLimit-*` headers on an MCP response report the budget observed AFTER the tool ran, not the snapshot taken before it, so pacing off them is accurate. All eight `delete_*` tools — delete_site, delete_goal, delete_funnel, delete_segment, delete_alert, delete_webhook, delete_annotation, delete_report — require the `admin` scope and declare `confirm` as a REQUIRED property of their input schema. Its value is the id of the resource being deleted. Do not invent one: supply it only after a human has explicitly asked for that deletion. Minimal client configuration: ```json { "mcpServers": { "analytics": { "type": "http", "url": "https://analytics.appfor.you/api/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` ### Tools (55) - `list_sites` [read] → `GET /api/v2/sites` — List every website this API key can access (owned plus accepted team memberships), newest first. START HERE — almost every other tool needs a `siteId`. Returns siteId, trackingId, name, domain, isActive, isPublic and the total accessible count. - `get_site` [read] → `GET /api/v2/sites/{siteId}` — Full configuration for one site: identity, active/public state, data-retention settings with the owner plan cap, bot-detection and session-recording settings, the caller role (owner/admin/viewer) and the ready-to-paste tracking snippet. - `create_site` [write] → `POST /api/v2/sites` — Create a new website to track. Returns the site including its public trackingId and the snippet to paste into the page HTML. API keys restricted to specific sites cannot create sites. - `update_site` [write] → `PATCH /api/v2/sites/{siteId}` — Partially update a site — send only the fields to change, at least one. Covers renaming, changing the domain, pausing collection (isActive), publishing or unpublishing the public dashboard (isPublic mints a share URL, or revokes it), data retention, bot detection and session recording. dataRetentionDays is capped by the site OWNER plan (free 90, starter 365, growth 730, business 1095). - `delete_site` [admin, destructive] → `DELETE /api/v2/sites/{siteId}` — DESTRUCTIVE AND IRREVERSIBLE — deletes a site together with all of its pageviews, events, goals, funnels and team memberships. Requires the `admin` scope, site OWNER access, and `confirm` set to the same identifier passed as `siteId`, only after the human has explicitly asked for this site to be deleted. Prefer update_site with isActive:false to merely stop tracking. Requires `confirm` (schema-required) = the `siteId` value. - `get_site_stats` [read] → `GET /api/v2/sites/{siteId}/stats` — Headline analytics for a window: unique visitors, pageviews, bounce rate, average visit duration, plus the top 10 pages, referrers, browsers and countries. Best first call for "how is my site doing?". - `get_site_realtime` [read] → `GET /api/v2/sites/{siteId}/realtime` — Visitors active in the last 5 minutes and the pages they are on right now. Takes no time window at all — no `range`, no `from`/`to`. For any other period use get_site_stats or get_site_pageviews. - `get_site_pageviews` [read] → `GET /api/v2/sites/{siteId}/pageviews` — Total pageviews, unique sessions and a day-by-day series. Pass `pathname` to narrow to a single page. Use for traffic-trend questions; use get_site_stats for the broader breakdown. - `get_site_events` [read] → `GET /api/v2/sites/{siteId}/events` — The 50 most frequent custom events in the window, with counts, ordered by volume. Use to discover which event names exist before creating an event-type goal. - `get_site_channels` [read] → `GET /api/v2/sites/{siteId}/channels` — GA4-style acquisition channels — Direct, Organic Search, Paid Search, Paid Social, Organic Social, Email, Referral, AI Assistant, Other — derived from referrer plus utm_source/utm_medium. Answers "where do my visitors come from". - `get_site_revenue` [read] → `GET /api/v2/sites/{siteId}/revenue` — Revenue summary plus daily trend: gross revenue from purchase/subscription/one_time events, refunds summed separately, netRevenue, order count and conversion rate (orders / unique sessions). Amounts are integers in cents — divide by 100 before presenting. Default window 30 days. - `get_revenue_by_source` [read] → `GET /api/v2/sites/{siteId}/revenue/sources` — Revenue attributed to source/medium pairs, highest first; source falls back utm_source then referrer then "(direct)". Answers "which traffic source makes the most money". Amounts in cents, default window 30 days. - `get_revenue_by_campaign` [read] → `GET /api/v2/sites/{siteId}/revenue/campaigns` — Revenue grouped by utm_campaign (with its source/medium), highest first, with a per-campaign conversion rate. Only events carrying a utm_campaign count, so totals are lower than get_site_revenue. Amounts in cents. - `get_revenue_by_product` [read] → `GET /api/v2/sites/{siteId}/revenue/products` — Revenue grouped by product name and category with quantity sold and average price, highest first. Only events carrying a productName are included; missing categories report as "(uncategorized)". Answers "what are my best sellers". Amounts in cents. - `get_site_retention` [read] → `GET /api/v2/sites/{siteId}/retention` — Cohort retention matrix: for each cohort of first-time visitors, the percentage returning in each later period, plus the averaged curve (index 0 is the cohort period itself and is always 100). Takes NO `range` preset — control the window with granularity + periods, or startDate/endDate. - `get_site_journeys` [read] → `GET /api/v2/sites/{siteId}/journeys` — Navigation-path analysis: the most common journeys (first five pathnames of multi-page sessions), the most frequent entry and exit pages, and average pages per session. Shows how visitors move through the site and where they leave. Default window 7 days. - `get_site_vitals` [read] → `GET /api/v2/sites/{siteId}/vitals` — Core Web Vitals: p50/p75/p99 for LCP, FID, CLS, FCP, TTFB and INP, the good / needs-improvement / poor split, a daily p75 trend, and p75 per page and per device. For page-speed questions. limit/offset page the per-page breakdown only. - `list_profiles` [read] → `GET /api/v2/sites/{siteId}/profiles` — Search individual visitor profiles with lifetime pageview/event/revenue totals. Free-text search plus tag, country, device and identified filters. Omit the date parameters to search the full lifetime list. EXCEPTION to the `from`/`to` rules: here a lone `from` or a lone `to` does NOT restrict anything — last-seen filtering is applied only with `range`, or with `from` and `to` together. - `get_profile` [read] → `GET /api/v2/sites/{siteId}/profiles/{profileId}` — One visitor profile plus a recent-activity timeline interleaving their latest events and pageviews, newest first. Get profileId from list_profiles. - `list_goals` [read] → `GET /api/v2/sites/{siteId}/goals` — Conversion goals defined for a site, newest first, each with its count of unique converting sessions in the window. Set includeConversions:false for definitions only and a faster response. Default window 30 days. - `get_goal` [read] → `GET /api/v2/sites/{siteId}/goals/{goalId}` — One conversion goal with its conversion count for the window. Same window parameters as list_goals. - `create_goal` [write] → `POST /api/v2/sites/{siteId}/goals` — Define a conversion goal: type "pageview" with a target pathname (e.g. "/thank-you") counts visits to a page, type "event" with a target event name (e.g. "signup") counts custom events. Call get_site_events first to discover which event names actually fire. Viewers cannot create goals. - `update_goal` [write] → `PATCH /api/v2/sites/{siteId}/goals/{goalId}` — Change a goal name, type and/or target; at least one field required. Viewers cannot modify goals. - `delete_goal` [admin, destructive] → `DELETE /api/v2/sites/{siteId}/goals/{goalId}` — DESTRUCTIVE — permanently deletes a goal definition and its historical conversion reporting. Requires the `admin` scope and `confirm` set to the exact goalId, only after the human has explicitly asked for this deletion. The underlying pageviews and events are not touched. Requires `confirm` (schema-required) = the `goalId` value. - `list_funnels` [read] → `GET /api/v2/sites/{siteId}/funnels` — Funnel definitions for a site, newest first. Definitions only — call get_funnel_stats for step conversion and drop-off numbers. - `get_funnel` [read] → `GET /api/v2/sites/{siteId}/funnels/{funnelId}` — One funnel definition and its ordered steps. Use get_funnel_stats for results. - `create_funnel` [write] → `POST /api/v2/sites/{siteId}/funnels` — Define a multi-step conversion funnel. Steps are ORDERED and at least two are required; each needs a display name and the pathname visitors must reach. Viewers cannot create funnels. - `update_funnel` [write] → `PATCH /api/v2/sites/{siteId}/funnels/{funnelId}` — Change a funnel name and/or its steps; at least one field required. WARNING: `steps` REPLACES the whole array, so send the complete ordered list (still minimum two). Viewers cannot modify funnels. - `delete_funnel` [admin, destructive] → `DELETE /api/v2/sites/{siteId}/funnels/{funnelId}` — DESTRUCTIVE — permanently deletes a funnel definition and its saved step configuration. Requires the `admin` scope and `confirm` set to the exact funnelId, only after the human has explicitly asked for this deletion. The underlying pageviews are not touched. Requires `confirm` (schema-required) = the `funnelId` value. - `get_funnel_stats` [read] → `GET /api/v2/sites/{siteId}/funnels/{funnelId}/stats` — Computed funnel performance: unique sessions reaching each step, drop-off versus the previous step, conversion versus step one, and the overall first-to-last rate (percentages to one decimal). Answers "where are people dropping out". Default window 30 days. - `list_segments` [read] → `GET /api/v2/sites/{siteId}/segments` — Saved audience segments for a site. PERSONAL: only segments belonging to the API key owner are visible, never other team members. - `get_segment` [read] → `GET /api/v2/sites/{siteId}/segments/{segmentId}` — One saved segment belonging to the API key owner on this site. - `create_segment` [write] → `POST /api/v2/sites/{siteId}/segments` — Save a reusable audience filter (e.g. "Mobile visitors from Germany") owned by the API key holder. Every filter key is optional and they combine with AND. - `update_segment` [write] → `PATCH /api/v2/sites/{siteId}/segments/{segmentId}` — Rename a segment and/or change its filters; at least one field required. WARNING: `filters` REPLACES the whole object, so resend every filter you want to keep. Only the key owner own segments can be updated. - `delete_segment` [admin, destructive] → `DELETE /api/v2/sites/{siteId}/segments/{segmentId}` — DESTRUCTIVE — permanently deletes a saved segment belonging to the API key owner. Requires the `admin` scope and `confirm` set to the exact segmentId, only after the human has explicitly asked for this deletion. No analytics data is removed. Requires `confirm` (schema-required) = the `segmentId` value. - `list_alerts` [read] → `GET /api/v2/sites/{siteId}/alerts` — Traffic and conversion alerts the API key owner has configured for a site. Per-user: a team member sees only their own. - `get_alert` [read] → `GET /api/v2/sites/{siteId}/alerts/{alertId}` — One alert owned by the API key holder on this site. - `create_alert` [write] → `POST /api/v2/sites/{siteId}/alerts` — Create an email alert. `threshold` is read according to `comparison`: a percentage change for "previous_day"/"previous_week", a raw count for "absolute". Recipient defaults to the key owner account email. Viewers cannot create alerts. - `update_alert` [write] → `PATCH /api/v2/sites/{siteId}/alerts/{alertId}` — Change an alert threshold, comparison, recipient or enabled state; at least one field required. The alert `type` is immutable. enabled:false mutes without deleting. Viewers cannot modify alerts. - `delete_alert` [admin, destructive] → `DELETE /api/v2/sites/{siteId}/alerts/{alertId}` — DESTRUCTIVE — permanently deletes an alert configuration. Requires the `admin` scope and `confirm` set to the exact alertId, only after the human has explicitly asked for this deletion. If they merely want the emails to stop, use update_alert with enabled:false. Requires `confirm` (schema-required) = the `alertId` value. - `list_webhooks` [read] → `GET /api/v2/sites/{siteId}/webhooks` — Webhooks the API key owner has registered for a site (per-user). Signing secrets are always masked as ****. - `get_webhook` [read] → `GET /api/v2/sites/{siteId}/webhooks/{webhookId}` — One webhook owned by the API key holder. The signing secret is masked — only create_webhook ever returns it in full. - `create_webhook` [write] → `POST /api/v2/sites/{siteId}/webhooks` — Register an HTTPS endpoint to receive event notifications. A signing secret is generated server-side and returned IN FULL exactly once, in this response — surface it to the user immediately and say it cannot be retrieved again. Viewers cannot create webhooks. - `update_webhook` [write] → `PATCH /api/v2/sites/{siteId}/webhooks/{webhookId}` — Change a webhook URL, subscribed events or enabled state; at least one field required. `events` replaces the whole list. The signing secret is immutable and never returned unmasked. Viewers cannot modify webhooks. - `delete_webhook` [admin, destructive] → `DELETE /api/v2/sites/{siteId}/webhooks/{webhookId}` — DESTRUCTIVE — permanently removes a webhook; deliveries stop immediately and the signing secret is lost. Requires the `admin` scope and `confirm` set to the exact webhookId, only after the human has explicitly asked for this deletion. update_webhook with enabled:false pauses deliveries reversibly. Requires `confirm` (schema-required) = the `webhookId` value. - `list_annotations` [read] → `GET /api/v2/sites/{siteId}/annotations` — Timeline annotations (deployments, campaigns, incidents, milestones) for a site. SITE-LEVEL: everyone with access sees all of them, each with its author. Use to explain traffic spikes or drops seen in get_site_pageviews. - `get_annotation` [read] → `GET /api/v2/sites/{siteId}/annotations/{annotationId}` — One annotation. Readable by anyone with access to the site, whoever authored it. - `create_annotation` [write] → `POST /api/v2/sites/{siteId}/annotations` — Mark a point on the site timeline so later traffic changes have context — a release, a campaign launch, an outage. Authored by the API key owner. Viewers cannot create annotations. - `update_annotation` [write] → `PATCH /api/v2/sites/{siteId}/annotations/{annotationId}` — Edit an annotation date, text, colour or category; at least one field required. Only the AUTHOR may edit — another user annotation reports as not found even for the site owner. - `delete_annotation` [admin, destructive] → `DELETE /api/v2/sites/{siteId}/annotations/{annotationId}` — DESTRUCTIVE — permanently deletes an annotation authored by the API key owner. Requires the `admin` scope and `confirm` set to the exact annotationId, only after the human has explicitly asked for this deletion. Requires `confirm` (schema-required) = the `annotationId` value. - `list_reports` [read] → `GET /api/v2/sites/{siteId}/reports` — Recurring email report subscriptions the API key owner has for a site (per-user), with frequency, recipient and whether each schedule is active. - `get_report` [read] → `GET /api/v2/sites/{siteId}/reports/{reportId}` — One scheduled report configuration owned by the API key holder. - `create_report` [write] → `POST /api/v2/sites/{siteId}/reports` — Schedule a recurring analytics summary email, owned by the API key holder. Recipient defaults to the key owner account email. Viewers cannot schedule reports. - `update_report` [write] → `PATCH /api/v2/sites/{siteId}/reports/{reportId}` — Change a scheduled report frequency, recipient or enabled state; at least one field required. enabled:false pauses the schedule, true resumes it. Viewers cannot modify reports. - `delete_report` [admin, destructive] → `DELETE /api/v2/sites/{siteId}/reports/{reportId}` — DESTRUCTIVE — permanently deletes a scheduled report configuration; no further emails are sent. Requires the `admin` scope and `confirm` set to the exact reportId, only after the human has explicitly asked for this deletion. update_report with enabled:false pauses it reversibly. Requires `confirm` (schema-required) = the `reportId` value. ## Deployment (self-hosting only) All 10 variables are optional and default safely; none of them changes the public API contract. - `MCP_INTERNAL_ORIGIN` (optional, default: http://127.0.0.1:$PORT (PORT defaults to 3000)) — Absolute http(s) URL pinning the origin the MCP server uses for its loopback calls into /api/v2. Only the origin is used; any path, query or embedded credentials make the value invalid and it is skipped. Resolved from configuration only — never from the request’s Host or X-Forwarded-* headers, which a caller could otherwise use to redirect an authenticated internal call. - `TRUSTED_PROXY_HOPS` (optional, default: 1 — correct for a single reverse proxy (Traefik/nginx) directly in front of the app) — How many proxies between the internet and this process APPEND to `x-forwarded-for`. The per-IP failed-authentication bucket is keyed on the entry that many positions from the END of the header, so getting it wrong only degrades failed-auth bucket accuracy — it never affects authentication, authorization or any endpoint result. The geo resolver (src/lib/geoip.ts) consults the same variable, but only when `GEO_TRUST_PROXY` is enabled — on the default Netlify deployment the pinned edge headers answer first and this value is never reached. Set it to 2 if a CDN is later placed in front of the reverse proxy. Absent, non-numeric or < 1 values fall back to 1 (the geo resolver additionally treats values > 16 as invalid). - `TRUST_X_REAL_IP` (optional, default: false — x-real-ip is ignored and the request falls into the shared unknown bucket) — Set to true ONLY when a proxy you operate overwrites `x-real-ip` on every inbound request. It is consulted for failed-authentication bucketing and by the geo resolver (src/lib/geoip.ts), in both cases only when the `x-forwarded-for` chain yields nothing usable. It stays off by default because `x-real-ip` is otherwise ordinary client input: a caller could set it to a victim address to pin that address to its bucket, or rotate it per request to evade the budget entirely. - `GEO_EDGE_PROVIDER` (optional, default: netlify — correct for this repo’s Netlify deployment) — The single CDN whose edge geo / client-IP headers the geo resolver believes: one of netlify | cloudflare | vercel | none. Exactly one provider is trusted per deployment — an edge header is only trustworthy because the CDN in front of the origin overwrites it on every request, so any other provider’s headers are ordinary client input and are ignored completely. Unrecognized values are treated as none (with a one-time warning), never as “trust everything”. If the site ever moves behind a different CDN, change this in the same deploy — getGeoHealth() in src/lib/geoip.ts exposes pinned-vs-observed provider counters to catch the deploy where that was forgotten. - `GEO_TRUST_PROXY` (optional, default: false — x-forwarded-for is ignored by geo resolution) — Whether the geo resolver may believe `x-forwarded-for` at all when the pinned edge provider’s headers are absent. Off by default: on Netlify the CDN terminates every request and writes the edge headers, so XFF adds zero coverage while its left entries are attacker-controlled text. When enabled, the entry `TRUSTED_PROXY_HOPS` positions from the END of the chain is selected, then walked leftward past private/reserved addresses to the first public one. Affects geo resolution and the /api/collect rate-limit / bot-detection address only — never authentication. - `MAXMIND_LICENSE_KEY` (optional, default: unset — the updater logs a loud banner, keeps the bundled GeoLite2 snapshot and exits 0 (a deploy never fails for lack of a key)) — Free MaxMind GeoLite2 licence key used by scripts/update-geoip.mjs (hooked into every build via the npm `prebuild` script) to refresh geoip-lite’s IP database, which otherwise stays frozen at the package’s npm publish date. Set it on the Coolify application (Configuration → Environment Variables), never in the repo — this deployment is Coolify + Nixpacks + Traefik and has not been on Netlify for some time, so the instruction that used to stand here named a settings page nobody can open; the refreshed .dat files are likewise never committed — GeoLite2 redistribution is licence-restricted, so the pipeline regenerates them per build. DB age is observable at runtime via getGeoHealth(). - `SITE_CLAIM_ALLOW_LOOPBACK_PROOF` (optional, default: false — the ownership-proof fetcher refuses loopback destinations, in every environment including development) — Affirmative opt-in allowing the site-ownership proof fetcher (POST /api/sites/claim) to dial a LOOPBACK address, so a developer can claim a site served from their own machine. Every other reserved range — 10/8, 172.16/12, 192.168/16, 169.254.169.254, CGNAT, TEST-NET, NAT64 and IPv4-mapped spellings of all of them — stays refused whatever this is set to, and the resolved address is pinned before connect so DNS cannot be rebound between the check and the socket. It is deliberately an opt-in rather than a “not production” default: gating it on the ABSENCE of NODE_ENV=production meant a deployment that merely forgot to set the production marker silently gained a loopback fetcher, which is both a narrow existence oracle and a way to satisfy a claim from a local service instead of the real public domain. Absence of a marker must never grant a capability. Ignored (treated as false) in production and whenever `SITE_CLAIM_REQUIRE_PROOF_ALWAYS` is on. A claim satisfied this way is reported with `proof.development: true` so it can never be mistaken for a production-grade proof. - `SITE_CLAIM_REQUIRE_PROOF_ALWAYS` (optional, default: false — production semantics still apply automatically when NODE_ENV=production) — Forces production ownership-proof semantics in a non-production process: no loopback destinations and no development exceptions, exactly as if NODE_ENV were production. Set it in staging so staging behaves like production. It can only ever make the proof stricter — there is no value of this variable, or of any other, that relaxes the reserved-range refusals. - `SITE_CLAIM_IP_PEPPER` (optional, default: NEXTAUTH_SECRET, then a fixed development string if that is unset too) — HMAC key used to key the client address stored against each unclaimed site, which is how the per-address cap on outstanding unclaimed sites is enforced without turning an anonymous endpoint into an IP log. A plain hash would not do: the IPv4 keyspace is small enough to brute-force exhaustively, and a keyed one is not. Set it to an independent secret if you would rather the cap not share key material with session signing; the fallback to NEXTAUTH_SECRET means a normal deployment needs no new configuration. - `MONGODB_AUTO_INDEX` (optional, default: off when NODE_ENV=production, on otherwise — Mongoose’s own default (always on) is deliberately not used) — Whether Mongoose may build a collection’s declared indexes at runtime, on the first query against each model. Off in production because it is a schema write performed by an ordinary request: a schema whose index declaration is wrong silently rewrites the live indexes on every restart — that is exactly how a corrected RevenueEvent orderId index was recreated in its broken form, discarding revenue events again — and a build on a large collection runs during a cold start at the cost of request latency. It also cannot converge a database on the schema even when left on, because it only ever CREATES indexes and never drops one a schema stopped declaring; index changes are applied deliberately by the migrations in scripts/ instead. Accepts true/1/yes/on and false/0/no/off; set it to true for the single deploy where an operator does want a build, and unset it afterwards. It never affects query results — only which indexes exist to serve them. ## Error codes - `400` Bad Request — Validation failed, or a destructive call is missing/mismatching its `confirm` value. The `error` string names the exact problem. - `401` Unauthorized — Missing, malformed, revoked or expired API key. A 401 from the MCP endpoint additionally carries `WWW-Authenticate: Bearer realm="analytics-platform", error="invalid_token"`. - `403` Forbidden — The key lacks the required scope, is restricted to other sites, or your role on the site is read-only (`viewer`). - `404` Not Found — The site or resource does not exist — or you have no access to it. Access failures are deliberately reported as 404 so other users’ data is never revealed. - `429` Too Many Requests — Rate limit exceeded — 100 requests per minute per API-key owner, or the tighter per-IP budget for failed authentications. Check the Retry-After and X-RateLimit-Reset headers. - `500` Internal Server Error — Unexpected failure. Safe to retry with backoff. - `503` Service Unavailable — The database was unreachable while authenticating. Retry with backoff. ## Optional - [Pricing and plan limits](https://analytics.appfor.you/pricing): data-retention caps are set by the site owner's plan (free 90 days, starter 365, growth 730, business 1095). - [Dashboard](https://analytics.appfor.you/dashboard): where API keys are created, scoped and revoked.