{
  "openapi": "3.1.0",
  "info": {
    "title": "Analytics Platform API",
    "version": "2.0.0",
    "summary": "Read analytics and manage every resource of a privacy-focused web analytics platform.",
    "description": "Programmatic access to the Analytics Platform: traffic, acquisition, revenue, retention, journeys, Core Web Vitals and visitor profiles, plus full management of sites, goals, funnels, segments, alerts, webhooks, annotations and scheduled email reports.\n\n## Authentication\n\nSend an API key as a bearer token: `Authorization: Bearer <api key>`. Create keys at Dashboard → Settings → API Keys; the raw key is shown once and only a SHA-256 hash is stored.\n\n## Scopes\n\nKeys carry scopes `read`, `write` and/or `admin`, hierarchically: admin implies write implies read. Each operation documents its requirement in `x-required-scope`. Keys created before scopes existed behave as `read`.\n\nKeys may additionally be restricted to specific sites and may carry a hard expiry. The restriction may name any site the creator can reach — owned sites and sites joined as an accepted team member alike.\n\nA scope is never the whole story: team members with the `viewer` role are read-only, and `isPublic` on `PATCH /api/v2/sites/{siteId}` is OWNER-ONLY — a team member with the `admin` role receives 403 for a PATCH carrying it, checked before any field is applied, because publishing exposes the site on an unauthenticated `/share/<slug>` URL.\n\n## Time windows\n\nAnalytics endpoints take a `range` preset plus optional `from`/`to` overrides. The two helpers behind them now agree, so the same parameters always describe the same window:\n\n- A date-only `from=YYYY-MM-DD` means that day from `T00:00:00.000Z`.\n- A date-only `to=YYYY-MM-DD` covers the WHOLE day, through `T23:59:59.999Z`. The final day is therefore included, and `from=X&to=X` is a full 24 hours — it used to be a zero-width window that always returned nothing.\n- A value carrying a time (`2026-07-30T09:15:00Z`) is an instant and is used verbatim, with no day expansion.\n- An upper bound in the future is clamped to now, except where clamping would invert the window. A window lying WHOLLY in the future is rejected with 400 everywhere, because it can never contain data.\n- Anchoring: neither → `[now - range, now]`; `from` only → `[from, now]`; `to` only → `[to - range, to]`; both → `[from, to]`.\n- 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`. No endpoint falls back to its `range` preset any more, so a successful response always describes the window you asked for.\n- GET /retention and GET /annotations validate their own from/to and answer 400 with their own wording.\n- 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.\n- An unrecognised range preset is a 400 everywhere; no endpoint silently substitutes its default any more.\n\n## Destructive operations\n\nOperations marked `x-destructive` permanently destroy data. They require the `admin` scope AND an explicit `confirm` value repeating the resource identifier from the URL, supplied either as the `confirm` query parameter or as a `confirm` field in the JSON body. The scope alone is never sufficient.\n\nEvery DELETE in this API is destructive, including goals, funnels and segments — there is no delete that a `write` key can perform. The expected `confirm` value is the identifier exactly as it appears in the path (the 24-character id), except for `DELETE /api/v2/sites/{siteId}`, which also accepts the site’s `trackingId` or Mongo `_id`.\n\nDeleting a site cascades: every child document across all of the collections named in the response’s `cascaded` array is removed before the site itself, so no visitor PII (session recordings, visitor profiles, revenue events, Web Vitals) is left orphaned. `deletedCounts` reports per-collection totals.\n\n## CORS\n\nEvery endpoint answers an OPTIONS preflight and allows any origin, for server-to-server and tooling convenience. Preflight is transport-level, so it is not described as an operation below. Never ship an API key to a browser.\n\n## Rate limits\n\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n\n## MCP\n\nThe same capabilities are exposed as Model Context Protocol tools at https://analytics.appfor.you/api/mcp (Streamable HTTP, JSON-RPC 2.0, stateless, same bearer API key). A `tools/list` without a cursor returns the COMPLETE tool registry in one response — pagination at 20 tools per page happens only when the client drives it with a `cursor`. JSON-RPC batches are capped at 20 messages, rejected with -32600 after transport authentication (so an oversized batch still costs one key lookup and one unit of budget). Tool result text is truncated at 24000 bytes and marked with `--- MCP_RESULT_TRUNCATED ---`. See https://analytics.appfor.you/docs#mcp.\n\n## Deployment\n\nSelf-hosting only. All 10 variables are optional and default safely; none of them changes the public API contract.\n\n- `MCP_INTERNAL_ORIGIN` — 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. Default: http://127.0.0.1:$PORT (PORT defaults to 3000).\n- `TRUSTED_PROXY_HOPS` — 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). Default: 1 — correct for a single reverse proxy (Traefik/nginx) directly in front of the app.\n- `TRUST_X_REAL_IP` — 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. Default: false — x-real-ip is ignored and the request falls into the shared unknown bucket.\n- `GEO_EDGE_PROVIDER` — 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. Default: netlify — correct for this repo’s Netlify deployment.\n- `GEO_TRUST_PROXY` — 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. Default: false — x-forwarded-for is ignored by geo resolution.\n- `MAXMIND_LICENSE_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(). 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).\n- `SITE_CLAIM_ALLOW_LOOPBACK_PROOF` — 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. Default: false — the ownership-proof fetcher refuses loopback destinations, in every environment including development.\n- `SITE_CLAIM_REQUIRE_PROOF_ALWAYS` — 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. Default: false — production semantics still apply automatically when NODE_ENV=production.\n- `SITE_CLAIM_IP_PEPPER` — 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. Default: NEXTAUTH_SECRET, then a fixed development string if that is unset too.\n- `MONGODB_AUTO_INDEX` — 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. Default: off when NODE_ENV=production, on otherwise — Mongoose’s own default (always on) is deliberately not used.",
    "contact": {
      "name": "Analytics Platform",
      "url": "https://analytics.appfor.you/docs"
    }
  },
  "servers": [
    {
      "url": "https://analytics.appfor.you",
      "description": "Production"
    },
    {
      "url": "http://localhost:3000",
      "description": "Local development"
    }
  ],
  "externalDocs": {
    "description": "Human-readable API and MCP documentation",
    "url": "https://analytics.appfor.you/docs"
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Sites",
      "description": "Create, inspect, configure and delete the websites you track. Every other resource hangs off a site, so start here to obtain a siteId."
    },
    {
      "name": "Core analytics",
      "description": "Headline traffic metrics: aggregate stats, live visitors, pageview series and custom events."
    },
    {
      "name": "Acquisition",
      "description": "Where your traffic comes from, grouped into GA4-style marketing channels."
    },
    {
      "name": "Revenue",
      "description": "E-commerce reporting built on the revenue events sent by the tracker. All monetary amounts are integers in the smallest currency unit (cents)."
    },
    {
      "name": "Behaviour & performance",
      "description": "Cohort retention, navigation paths and Core Web Vitals."
    },
    {
      "name": "Visitor profiles",
      "description": "Per-visitor records with lifetime pageview, event and revenue totals, plus an activity timeline."
    },
    {
      "name": "Goals",
      "description": "Conversion goals. A `pageview` goal converts when a visitor loads a pathname; an `event` goal converts when a named custom event fires."
    },
    {
      "name": "Funnels",
      "description": "Ordered multi-step paths through the site, plus their computed drop-off statistics."
    },
    {
      "name": "Segments",
      "description": "Saved audience filters. Segments are PERSONAL to the API key owner — a team member never sees the site owner’s segments, and vice versa."
    },
    {
      "name": "Alerts",
      "description": "Email alerts on traffic and goal conditions. Alerts are per-user: a team member only ever sees and manages their own."
    },
    {
      "name": "Webhooks",
      "description": "HTTP callbacks for pageview, goal and alert events. Webhooks are per-user. Signing secrets are shown in full exactly once."
    },
    {
      "name": "Annotations",
      "description": "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."
    },
    {
      "name": "Scheduled reports",
      "description": "Recurring email summaries. Report configs are per-user subscriptions."
    }
  ],
  "paths": {
    "/api/v2/sites": {
      "get": {
        "operationId": "listSites",
        "summary": "List every site the API key can access.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nReturns the sites owned by the API key's user plus the sites where they are an accepted team member, newest first. If the key was created with a site restriction, only those sites are returned.\n\n- `total` is the full accessible count before pagination is applied.\n- Owned sites and sites you joined as a team member are interleaved into ONE global `createdAt` descending order, and `limit`/`offset` are applied by the database — so paging walks the whole accessible set consistently instead of restarting per source.\n- Backward compatible with the pre-scopes API: every previously returned field is unchanged. `trackingId` and `isPublic` are new additions.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List every site the API key can access.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteSummary"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "sites": [
                    {
                      "siteId": "6507f1f77bcf86cd799439011",
                      "trackingId": "a1b2c3d4e5",
                      "name": "My Website",
                      "domain": "example.com",
                      "isActive": true,
                      "isPublic": false,
                      "createdAt": "2025-01-15T10:30:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 25,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createSite",
        "summary": "Create a new site owned by the API key holder.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nCreates a site with a freshly generated 10-character public `trackingId`. Every setting other than name and domain takes its schema default: isActive true, isPublic false, dataRetentionDays 90, botDetectionEnabled true, botLogEnabled true, recordingEnabled false, recordingSamplingRate 100, recordingMaxDuration 600, recordingPrivacyLevel \"standard\". The response includes the ready-to-paste tracking snippet.\n\n- A site-restricted API key (one created with an explicit site list) cannot create sites and receives 403.\n- No plan-based site-count limit is enforced, matching the dashboard behaviour.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "responses": {
          "201": {
            "description": "Create a new site owned by the API key holder.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "example": {
                  "siteId": "6507f1f77bcf86cd799439011",
                  "trackingId": "a1b2c3d4e5",
                  "name": "Marketing site",
                  "domain": "example.com",
                  "isActive": true,
                  "isPublic": false,
                  "publicSlug": null,
                  "publicUrl": null,
                  "dataRetentionDays": 90,
                  "maxRetentionDays": 90,
                  "plan": "free",
                  "botDetectionEnabled": true,
                  "botLogEnabled": true,
                  "recordingEnabled": false,
                  "recordingSamplingRate": 100,
                  "recordingMaxDuration": 600,
                  "recordingPrivacyLevel": "standard",
                  "role": "owner",
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z",
                  "tracking": {
                    "scriptUrl": "https://analytics.appfor.you/api/sites/a1b2c3d4e5/tracker.js",
                    "snippet": "<script defer data-site=\"a1b2c3d4e5\" src=\"https://analytics.appfor.you/api/sites/a1b2c3d4e5/tracker.js\"></script>"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Display name. Trimmed; must be non-empty."
                  },
                  "domain": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Domain being tracked, e.g. \"example.com\" (no protocol, no trailing slash). Trimmed; must be non-empty."
                  }
                },
                "required": [
                  "name",
                  "domain"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "Marketing site",
                "domain": "example.com"
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}": {
      "get": {
        "operationId": "getSite",
        "summary": "Full configuration for one site, including the tracking snippet.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nReturns identity, active/public state, data-retention settings with the owner's plan cap, bot-detection flags, session-recording settings, the caller's role on the site, and the tracker script URL plus ready-to-paste snippet.\n\n- Returns 404 (never 403) when the site does not exist or you have no access, so other users’ sites are never revealed.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Full configuration for one site, including the tracking snippet.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "example": {
                  "siteId": "6507f1f77bcf86cd799439011",
                  "trackingId": "a1b2c3d4e5",
                  "name": "My Website",
                  "domain": "example.com",
                  "isActive": true,
                  "isPublic": true,
                  "publicSlug": "sunny-otter-42",
                  "publicUrl": "/share/sunny-otter-42",
                  "dataRetentionDays": 365,
                  "maxRetentionDays": 365,
                  "plan": "starter",
                  "botDetectionEnabled": true,
                  "botLogEnabled": true,
                  "recordingEnabled": false,
                  "recordingSamplingRate": 100,
                  "recordingMaxDuration": 600,
                  "recordingPrivacyLevel": "standard",
                  "role": "owner",
                  "createdAt": "2025-01-15T10:30:00.000Z",
                  "updatedAt": "2025-03-20T12:00:00.000Z",
                  "tracking": {
                    "scriptUrl": "https://analytics.appfor.you/api/sites/a1b2c3d4e5/tracker.js",
                    "snippet": "<script defer data-site=\"a1b2c3d4e5\" src=\"https://analytics.appfor.you/api/sites/a1b2c3d4e5/tracker.js\"></script>"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateSite",
        "summary": "Partially update site settings; returns the complete updated site.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nEvery field is optional but at least one must be supplied. This one endpoint consolidates what the dashboard splits across site settings, sharing, retention, bot detection and session recording.\n\n- Team members with the `viewer` role are read-only and receive 403.\n- `dataRetentionDays` is capped by the SITE OWNER's plan — free 90, starter 365, growth 730, business 1095. Exceeding it returns 400 with a message naming the plan and its limit.\n- `isPublic` is OWNER-ONLY. A PATCH carrying it from an accepted team member — including one with the `admin` role — returns 403, and that check runs before any field is applied, so a mixed PATCH cannot partially succeed. Publishing a site exposes its analytics on an unauthenticated `/share/<slug>` URL, so this mirrors the dashboard sharing route, which resolves only for the owner.\n- Setting `isPublic` to true mints a `publicSlug` if the site has none; setting it to false clears the slug and revokes the public dashboard URL.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Partially update site settings; returns the complete updated site.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "example": {
                  "siteId": "6507f1f77bcf86cd799439011",
                  "trackingId": "a1b2c3d4e5",
                  "name": "Marketing site (EU)",
                  "domain": "example.com",
                  "isActive": true,
                  "isPublic": true,
                  "publicSlug": "sunny-otter-42",
                  "publicUrl": "/share/sunny-otter-42",
                  "dataRetentionDays": 365,
                  "maxRetentionDays": 365,
                  "plan": "starter",
                  "role": "owner",
                  "updatedAt": "2025-03-21T09:14:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "New display name (trimmed, non-empty)."
                  },
                  "domain": {
                    "type": "string",
                    "minLength": 1,
                    "description": "New tracked domain (trimmed, non-empty)."
                  },
                  "isActive": {
                    "type": "boolean",
                    "description": "false pauses the site."
                  },
                  "isPublic": {
                    "type": "boolean",
                    "description": "Publish or unpublish the read-only public dashboard. OWNER ONLY — a team member of any role, admin included, receives 403 for a PATCH carrying this field."
                  },
                  "dataRetentionDays": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 1095,
                    "description": "Days of raw data to keep, 30–1095, further capped by the owner plan."
                  },
                  "botDetectionEnabled": {
                    "type": "boolean",
                    "description": "Toggle automatic bot filtering."
                  },
                  "botLogEnabled": {
                    "type": "boolean",
                    "description": "Toggle logging of detected bot traffic."
                  },
                  "recordingEnabled": {
                    "type": "boolean",
                    "description": "Toggle session-recording capture."
                  },
                  "recordingSamplingRate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Percentage of sessions to record, 0–100."
                  },
                  "recordingMaxDuration": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600,
                    "description": "Maximum recording length in seconds, 60–3600."
                  },
                  "recordingPrivacyLevel": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "strict"
                    ],
                    "description": "\"strict\" masks more input content in recordings."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "name": "Marketing site (EU)",
                "dataRetentionDays": 365,
                "isPublic": true
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteSite",
        "summary": "Permanently delete a site and its collected analytics. Irreversible.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<siteId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nRequires the `admin` scope, requires the caller to be the site OWNER (accepted team members with the admin role receive 403), and requires an explicit confirmation. Pass `confirm=<site id>` as a query parameter or as a `confirm` field in the JSON body, repeating whichever identifier you used in the URL (the Mongo _id or the public trackingId).\n\n- Cascade — every child document is removed before the Site itself, across all 23 collections listed in `cascaded`: RecordingChunk, PageView, Event, Goal, Funnel, TeamMember, UserProfile, SessionRecording, SessionSummary, RevenueEvent, WebVital, BotEvent, BotRule, Segment, Alert, Webhook, Dashboard, Annotation, ReportConfig, Insight, Integration, CustomDimension, CustomDimensionValue. No visitor PII (session recordings, visitor profiles, revenue events, Web Vitals) is left orphaned.\n- RecordingChunk is keyed by recording, not by site, so it is resolved from the site’s SessionRecording ids and cleared first.\n- Children are deleted BEFORE the Site document, so a failure part-way through leaves the site intact and the call safe to retry.\n- API keys restricted to this site are deliberately NOT rewritten. An empty site list means \"all of the owner’s sites\", so pulling the deleted id out of a single-site key would silently widen it. A stale id simply never matches again.\n- `deletedCounts` reports the number of documents removed per collection and `totalDeleted` their sum; the Site document itself is not counted.\n- Without a confirmation you get 400 naming the exact value to send.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "siteId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `siteId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently delete a site and its collected analytics. Irreversible.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "siteId": {
                      "type": "string"
                    },
                    "trackingId": {
                      "type": "string"
                    },
                    "cascaded": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Collections whose documents were removed, in a stable order: RecordingChunk, PageView, Event, Goal, Funnel, TeamMember, UserProfile, SessionRecording, SessionSummary, RevenueEvent, WebVital, BotEvent, BotRule, Segment, Alert, Webhook, Dashboard, Annotation, ReportConfig, Insight, Integration, CustomDimension, CustomDimensionValue."
                    },
                    "deletedCounts": {
                      "type": "object",
                      "description": "Documents removed per collection. One entry for every name in `cascaded`.",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "totalDeleted": {
                      "type": "integer",
                      "description": "Sum of every deletedCount. Excludes the Site document itself."
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "siteId": "6507f1f77bcf86cd799439011",
                  "trackingId": "a1b2c3d4e5",
                  "cascaded": [
                    "RecordingChunk",
                    "PageView",
                    "Event",
                    "Goal",
                    "Funnel",
                    "TeamMember",
                    "UserProfile",
                    "SessionRecording",
                    "SessionSummary",
                    "RevenueEvent",
                    "WebVital",
                    "BotEvent",
                    "BotRule",
                    "Segment",
                    "Alert",
                    "Webhook",
                    "Dashboard",
                    "Annotation",
                    "ReportConfig",
                    "Insight",
                    "Integration",
                    "CustomDimension",
                    "CustomDimensionValue"
                  ],
                  "deletedCounts": {
                    "RecordingChunk": 18402,
                    "PageView": 152110,
                    "Event": 4820,
                    "Goal": 3,
                    "Funnel": 1,
                    "TeamMember": 2,
                    "UserProfile": 9120,
                    "SessionRecording": 640,
                    "SessionSummary": 640,
                    "RevenueEvent": 212,
                    "WebVital": 30110,
                    "BotEvent": 880,
                    "BotRule": 0,
                    "Segment": 4,
                    "Alert": 2,
                    "Webhook": 1,
                    "Dashboard": 1,
                    "Annotation": 7,
                    "ReportConfig": 1,
                    "Insight": 12,
                    "Integration": 1,
                    "CustomDimension": 2,
                    "CustomDimensionValue": 5504
                  },
                  "totalDeleted": 222475
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional. Either send no body and use `?confirm=<siteId>`, or send `{ \"confirm\": \"<siteId>\" }`.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must exactly equal the site identifier used in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/stats": {
      "get": {
        "operationId": "siteStats",
        "summary": "Visitors, pageviews, bounce rate, average duration and top-10 breakdowns.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nThe best single call for \"how is my site doing\". Returns headline numbers plus the top 10 pages, referrers, browsers and countries for the window, and echoes the resolved window back as `range`. The numbers are computed with the same maths as the dashboard, so an agent and a human never see two different figures for the same site.\n\n- `avgDuration` is TOTAL time on site divided by UNIQUE VISITORS (a visitor is `visitorId`, falling back to `sessionId`) — not the mean of individual pageview durations. Rounded to one decimal.\n- `bounceRate` is a percentage of sessions, also rounded to one decimal.\n- This endpoint takes only the `range` preset — there are no `from`/`to` overrides here. The echoed `range` object reports the preset plus the exact window used.",
        "tags": [
          "Core analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `7d`, `30d` or `90d`. Default `7d`.",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d",
                "90d"
              ],
              "default": "7d"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Visitors, pageviews, bounce rate, average duration and top-10 breakdowns.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "visitors": {
                      "type": "integer"
                    },
                    "pageviews": {
                      "type": "integer"
                    },
                    "bounceRate": {
                      "type": "number",
                      "description": "Percentage of sessions that bounced, rounded to one decimal."
                    },
                    "avgDuration": {
                      "type": "number",
                      "description": "Total time on site divided by unique visitors, in seconds, rounded to one decimal. Not a per-pageview average."
                    },
                    "topPages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "pathname": {
                            "type": "string"
                          },
                          "views": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "topReferrers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "referrer": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "browsers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "browser": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "country": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation, together with the `range` preset it was derived from.",
                      "properties": {
                        "preset": {
                          "type": "string",
                          "description": "The `range` preset in effect. Explicit `from`/`to` still override the dates."
                        },
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "visitors": 1234,
                  "pageviews": 5678,
                  "bounceRate": 42.3,
                  "avgDuration": 145.7,
                  "topPages": [
                    {
                      "pathname": "/",
                      "views": 2000
                    },
                    {
                      "pathname": "/pricing",
                      "views": 800
                    }
                  ],
                  "topReferrers": [
                    {
                      "referrer": "google.com",
                      "count": 500
                    }
                  ],
                  "browsers": [
                    {
                      "browser": "Chrome",
                      "count": 3000
                    }
                  ],
                  "countries": [
                    {
                      "country": "US",
                      "count": 2000
                    }
                  ],
                  "range": {
                    "preset": "30d",
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/realtime": {
      "get": {
        "operationId": "siteRealtime",
        "summary": "Visitors active in the last 5 minutes and the pages they are on.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nA fixed 5-minute window — this endpoint takes no time-range parameter. Use it for live dashboards and \"is anyone on the site right now\" checks.",
        "tags": [
          "Core analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Visitors active in the last 5 minutes and the pages they are on.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "activeVisitors": {
                      "type": "integer"
                    },
                    "activePages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "string"
                          },
                          "visitors": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "activeVisitors": 42,
                  "activePages": [
                    {
                      "path": "/",
                      "visitors": 15
                    },
                    {
                      "path": "/pricing",
                      "visitors": 8
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/pageviews": {
      "get": {
        "operationId": "sitePageviews",
        "summary": "Pageview total, unique sessions and a daily chart series.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nOptionally filtered to a single pathname, which makes it the right call for \"how did /pricing perform this month\".",
        "tags": [
          "Core analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `7d`, `30d` or `90d`. Default `7d`.",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d",
                "90d"
              ],
              "default": "7d"
            }
          },
          {
            "name": "pathname",
            "in": "query",
            "required": false,
            "description": "Restrict to a single page path, e.g. `/pricing`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pageview total, unique sessions and a daily chart series.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer"
                    },
                    "unique": {
                      "type": "integer",
                      "description": "Unique sessions."
                    },
                    "chart": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "total": 5678,
                  "unique": 1234,
                  "chart": [
                    {
                      "date": "2025-03-21",
                      "count": 800
                    },
                    {
                      "date": "2025-03-22",
                      "count": 920
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/events": {
      "get": {
        "operationId": "siteEvents",
        "summary": "Top 50 custom event names by volume.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nUse this to discover which event names exist before creating an event-type goal or an alert.",
        "tags": [
          "Core analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `7d`, `30d` or `90d`. Default `7d`.",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d",
                "90d"
              ],
              "default": "7d"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Top 50 custom event names by volume.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "events": [
                    {
                      "name": "signup_click",
                      "count": 234
                    },
                    {
                      "name": "download_pdf",
                      "count": 89
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/channels": {
      "get": {
        "operationId": "siteChannels",
        "summary": "Visitors and pageviews per acquisition channel.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nGroups pageviews by referrer / utm_source / utm_medium, classifies each row into a channel and rolls the rows up. Visitors are counted by `visitorId`, falling back to `sessionId`, and are **unioned** across the rows of a channel rather than summed — one person who arrives through three different ChatGPT URLs is one visitor, and the per-channel figures do not add up to the site total because one person can legitimately arrive through two channels. Channel labels: Direct, Organic Search, Paid Search, Paid Social, Organic Social, Email, Referral, AI Assistant, Other.\n\n`AI Assistant` counts people referred by an AI assistant — a human who read an answer in ChatGPT, Perplexity, Claude, Microsoft Copilot, Google Gemini or one of the other assistants in `src/data/ai-assistants.json` and clicked through. That channel, and only that channel, carries an `assistants` array naming the individual assistant. AI *crawlers* (GPTBot, ClaudeBot, PerplexityBot …) are matched on the user agent, are dropped as bots before persistence and never appear in this channel.\n\nCoverage is honest about its limits: attribution is host-based on `document.referrer`, with `utm_source` as a fallback when the assistant tags its outbound links. Assistants that strip the referrer entirely and tag nothing are indistinguishable from Direct and are **not** counted here. Assistant traffic we can recognise but cannot attribute to a specific product is reported inside `assistants` as `AI Assistant (unattributed)` rather than being merged silently into the channel total.",
        "tags": [
          "Acquisition"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "7d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Visitors and pageviews per acquisition channel.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "channels": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "channel": {
                            "type": "string",
                            "enum": [
                              "Direct",
                              "Organic Search",
                              "Paid Search",
                              "Paid Social",
                              "Organic Social",
                              "Email",
                              "Referral",
                              "AI Assistant",
                              "Other"
                            ]
                          },
                          "visitors": {
                            "type": "integer"
                          },
                          "pageviews": {
                            "type": "integer"
                          },
                          "assistants": {
                            "type": "array",
                            "description": "Per-assistant breakdown. Present on the \"AI Assistant\" channel only. `assistant` is the product name from src/data/ai-assistants.json, or \"AI Assistant (unattributed)\" for assistant traffic that could not be attributed to a specific product. Visitors are unique per assistant and do not sum to the channel total.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "assistant": {
                                  "type": "string"
                                },
                                "visitors": {
                                  "type": "integer"
                                },
                                "pageviews": {
                                  "type": "integer"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "channels": [
                    {
                      "channel": "Organic Search",
                      "visitors": 820,
                      "pageviews": 2140
                    },
                    {
                      "channel": "Direct",
                      "visitors": 610,
                      "pageviews": 1502
                    },
                    {
                      "channel": "Referral",
                      "visitors": 133,
                      "pageviews": 288
                    },
                    {
                      "channel": "AI Assistant",
                      "visitors": 96,
                      "pageviews": 211,
                      "assistants": [
                        {
                          "assistant": "ChatGPT",
                          "visitors": 61,
                          "pageviews": 140
                        },
                        {
                          "assistant": "Perplexity",
                          "visitors": 22,
                          "pageviews": 47
                        },
                        {
                          "assistant": "Google Gemini",
                          "visitors": 9,
                          "pageviews": 18
                        },
                        {
                          "assistant": "AI Assistant (unattributed)",
                          "visitors": 4,
                          "pageviews": 6
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/revenue": {
      "get": {
        "operationId": "siteRevenue",
        "summary": "Gross, refunds, net revenue, orders, conversion rate and a daily trend.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nCounts revenue events of type `purchase`, `subscription` and `one_time`. Refunds are summed separately and subtracted to produce `netRevenue`. `conversionRate` is orders divided by unique pageview sessions in the same window, as a percentage rounded to 2 decimals.\n\n- All amounts are integers in the smallest currency unit (cents), exactly as stored by the tracker.",
        "tags": [
          "Revenue"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `30d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "30d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Gross, refunds, net revenue, orders, conversion rate and a daily trend.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "totalRevenue": {
                      "type": "integer",
                      "description": "Gross revenue in cents."
                    },
                    "netRevenue": {
                      "type": "integer",
                      "description": "totalRevenue minus refundTotal, in cents."
                    },
                    "orderCount": {
                      "type": "integer"
                    },
                    "avgOrderValue": {
                      "type": "integer",
                      "description": "In cents."
                    },
                    "conversionRate": {
                      "type": "number",
                      "description": "orders / unique pageview sessions, percentage to 2dp."
                    },
                    "refundTotal": {
                      "type": "integer",
                      "description": "In cents."
                    },
                    "totalSessions": {
                      "type": "integer"
                    },
                    "trend": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "revenue": {
                            "type": "integer"
                          },
                          "orders": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "totalRevenue": 1284500,
                  "netRevenue": 1230500,
                  "orderCount": 143,
                  "avgOrderValue": 8983,
                  "conversionRate": 2.41,
                  "refundTotal": 54000,
                  "totalSessions": 5934,
                  "trend": [
                    {
                      "date": "2025-03-20",
                      "revenue": 42900,
                      "orders": 5
                    },
                    {
                      "date": "2025-03-21",
                      "revenue": 51200,
                      "orders": 6
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/revenue/sources": {
      "get": {
        "operationId": "revenueSources",
        "summary": "Revenue broken down by acquisition source and medium.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nSource falls back utm_source → referrer → `(direct)`; medium falls back to `(none)`. Sorted by revenue descending.",
        "tags": [
          "Revenue"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `30d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "30d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revenue broken down by acquisition source and medium.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "source": {
                            "type": "string"
                          },
                          "medium": {
                            "type": "string"
                          },
                          "revenue": {
                            "type": "integer"
                          },
                          "orders": {
                            "type": "integer"
                          },
                          "sessions": {
                            "type": "integer"
                          },
                          "avgOrderValue": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "sources": [
                    {
                      "source": "google",
                      "medium": "cpc",
                      "revenue": 482300,
                      "orders": 51,
                      "sessions": 1204,
                      "avgOrderValue": 9457
                    }
                  ],
                  "limit": 10,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/revenue/campaigns": {
      "get": {
        "operationId": "revenueCampaigns",
        "summary": "Revenue broken down by utm_campaign, with source, medium and conversion rate.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nOnly revenue events that carry a `utm_campaign` are included. `conversionRate` here is orders divided by sessions within the campaign group, rounded to 1 decimal. Sorted by revenue descending.",
        "tags": [
          "Revenue"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `30d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "30d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revenue broken down by utm_campaign, with source, medium and conversion rate.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "campaigns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "campaign": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string"
                          },
                          "medium": {
                            "type": "string"
                          },
                          "revenue": {
                            "type": "integer"
                          },
                          "orders": {
                            "type": "integer"
                          },
                          "sessions": {
                            "type": "integer"
                          },
                          "avgOrderValue": {
                            "type": "integer"
                          },
                          "conversionRate": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "campaigns": [
                    {
                      "campaign": "spring_sale",
                      "source": "google",
                      "medium": "cpc",
                      "revenue": 302400,
                      "orders": 33,
                      "sessions": 812,
                      "avgOrderValue": 9163,
                      "conversionRate": 4.1
                    }
                  ],
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/revenue/products": {
      "get": {
        "operationId": "revenueProducts",
        "summary": "Revenue broken down by product name and category.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nOnly revenue events that carry a `productName` are included; category falls back to `(uncategorized)`. `avgPrice` is revenue divided by quantity. Sorted by revenue descending — this is the best-seller report.",
        "tags": [
          "Revenue"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `30d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "30d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revenue broken down by product name and category.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "products": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string"
                          },
                          "revenue": {
                            "type": "integer"
                          },
                          "quantity": {
                            "type": "integer"
                          },
                          "orders": {
                            "type": "integer"
                          },
                          "avgPrice": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2024-12-21T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "products": [
                    {
                      "name": "Pro annual plan",
                      "category": "subscriptions",
                      "revenue": 780000,
                      "quantity": 65,
                      "orders": 65,
                      "avgPrice": 12000
                    }
                  ],
                  "limit": 20,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/retention": {
      "get": {
        "operationId": "siteRetention",
        "summary": "Retention matrix plus the averaged retention curve.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nComputed from session summaries with a 5-minute in-process cache. Retention values are percentages rounded to 1 decimal; index 0 is the cohort period itself and is always 100.\n\n- This endpoint does NOT accept the usual `range` presets — it is parameterised by `granularity` and `periods` instead.",
        "tags": [
          "Behaviour & performance"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "description": "Cohort bucket size. Default `week`.",
            "schema": {
              "type": "string",
              "enum": [
                "day",
                "week",
                "month"
              ],
              "default": "week"
            }
          },
          {
            "name": "periods",
            "in": "query",
            "required": false,
            "description": "Number of periods to project, 2–24. Default 8. Alias: `weeks`.",
            "schema": {
              "type": "integer",
              "minimum": 2,
              "maximum": 24,
              "default": 8
            }
          },
          {
            "name": "startDate",
            "in": "query",
            "required": false,
            "description": "ISO-8601 or YYYY-MM-DD. Alias: `from`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "endDate",
            "in": "query",
            "required": false,
            "description": "ISO-8601 or YYYY-MM-DD. Alias: `to`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Retention matrix plus the averaged retention curve.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "granularity": {
                      "type": "string",
                      "enum": [
                        "day",
                        "week",
                        "month"
                      ]
                    },
                    "periods": {
                      "type": "integer"
                    },
                    "averageRetention": {
                      "type": "array",
                      "items": {
                        "type": "number"
                      },
                      "description": "Averaged curve across all cohorts."
                    },
                    "curves": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "cohort": {
                            "type": "string"
                          },
                          "totalUsers": {
                            "type": "integer"
                          },
                          "retention": {
                            "type": "array",
                            "items": {
                              "type": "number"
                            }
                          }
                        }
                      }
                    },
                    "cohorts": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Raw per-cohort rows as computed."
                    }
                  }
                },
                "example": {
                  "granularity": "week",
                  "periods": 8,
                  "averageRetention": [
                    100,
                    41.2,
                    28.7,
                    22.1,
                    18.9,
                    16.4,
                    15,
                    13.8
                  ],
                  "curves": [
                    {
                      "cohort": "2025-01-06",
                      "totalUsers": 512,
                      "retention": [
                        100,
                        44.1,
                        30.2,
                        24,
                        20.1,
                        17.2,
                        15.4,
                        14.1
                      ]
                    }
                  ],
                  "cohorts": [
                    {
                      "cohort": "2025-01-06",
                      "totalUsers": 512,
                      "periods": [
                        100,
                        44.1,
                        30.2,
                        24,
                        20.1,
                        17.2,
                        15.4,
                        14.1
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/journeys": {
      "get": {
        "operationId": "siteJourneys",
        "summary": "Top navigation paths, entry pages, exit pages and pages per session.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\n`topJourneys` groups sessions with more than one pageview by their first 5 pathnames in timestamp order. `entryPages` / `exitPages` are the first and last pathname of each session. `avgPagesPerSession` is the mean pageview count per session, rounded to 1 decimal.\n\n- `limit` and `offset` apply to `topJourneys`, `entryPages` and `exitPages` alike.",
        "tags": [
          "Behaviour & performance"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "7d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Top navigation paths, entry pages, exit pages and pages per session.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "topJourneys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "maxItems": 5
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "entryPages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "page": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "exitPages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "page": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "avgPagesPerSession": {
                      "type": "number"
                    },
                    "totalSessions": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "topJourneys": [
                    {
                      "path": [
                        "/",
                        "/pricing",
                        "/signup"
                      ],
                      "count": 184
                    }
                  ],
                  "entryPages": [
                    {
                      "page": "/",
                      "count": 2410
                    }
                  ],
                  "exitPages": [
                    {
                      "page": "/pricing",
                      "count": 890
                    }
                  ],
                  "avgPagesPerSession": 2.4,
                  "totalSessions": 5934,
                  "limit": 10,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/vitals": {
      "get": {
        "operationId": "siteVitals",
        "summary": "p50/p75/p99 for LCP, FID, CLS, FCP, TTFB and INP with per-page and per-device breakdowns.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\n`summary` carries p50/p75/p99 for each metric (null when there are no samples) plus good / needsImprovement / poor percentages for the four core metrics. Thresholds: LCP 2500/4000 ms, FID 100/300 ms, CLS 0.1/0.25, INP 200/500 ms. `trend` is a daily p75 series for LCP, FID and CLS; `byPage` and `byDevice` give p75 for LCP, CLS and INP with sample counts.\n\n- `limit` / `offset` paginate `byPage` only (sorted by sample count desc). `byDevice` and `trend` are always returned in full.",
        "tags": [
          "Behaviour & performance"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "7d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "p50/p75/p99 for LCP, FID, CLS, FCP, TTFB and INP with per-page and per-device breakdowns.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "Keyed by metric: lcp, fid, cls, fcp, ttfb, inp.",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/VitalSummaryEntry"
                      }
                    },
                    "trend": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "lcp_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "fid_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "cls_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "byPage": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "pathname": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          },
                          "lcp_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "cls_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "inp_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "byDevice": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "device": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          },
                          "lcp_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "cls_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "inp_p75": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "summary": {
                    "lcp": {
                      "p50": 1820,
                      "p75": 2410,
                      "p99": 5120,
                      "good": 78.2,
                      "needsImprovement": 16.4,
                      "poor": 5.4
                    },
                    "cls": {
                      "p50": 0.02,
                      "p75": 0.08,
                      "p99": 0.31,
                      "good": 88.1,
                      "needsImprovement": 8.6,
                      "poor": 3.3
                    },
                    "ttfb": {
                      "p50": 210,
                      "p75": 380,
                      "p99": 1290
                    }
                  },
                  "trend": [
                    {
                      "date": "2025-03-21",
                      "lcp_p75": 2380,
                      "fid_p75": 12,
                      "cls_p75": 0.07
                    }
                  ],
                  "byPage": [
                    {
                      "pathname": "/",
                      "count": 1820,
                      "lcp_p75": 2210,
                      "cls_p75": 0.05,
                      "inp_p75": 148
                    }
                  ],
                  "byDevice": [
                    {
                      "device": "desktop",
                      "count": 3120,
                      "lcp_p75": 1980,
                      "cls_p75": 0.04,
                      "inp_p75": 122
                    }
                  ],
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/bot-events": {
      "get": {
        "operationId": "siteBotEvents",
        "summary": "Beacons this site refused before storing them, broken down by reason, by ingestion path and by day.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nEvery beacon this product judges a bot is discarded before storage and answered with the same 204 an accepted beacon gets, so this endpoint is the only way an integrator can see what left the dataset. `byReason` carries the machine identifiers (`ua_pattern`, `referrer_spam`, `datacenter_ip`, `js_challenge`, `custom_rule`) each with a `costOfBeingWrong` line naming who a false positive on it actually is -- that is the difference between \"we blocked 43 bots\" and \"we deleted 12 of your customers\". `byPath` says which of the three ingestion routes refused it (`collect`, `vitals`, `recordings`); rows written before the path was recorded report `unknown` rather than being attributed to a guess. `sample` is the most recent rows with the hashed address, so the counts can be checked against real user agents rather than trusted. The rule itself is documented in `docs/bot-rule.md`.\n\n- `sessions` is the number of DISTINCT VISITS behind the refusals, counted once across every reason. `byReason[].sessions` is a per-reason set and the sets overlap, so adding them up over-counts -- use the top-level figure.\n- `datacenter_ip` is off by default. A site only produces it when its owner sets `treatDatacenterAsBot` to `\"corroborated\"` (refuse when the client also fails to identify a browser engine) or `true` (refuse on the address alone).\n- `ip` is a truncated SHA-256 of the client address, never the address. It is stable within a site, so repeat offenders group, and it does not reverse to an IP.\n- `retention.actual` is `indefinite` and says so deliberately: `BotEvent` declares a 30-day TTL index that has never been built, and `Site.dataRetentionDays` is validated, plan-capped, displayed and enforced by nothing. Do not build a compliance report on the declared figure.\n- `limit` / `offset` paginate `sample` only. `byReason`, `byPath` and `byDay` are always returned in full.",
        "tags": [
          "Behaviour & performance"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "7d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Beacons this site refused before storing them, broken down by reason, by ingestion path and by day.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Refusals in the window."
                    },
                    "sessions": {
                      "type": "integer",
                      "description": "Distinct visits behind those refusals, counted once across every reason. Never the sum of byReason[].sessions."
                    },
                    "addresses": {
                      "type": "integer",
                      "description": "Distinct hashed client addresses behind those refusals, counted once across every reason."
                    },
                    "byReason": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "reason": {
                            "type": "string",
                            "enum": [
                              "ua_pattern",
                              "referrer_spam",
                              "datacenter_ip",
                              "js_challenge",
                              "custom_rule"
                            ]
                          },
                          "count": {
                            "type": "integer"
                          },
                          "sessions": {
                            "type": "integer",
                            "description": "Distinct visits refused for THIS reason. Overlaps the other reasons; do not sum."
                          },
                          "addresses": {
                            "type": "integer"
                          },
                          "costOfBeingWrong": {
                            "type": "string",
                            "description": "Who a false positive on this reason actually is, in the terms a customer would use."
                          }
                        }
                      }
                    },
                    "byPath": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "string",
                            "enum": [
                              "collect",
                              "vitals",
                              "recordings",
                              "unknown"
                            ],
                            "description": "Which ingestion route refused it. `unknown` means the row predates the field."
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "byDay": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "sample": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "reason": {
                            "type": "string"
                          },
                          "path": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sessionId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "ip": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Truncated SHA-256 of the client address, never the address."
                          },
                          "userAgent": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "referrer": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "timestamp": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "retention": {
                      "type": "object",
                      "description": "What this record is DECLARED to be kept for, and what it is ACTUALLY kept for.",
                      "properties": {
                        "declared": {
                          "type": "string"
                        },
                        "actual": {
                          "type": "string"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "range": {
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  },
                  "total": 2351,
                  "sessions": 1804,
                  "addresses": 972,
                  "byReason": [
                    {
                      "reason": "ua_pattern",
                      "count": 2351,
                      "sessions": 1804,
                      "addresses": 972,
                      "costOfBeingWrong": "a browser whose user agent matched a bot pattern. Cheap to get wrong: the patterns match self-identifying crawlers and headless automation, which no ordinary visitor sends."
                    }
                  ],
                  "byPath": [
                    {
                      "path": "collect",
                      "count": 2301
                    },
                    {
                      "path": "vitals",
                      "count": 44
                    },
                    {
                      "path": "unknown",
                      "count": 6
                    }
                  ],
                  "byDay": [
                    {
                      "date": "2025-03-21",
                      "count": 84
                    }
                  ],
                  "sample": [
                    {
                      "reason": "ua_pattern",
                      "path": "collect",
                      "sessionId": "s_8f2a1c",
                      "ip": "9d2f1a4b8c3e5077",
                      "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/146.0.0.0 Safari/537.36",
                      "referrer": null,
                      "timestamp": "2025-03-21T09:14:02.118Z"
                    }
                  ],
                  "retention": {
                    "declared": "30 days, by the TTL index on BotEvent.timestamp",
                    "actual": "indefinite",
                    "note": "The TTL index is declared and has never been built."
                  },
                  "limit": 5,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/profiles": {
      "get": {
        "operationId": "listProfiles",
        "summary": "Search, filter, sort and page through visitor profiles.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nProfiles are lifetime records. Date parameters are optional here: pass `range`, or `from` together with `to`, to additionally restrict on `lastSeenAt`; omit them to get the full lifetime list.\n\n- The `q` search input is regex-escaped before use, so it is safe to pass raw user input.",
        "tags": [
          "Visitor profiles"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search (max 100 chars), case-insensitive, over email, identifiedId, name and anonymousId.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Match a profile tag.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Match `lastCountry`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "device",
            "in": "query",
            "required": false,
            "description": "Match `lastDevice`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "identified",
            "in": "query",
            "required": false,
            "description": "Only identified or only anonymous profiles. Must be exactly `true` or `false`, otherwise 400.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "One of lastSeenAt, firstSeenAt, totalRevenue. Default `lastSeenAt`; unknown values fall back to it. `totalPageviews` and `totalEvents` are deliberately NOT sortable: both counters are incremented on essentially every pageview, so the indexes such a sort would need are too expensive to maintain on that write path.",
            "schema": {
              "type": "string",
              "enum": [
                "lastSeenAt",
                "firstSeenAt",
                "totalRevenue"
              ],
              "default": "lastSeenAt"
            }
          },
          {
            "name": "dir",
            "in": "query",
            "required": false,
            "description": "Sort direction. Default `desc`.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "90d",
                "6m",
                "12m"
              ],
              "default": "7d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search, filter, sort and page through visitor profiles.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "profiles": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "UserProfile document."
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "range": {
                      "type": "object",
                      "description": "Present only when a date parameter was supplied.",
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "field": {
                          "type": "string",
                          "const": "lastSeenAt"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "profiles": [
                    {
                      "_id": "65121108bcf86cd799439118",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "anonymousId": "s_9f2c...",
                      "identifiedId": "user_8412",
                      "email": "ada@example.com",
                      "name": "Ada Lovelace",
                      "tags": [
                        "trial"
                      ],
                      "lastCountry": "GB",
                      "lastDevice": "desktop",
                      "totalPageviews": 84,
                      "totalEvents": 12,
                      "totalRevenue": 24000,
                      "firstSeenAt": "2025-01-04T11:02:00.000Z",
                      "lastSeenAt": "2025-03-20T18:41:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 20,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/profiles/{profileId}": {
      "get": {
        "operationId": "getProfile",
        "summary": "One visitor profile plus an interleaved recent-activity timeline.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nThe timeline interleaves the most recent events and pageviews for the profile, newest first — 25 of each by default, or `limit` of each when `limit` is supplied (max 500).\n\n- The profile must belong to `{siteId}`. An invalid id, or a profile from another site, returns 404 \"Profile not found\".",
        "tags": [
          "Visitor profiles"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "profileId",
            "in": "path",
            "required": true,
            "description": "Identifier of the visitor profile.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Timeline entries to fetch from EACH source (events and pageviews). Default 25, max 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One visitor profile plus an interleaved recent-activity timeline.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "profile": {
                      "type": "object",
                      "description": "UserProfile document."
                    },
                    "timeline": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "event",
                              "pageview"
                            ]
                          },
                          "name": {
                            "type": "string",
                            "description": "Events only."
                          },
                          "pathname": {
                            "type": "string"
                          },
                          "metadata": {
                            "type": "object",
                            "description": "Events only."
                          },
                          "duration": {
                            "type": "integer",
                            "description": "Pageviews only, in seconds."
                          },
                          "timestamp": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "profile": {
                    "_id": "65121108bcf86cd799439118",
                    "siteId": "6507f1f77bcf86cd799439011",
                    "anonymousId": "s_9f2c...",
                    "email": "ada@example.com",
                    "totalPageviews": 84,
                    "totalRevenue": 24000,
                    "lastSeenAt": "2025-03-20T18:41:00.000Z"
                  },
                  "timeline": [
                    {
                      "type": "event",
                      "name": "signup_click",
                      "pathname": "/pricing",
                      "metadata": {
                        "plan": "pro"
                      },
                      "timestamp": "2025-03-20T18:41:00.000Z"
                    },
                    {
                      "type": "pageview",
                      "pathname": "/pricing",
                      "duration": 42,
                      "timestamp": "2025-03-20T18:39:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/goals": {
      "get": {
        "operationId": "listGoals",
        "summary": "Goals for a site, newest first, with conversions in the window.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nEach goal carries the number of unique converting sessions for the window. Available to the site owner and to accepted team members of any role.\n\n- The resolved window is echoed as a `range` OBJECT — `{ preset, since, until }` — and only when `includeConversions` is not disabled. The earlier flat `range`/`since`/`until` fields no longer exist.\n- Conversion counts for the whole page are computed with at most two aggregations regardless of `limit`, so requesting `limit=500` costs the same as `limit=10`.\n- An unusable window (unparseable `from`/`to`, or a window that ends before it starts) returns 400 rather than silently reporting zero conversions. `GET /goals/{goalId}` and `GET /funnels/{funnelId}/stats` now behave identically.\n- Date-only `from`/`to` cover WHOLE days: `to=2025-03-21` runs through `2025-03-21T23:59:59.999Z`, so `from=X&to=X` is a full 24-hour day. It previously produced an empty window that always reported zero.",
        "tags": [
          "Goals"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `24h`, `7d`, `30d`, `90d` or `365d`. Default `30d`. Overridden by `from`/`to`.",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d",
                "90d",
                "365d"
              ],
              "default": "30d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start, overriding `range`. A date-only `YYYY-MM-DD` value means that day from `T00:00:00.000Z`; a value carrying a time is an instant and is used verbatim. Supplying only `from` gives `[from, now]`. An unparseable value, or a window that ends before it starts, returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end, overriding `range`. A date-only `YYYY-MM-DD` value covers the WHOLE day, through `T23:59:59.999Z` — the final day is included, and `from=X&to=X` is a full 24 hours rather than the zero-width window it used to be. A value carrying a time is used verbatim. An end in the future is clamped to now, except where clamping would invert the window. Supplying only `to` gives `[to - range, to]`. An unparseable value, or a window that ends before it starts, returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeConversions",
            "in": "query",
            "required": false,
            "description": "Set to `false` or `0` to skip the conversion aggregation (much faster). Any other value, and omitting it, counts as true.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goals for a site, newest first, with conversions in the window.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goals": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Goal"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "range": {
                      "type": "object",
                      "description": "Resolved time window actually used for the aggregation, together with the `range` preset it was derived from.",
                      "properties": {
                        "preset": {
                          "type": "string",
                          "description": "The `range` preset in effect. Explicit `from`/`to` still override the dates."
                        },
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "until": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "goals": [
                    {
                      "goalId": "6512aa01bcf86cd7994390a1",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "name": "Signup completed",
                      "type": "pageview",
                      "target": "/welcome",
                      "conversions": 213,
                      "createdAt": "2025-02-01T08:00:00.000Z",
                      "updatedAt": "2025-02-01T08:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "range": {
                    "preset": "30d",
                    "since": "2025-02-19T00:00:00.000Z",
                    "until": "2025-03-21T00:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createGoal",
        "summary": "Define a conversion goal.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nType `pageview` matches a PageView whose pathname equals `target`; type `event` matches an Event whose name equals `target`. Call the events endpoint first if you need to discover valid event names.\n\n- Team members with the `viewer` role receive 403.",
        "tags": [
          "Goals"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Define a conversion goal.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Goal"
                },
                "example": {
                  "goalId": "6512aa01bcf86cd7994390a1",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "Signup completed",
                  "type": "pageview",
                  "target": "/welcome",
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Display name. Non-empty after trimming."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "pageview",
                      "event"
                    ],
                    "description": "What kind of activity converts this goal."
                  },
                  "target": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Pathname (type=pageview) or custom event name (type=event). Non-empty after trimming."
                  }
                },
                "required": [
                  "name",
                  "type",
                  "target"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "Signup completed",
                "type": "pageview",
                "target": "/welcome"
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/goals/{goalId}": {
      "get": {
        "operationId": "getGoal",
        "summary": "One goal with its conversion count for the window.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nAccepts the same window parameters as the list endpoint.\n\n- Returns 404 \"Goal not found\" when the id is malformed or belongs to another site.\n- Unlike the list endpoint, this one echoes a FLAT `range` string alongside top-level `since` and `until` rather than a `range` object. Those three fields, and `conversions`, are omitted entirely when `includeConversions` is disabled.\n- An unparseable or inverted `from`/`to` returns 400 here too, matching the list endpoint; it is no longer silently downgraded to the `range` preset. Date-only values cover whole days, so `from=X&to=X` is a full 24-hour day rather than an empty window.",
        "tags": [
          "Goals"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "goalId",
            "in": "path",
            "required": true,
            "description": "Identifier of the goal.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `24h`, `7d`, `30d`, `90d` or `365d`. Default `30d`. Overridden by `from`/`to`.",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d",
                "90d",
                "365d"
              ],
              "default": "30d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start, overriding `range`. A date-only `YYYY-MM-DD` value means that day from `T00:00:00.000Z`; a value carrying a time is an instant and is used verbatim. Supplying only `from` gives `[from, now]`. An unparseable value, or a window that ends before it starts, returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end, overriding `range`. A date-only `YYYY-MM-DD` value covers the WHOLE day, through `T23:59:59.999Z` — the final day is included, and `from=X&to=X` is a full 24 hours rather than the zero-width window it used to be. A value carrying a time is used verbatim. An end in the future is clamped to now, except where clamping would invert the window. Supplying only `to` gives `[to - range, to]`. An unparseable value, or a window that ends before it starts, returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeConversions",
            "in": "query",
            "required": false,
            "description": "Set to `false` or `0` to skip the conversion aggregation (much faster). Any other value, and omitting it, counts as true.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One goal with its conversion count for the window.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Goal"
                    }
                  ],
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "string",
                      "description": "Echoed window preset. Omitted when includeConversions=false."
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "until": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "goalId": "6512aa01bcf86cd7994390a1",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "Signup completed",
                  "type": "pageview",
                  "target": "/welcome",
                  "conversions": 213,
                  "createdAt": "2025-02-01T08:00:00.000Z",
                  "updatedAt": "2025-02-01T08:00:00.000Z",
                  "range": "30d",
                  "since": "2025-02-19T00:00:00.000Z",
                  "until": "2025-03-21T00:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateGoal",
        "summary": "Change a goal's name, type and/or target.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nAt least one field is required. Mongoose validators run on update; the updated goal is returned.\n\n- Viewers receive 403.",
        "tags": [
          "Goals"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "goalId",
            "in": "path",
            "required": true,
            "description": "Identifier of the goal.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Change a goal's name, type and/or target.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Goal"
                },
                "example": {
                  "goalId": "6512aa01bcf86cd7994390a1",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "Signup completed",
                  "type": "pageview",
                  "target": "/thank-you",
                  "createdAt": "2025-02-01T08:00:00.000Z",
                  "updatedAt": "2025-03-21T09:20:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "New display name."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "pageview",
                      "event"
                    ],
                    "description": "New goal type."
                  },
                  "target": {
                    "type": "string",
                    "minLength": 1,
                    "description": "New pathname or event name."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "target": "/thank-you"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteGoal",
        "summary": "Permanently delete a goal. Requires the admin scope and a confirmation.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<goalId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nDeleting a goal destroys the definition and every conversion report built on it. Pass `confirm=<goalId>` as a query parameter or as a `confirm` field in the JSON body — it must equal the 24-character goal id from the URL, not the goal name.\n\n- Viewers receive 403; an unknown id returns 404.\n- The confirmation is checked AFTER the scope and site-access checks but BEFORE the id is validated, so a wrong `confirm` with a non-existent id returns 400, never a 404 — a missing confirmation never reveals whether the goal exists.",
        "tags": [
          "Goals"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "goalId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "goalId",
            "in": "path",
            "required": true,
            "description": "Identifier of the goal.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `goalId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently delete a goal. Requires the admin scope and a confirmation.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "goalId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "goalId"
                  ]
                },
                "example": {
                  "deleted": true,
                  "goalId": "6512aa01bcf86cd7994390a1"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional — send `{ \"confirm\": \"<goalId>\" }` instead of the query parameter if you prefer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must equal the goalId in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/funnels": {
      "get": {
        "operationId": "listFunnels",
        "summary": "Funnel definitions for a site, newest first.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nDefinitions only — no computed results. Available to the site owner and accepted team members of any role.",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Funnel definitions for a site, newest first.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "funnels": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Funnel"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "funnels": [
                    {
                      "funnelId": "6512bb02bcf86cd7994390b2",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "name": "Checkout",
                      "steps": [
                        {
                          "name": "Cart",
                          "pathname": "/cart"
                        },
                        {
                          "name": "Payment",
                          "pathname": "/checkout"
                        },
                        {
                          "name": "Done",
                          "pathname": "/thank-you"
                        }
                      ],
                      "createdAt": "2025-02-10T08:00:00.000Z",
                      "updatedAt": "2025-02-10T08:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createFunnel",
        "summary": "Define an ordered funnel with at least two steps.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nEach step needs a non-empty name and pathname. Order matters — step 1 is the entry point used as the conversion baseline.\n\n- Viewers receive 403.",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Define an ordered funnel with at least two steps.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Funnel"
                },
                "example": {
                  "funnelId": "6512bb02bcf86cd7994390b2",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "Checkout",
                  "steps": [
                    {
                      "name": "Cart",
                      "pathname": "/cart"
                    },
                    {
                      "name": "Payment",
                      "pathname": "/checkout"
                    },
                    {
                      "name": "Done",
                      "pathname": "/thank-you"
                    }
                  ],
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Funnel display name."
                  },
                  "steps": {
                    "type": "array",
                    "minItems": 2,
                    "items": {
                      "$ref": "#/components/schemas/FunnelStep"
                    },
                    "description": "Ordered steps. Minimum 2 items."
                  }
                },
                "required": [
                  "name",
                  "steps"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "Checkout",
                "steps": [
                  {
                    "name": "Cart",
                    "pathname": "/cart"
                  },
                  {
                    "name": "Payment",
                    "pathname": "/checkout"
                  },
                  {
                    "name": "Done",
                    "pathname": "/thank-you"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/funnels/{funnelId}": {
      "get": {
        "operationId": "getFunnel",
        "summary": "One funnel definition with its ordered steps.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nNo computed results here — use the funnel stats sub-resource for those.\n\n- Returns 404 \"Funnel not found\" for malformed or foreign ids.",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "funnelId",
            "in": "path",
            "required": true,
            "description": "Identifier of the funnel.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One funnel definition with its ordered steps.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Funnel"
                },
                "example": {
                  "funnelId": "6512bb02bcf86cd7994390b2",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "Checkout",
                  "steps": [
                    {
                      "name": "Cart",
                      "pathname": "/cart"
                    },
                    {
                      "name": "Payment",
                      "pathname": "/checkout"
                    }
                  ],
                  "createdAt": "2025-02-10T08:00:00.000Z",
                  "updatedAt": "2025-02-10T08:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateFunnel",
        "summary": "Change a funnel's name and/or steps.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nAt least one field is required. `steps` REPLACES the entire array — it is not merged — and the replacement must still contain at least 2 steps.\n\n- Viewers receive 403.",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "funnelId",
            "in": "path",
            "required": true,
            "description": "Identifier of the funnel.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Change a funnel's name and/or steps.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Funnel"
                },
                "example": {
                  "funnelId": "6512bb02bcf86cd7994390b2",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "Checkout v2",
                  "steps": [
                    {
                      "name": "Cart",
                      "pathname": "/cart"
                    },
                    {
                      "name": "Payment",
                      "pathname": "/checkout"
                    }
                  ],
                  "updatedAt": "2025-03-21T09:25:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "New funnel name."
                  },
                  "steps": {
                    "type": "array",
                    "minItems": 2,
                    "items": {
                      "$ref": "#/components/schemas/FunnelStep"
                    },
                    "description": "Full replacement step list, minimum 2 items."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "name": "Checkout v2"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteFunnel",
        "summary": "Permanently delete a funnel. Requires the admin scope and a confirmation.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<funnelId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nDeleting a funnel destroys the step definition and every drop-off report built on it. Pass `confirm=<funnelId>` as a query parameter or as a `confirm` field in the JSON body — it must equal the 24-character funnel id from the URL, not the funnel name.\n\n- Viewers receive 403; an unknown id returns 404.\n- The confirmation is checked AFTER the scope and site-access checks but BEFORE the id is validated, so a wrong `confirm` with a non-existent id returns 400, never a 404.",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "funnelId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "funnelId",
            "in": "path",
            "required": true,
            "description": "Identifier of the funnel.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `funnelId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently delete a funnel. Requires the admin scope and a confirmation.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "funnelId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "funnelId"
                  ]
                },
                "example": {
                  "deleted": true,
                  "funnelId": "6512bb02bcf86cd7994390b2"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional — send `{ \"confirm\": \"<funnelId>\" }` instead of the query parameter if you prefer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must equal the funnelId in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/funnels/{funnelId}/stats": {
      "get": {
        "operationId": "funnelStats",
        "summary": "Sessions per step, drop-off and conversion rates.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nFor each step: the number of unique sessions that hit the step pathname, the drop-off rate versus the previous step, and the conversion rate versus step 1. `overallConversionRate` is first-step to last-step. Percentages are rounded to one decimal. Same maths as the dashboard.\n\n- The resolved window is echoed as a FLAT `range` string alongside top-level `since` and `until`, not as a `range` object. This and `GET /goals/{goalId}` are the only two endpoints that still do.\n- An unusable window (unparseable `from`/`to`, or one that ends before it starts) returns 400, matching the goal endpoints, rather than silently falling back to the `range` preset.\n- Date-only `from`/`to` cover WHOLE days: `to=2025-03-21` runs through `2025-03-21T23:59:59.999Z`, so `from=X&to=X` is a full 24-hour day.",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "funnelId",
            "in": "path",
            "required": true,
            "description": "Identifier of the funnel.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Relative window: `24h`, `7d`, `30d`, `90d` or `365d`. Default `30d`. Overridden by `from`/`to`.",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d",
                "90d",
                "365d"
              ],
              "default": "30d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Explicit window start, overriding `range`. A date-only `YYYY-MM-DD` value means that day from `T00:00:00.000Z`; a value carrying a time is an instant and is used verbatim. Supplying only `from` gives `[from, now]`. An unparseable value, or a window that ends before it starts, returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Explicit window end, overriding `range`. A date-only `YYYY-MM-DD` value covers the WHOLE day, through `T23:59:59.999Z` — the final day is included, and `from=X&to=X` is a full 24 hours rather than the zero-width window it used to be. A value carrying a time is used verbatim. An end in the future is clamped to now, except where clamping would invert the window. Supplying only `to` gives `[to - range, to]`. An unparseable value, or a window that ends before it starts, returns 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sessions per step, drop-off and conversion rates.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "funnel": {
                      "type": "object",
                      "properties": {
                        "funnelId": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "range": {
                      "type": "string"
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "until": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "steps": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "pathname": {
                            "type": "string"
                          },
                          "sessions": {
                            "type": "integer"
                          },
                          "dropoffRate": {
                            "type": "number",
                            "description": "Percentage lost versus the previous step."
                          },
                          "conversionRate": {
                            "type": "number",
                            "description": "Percentage of step-1 sessions reaching this step."
                          }
                        }
                      }
                    },
                    "overallConversionRate": {
                      "type": "number"
                    }
                  }
                },
                "example": {
                  "funnel": {
                    "funnelId": "6512bb02bcf86cd7994390b2",
                    "name": "Checkout"
                  },
                  "range": "30d",
                  "since": "2025-02-19T00:00:00.000Z",
                  "until": "2025-03-21T00:00:00.000Z",
                  "steps": [
                    {
                      "name": "Cart",
                      "pathname": "/cart",
                      "sessions": 1000,
                      "dropoffRate": 0,
                      "conversionRate": 100
                    },
                    {
                      "name": "Payment",
                      "pathname": "/checkout",
                      "sessions": 620,
                      "dropoffRate": 38,
                      "conversionRate": 62
                    },
                    {
                      "name": "Done",
                      "pathname": "/thank-you",
                      "sessions": 415,
                      "dropoffRate": 33.1,
                      "conversionRate": 41.5
                    }
                  ],
                  "overallConversionRate": 41.5
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/segments": {
      "get": {
        "operationId": "listSegments",
        "summary": "Saved segments for this site that belong to the key's own user.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nSegments are per-user in the data model, so this list never contains another user’s segments even for the site owner.",
        "tags": [
          "Segments"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Saved segments for this site that belong to the key's own user.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "segments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Segment"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "segments": [
                    {
                      "segmentId": "6512cc03bcf86cd7994390c3",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "name": "UK mobile",
                      "filters": {
                        "country": "GB",
                        "device": "mobile"
                      },
                      "createdAt": "2025-02-14T08:00:00.000Z",
                      "updatedAt": "2025-02-14T08:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createSegment",
        "summary": "Save a reusable audience filter owned by the key holder.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nAll filter keys are optional strings and unknown keys are stripped. Supplied filters are ANDed together. There is no viewer restriction here — segments are personal, matching the dashboard route which only checks site access.",
        "tags": [
          "Segments"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Save a reusable audience filter owned by the key holder.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Segment"
                },
                "example": {
                  "segmentId": "6512cc03bcf86cd7994390c3",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "UK mobile",
                  "filters": {
                    "country": "GB",
                    "device": "mobile"
                  },
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Segment display name."
                  },
                  "filters": {
                    "$ref": "#/components/schemas/SegmentFilters",
                    "description": "Any of: dateRange, country, city, browser, os, device, referrer, utm_source, utm_medium, utm_campaign, pathname. All optional strings."
                  }
                },
                "required": [
                  "name",
                  "filters"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "UK mobile",
                "filters": {
                  "country": "GB",
                  "device": "mobile"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/segments/{segmentId}": {
      "get": {
        "operationId": "getSegment",
        "summary": "One saved segment belonging to the key holder.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nScoped to the key owner’s own segments and to this site; anything else returns 404 \"Segment not found\".",
        "tags": [
          "Segments"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "segmentId",
            "in": "path",
            "required": true,
            "description": "Identifier of the segment.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One saved segment belonging to the key holder.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Segment"
                },
                "example": {
                  "segmentId": "6512cc03bcf86cd7994390c3",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "UK mobile",
                  "filters": {
                    "country": "GB",
                    "device": "mobile"
                  },
                  "createdAt": "2025-02-14T08:00:00.000Z",
                  "updatedAt": "2025-02-14T08:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateSegment",
        "summary": "Rename a segment and/or replace its filters.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nAt least one field is required. `filters` REPLACES the whole filter object rather than merging — send every filter you want to keep. Only the key owner’s own segments can be updated.",
        "tags": [
          "Segments"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "segmentId",
            "in": "path",
            "required": true,
            "description": "Identifier of the segment.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rename a segment and/or replace its filters.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Segment"
                },
                "example": {
                  "segmentId": "6512cc03bcf86cd7994390c3",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "name": "UK mobile",
                  "filters": {
                    "country": "GB",
                    "device": "mobile",
                    "browser": "Safari"
                  },
                  "updatedAt": "2025-03-21T09:30:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "New segment name."
                  },
                  "filters": {
                    "$ref": "#/components/schemas/SegmentFilters",
                    "description": "Full replacement filter object."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "filters": {
                  "country": "GB",
                  "device": "mobile",
                  "browser": "Safari"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteSegment",
        "summary": "Permanently delete a saved segment. Requires the admin scope and a confirmation.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<segmentId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nPass `confirm=<segmentId>` as a query parameter or as a `confirm` field in the JSON body — it must equal the 24-character segment id from the URL, not the segment name. Only the key owner’s own segments are visible or deletable; an unknown id returns 404.\n\n- Segments are per-user, so there is no separate viewer check here — the delete only ever matches your own segments.\n- The confirmation is checked BEFORE the id is validated, so a wrong `confirm` with a non-existent id returns 400, never a 404.",
        "tags": [
          "Segments"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "segmentId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "segmentId",
            "in": "path",
            "required": true,
            "description": "Identifier of the segment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `segmentId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently delete a saved segment. Requires the admin scope and a confirmation.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "segmentId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "segmentId"
                  ]
                },
                "example": {
                  "deleted": true,
                  "segmentId": "6512cc03bcf86cd7994390c3"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional — send `{ \"confirm\": \"<segmentId>\" }` instead of the query parameter if you prefer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must equal the segmentId in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/alerts": {
      "get": {
        "operationId": "listAlerts",
        "summary": "Alerts the key's owner has configured for the site.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nSorted by creation date descending.",
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "enabled",
            "in": "query",
            "required": false,
            "description": "Filter to only enabled (`true`) or only disabled (`false`) records. Omit for all.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts the key's owner has configured for the site.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "alerts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Alert"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "alerts": [
                    {
                      "alertId": "6512dd04bcf86cd7994390d4",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "type": "traffic_drop",
                      "threshold": 30,
                      "comparison": "previous_week",
                      "enabled": true,
                      "email": "ops@example.com",
                      "lastTriggeredAt": null,
                      "createdAt": "2025-02-20T08:00:00.000Z",
                      "updatedAt": "2025-02-20T08:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAlert",
        "summary": "Create an email alert owned by the key holder.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nFor `previous_day` and `previous_week` comparisons the threshold is a percentage change; for `absolute` it is a raw count. `email` defaults to the key owner’s account email.\n\n- Viewers receive 403.",
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Create an email alert owned by the key holder.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                },
                "example": {
                  "alertId": "6512dd04bcf86cd7994390d4",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "type": "traffic_drop",
                  "threshold": 30,
                  "comparison": "previous_week",
                  "enabled": true,
                  "email": "ops@example.com",
                  "lastTriggeredAt": null,
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "traffic_spike",
                      "traffic_drop",
                      "goal_completed",
                      "error_rate"
                    ],
                    "description": "What the alert watches. Immutable after creation."
                  },
                  "threshold": {
                    "type": "number",
                    "minimum": 1,
                    "description": "Minimum 1. Percentage or raw count depending on `comparison`."
                  },
                  "comparison": {
                    "type": "string",
                    "enum": [
                      "previous_day",
                      "previous_week",
                      "absolute"
                    ],
                    "description": "Baseline the threshold is measured against."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Recipient. Defaults to the key owner's account email."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Start enabled or muted."
                  }
                },
                "required": [
                  "type",
                  "threshold",
                  "comparison"
                ],
                "additionalProperties": false
              },
              "example": {
                "type": "traffic_drop",
                "threshold": 30,
                "comparison": "previous_week",
                "email": "ops@example.com"
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/alerts/{alertId}": {
      "get": {
        "operationId": "getAlert",
        "summary": "One alert owned by the key's owner.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nReturns 404 \"Alert not found\" if it belongs to another user or another site.",
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "alertId",
            "in": "path",
            "required": true,
            "description": "Identifier of the alert.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One alert owned by the key's owner.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                },
                "example": {
                  "alertId": "6512dd04bcf86cd7994390d4",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "type": "traffic_drop",
                  "threshold": 30,
                  "comparison": "previous_week",
                  "enabled": true,
                  "email": "ops@example.com",
                  "lastTriggeredAt": "2025-03-18T06:00:00.000Z",
                  "createdAt": "2025-02-20T08:00:00.000Z",
                  "updatedAt": "2025-02-20T08:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateAlert",
        "summary": "Change an alert's threshold, comparison, recipient or enabled state.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\n`type` is immutable, mirroring the dashboard. At least one field is required. Setting `enabled` to false mutes the alert without deleting it — prefer this over deletion.\n\n- Viewers receive 403.",
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "alertId",
            "in": "path",
            "required": true,
            "description": "Identifier of the alert.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Change an alert's threshold, comparison, recipient or enabled state.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                },
                "example": {
                  "alertId": "6512dd04bcf86cd7994390d4",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "type": "traffic_drop",
                  "threshold": 30,
                  "comparison": "previous_week",
                  "enabled": false,
                  "email": "ops@example.com",
                  "updatedAt": "2025-03-21T09:35:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "threshold": {
                    "type": "number",
                    "minimum": 1,
                    "description": "Minimum 1."
                  },
                  "comparison": {
                    "type": "string",
                    "enum": [
                      "previous_day",
                      "previous_week",
                      "absolute"
                    ],
                    "description": "New baseline."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "New recipient."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "false mutes the alert."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "enabled": false
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteAlert",
        "summary": "Permanently delete an alert. Requires the admin scope and a confirmation.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<alertId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nPass `confirm=<alertId>` as a query parameter or as a `confirm` field in the JSON body. A missing or mismatched value returns 400 with an explicit message. Consider `enabled: false` via PATCH instead — it is reversible.\n\n- Viewers receive 403.",
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "alertId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "alertId",
            "in": "path",
            "required": true,
            "description": "Identifier of the alert.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `alertId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently delete an alert. Requires the admin scope and a confirmation.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "alertId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "alertId"
                  ]
                },
                "example": {
                  "deleted": true,
                  "alertId": "6512dd04bcf86cd7994390d4"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional — send `{ \"confirm\": \"<alertId>\" }` instead of the query parameter if you prefer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must equal the alertId in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "Webhooks the key's owner registered for the site.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nSigning secrets are MASKED as `****<last 4>` so a leaked listing cannot be used to forge signatures.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "enabled",
            "in": "query",
            "required": false,
            "description": "Filter to only enabled (`true`) or only disabled (`false`) records. Omit for all.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhooks the key's owner registered for the site.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "webhooks": [
                    {
                      "webhookId": "6512ee05bcf86cd7994390e5",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "url": "https://hooks.example.com/analytics",
                      "events": [
                        "goal_completed",
                        "alert_triggered"
                      ],
                      "enabled": true,
                      "secret": "****9f2c",
                      "lastTriggeredAt": null,
                      "createdAt": "2025-02-25T08:00:00.000Z",
                      "updatedAt": "2025-02-25T08:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "summary": "Register a delivery endpoint. The signing secret is returned in full exactly once.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nA 32-byte hex signing secret is generated server-side and returned IN FULL only in this response, alongside `secretRevealed: true`. Store it immediately — every later read returns `****<last 4>` and the secret cannot be re-issued.\n\n- Viewers receive 403.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Register a delivery endpoint. The signing secret is returned in full exactly once.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                },
                "example": {
                  "webhookId": "6512ee05bcf86cd7994390e5",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "url": "https://hooks.example.com/analytics",
                  "events": [
                    "goal_completed",
                    "alert_triggered"
                  ],
                  "enabled": true,
                  "secret": "b7c1e0a4d29f4f5c8a3b1d6e0f2a7c519f2c",
                  "secretRevealed": true,
                  "lastTriggeredAt": null,
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Destination URL. Must be a valid absolute URL."
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "pageview",
                        "goal_completed",
                        "alert_triggered"
                      ]
                    },
                    "description": "Events to subscribe to. At least one."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Start enabled or paused."
                  }
                },
                "required": [
                  "url",
                  "events"
                ],
                "additionalProperties": false
              },
              "example": {
                "url": "https://hooks.example.com/analytics",
                "events": [
                  "goal_completed",
                  "alert_triggered"
                ]
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/webhooks/{webhookId}": {
      "get": {
        "operationId": "getWebhook",
        "summary": "One webhook owned by the key holder.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nThe signing secret is masked as `****<last 4>`.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "description": "Identifier of the webhook.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One webhook owned by the key holder.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                },
                "example": {
                  "webhookId": "6512ee05bcf86cd7994390e5",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "url": "https://hooks.example.com/analytics",
                  "events": [
                    "goal_completed"
                  ],
                  "enabled": true,
                  "secret": "****9f2c",
                  "lastTriggeredAt": "2025-03-19T14:02:00.000Z",
                  "createdAt": "2025-02-25T08:00:00.000Z",
                  "updatedAt": "2025-02-25T08:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "summary": "Change a webhook's url, subscribed events or enabled state.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nAt least one field is required. `events` replaces the whole list. The signing secret is immutable and is never echoed back unmasked.\n\n- Viewers receive 403.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "description": "Identifier of the webhook.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Change a webhook's url, subscribed events or enabled state.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                },
                "example": {
                  "webhookId": "6512ee05bcf86cd7994390e5",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "url": "https://hooks.example.com/analytics",
                  "events": [
                    "goal_completed"
                  ],
                  "enabled": true,
                  "secret": "****9f2c",
                  "updatedAt": "2025-03-21T09:40:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "New destination URL."
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "pageview",
                        "goal_completed",
                        "alert_triggered"
                      ]
                    },
                    "description": "Full replacement subscription list, at least one item."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "false pauses deliveries."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "events": [
                  "goal_completed"
                ]
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Permanently remove a webhook. Deliveries stop immediately.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<webhookId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nRequires `confirm=<webhookId>` as a query parameter or a `confirm` body field. The signing secret is lost with the record. Consider `enabled: false` via PATCH instead — it is reversible.\n\n- Viewers receive 403.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "webhookId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "description": "Identifier of the webhook.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `webhookId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently remove a webhook. Deliveries stop immediately.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "webhookId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "webhookId"
                  ]
                },
                "example": {
                  "deleted": true,
                  "webhookId": "6512ee05bcf86cd7994390e5"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional — send `{ \"confirm\": \"<webhookId>\" }` instead of the query parameter if you prefer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must equal the webhookId in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/annotations": {
      "get": {
        "operationId": "listAnnotations",
        "summary": "Timeline annotations for the site, newest first.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nEveryone with site access sees all annotations, each carrying its author’s `userId`. With no date parameters you get the full history.\n\n- 400 is returned for an unparseable `from`/`to` or an unknown `category`.",
        "tags": [
          "Annotations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inclusive window start. Alias: `startDate`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Inclusive window end. Alias: `endDate`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Filter by category.",
            "schema": {
              "type": "string",
              "enum": [
                "deployment",
                "campaign",
                "incident",
                "milestone",
                "other"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timeline annotations for the site, newest first.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "annotations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Annotation"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "annotations": [
                    {
                      "annotationId": "6512ff06bcf86cd7994390f6",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "userId": "6507aaa7bcf86cd799439000",
                      "date": "2025-03-18T00:00:00.000Z",
                      "text": "Shipped the new pricing page",
                      "color": "#3B82F6",
                      "category": "deployment",
                      "createdAt": "2025-03-18T09:00:00.000Z",
                      "updatedAt": "2025-03-18T09:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAnnotation",
        "summary": "Mark a point on the timeline, authored by the key holder.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nDefaults: `color` \"#3B82F6\" and `category` \"other\", matching the dashboard.\n\n- Viewers receive 403.",
        "tags": [
          "Annotations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Mark a point on the timeline, authored by the key holder.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Annotation"
                },
                "example": {
                  "annotationId": "6512ff06bcf86cd7994390f6",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "userId": "6507aaa7bcf86cd799439000",
                  "date": "2025-03-18T00:00:00.000Z",
                  "text": "Shipped the new pricing page",
                  "color": "#3B82F6",
                  "category": "deployment",
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "date": {
                    "type": "string",
                    "description": "Any parseable date; the point on the timeline."
                  },
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500,
                    "description": "Note body, 1–500 characters after trimming."
                  },
                  "color": {
                    "type": "string",
                    "description": "Display colour. Defaults to \"#3B82F6\"."
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "deployment",
                      "campaign",
                      "incident",
                      "milestone",
                      "other"
                    ],
                    "description": "Defaults to \"other\"."
                  }
                },
                "required": [
                  "date",
                  "text"
                ],
                "additionalProperties": false
              },
              "example": {
                "date": "2025-03-18",
                "text": "Shipped the new pricing page",
                "category": "deployment"
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/annotations/{annotationId}": {
      "get": {
        "operationId": "getAnnotation",
        "summary": "One annotation, readable by anyone with site access.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nSame site-level visibility rule as the list endpoint.",
        "tags": [
          "Annotations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "annotationId",
            "in": "path",
            "required": true,
            "description": "Identifier of the annotation.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One annotation, readable by anyone with site access.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Annotation"
                },
                "example": {
                  "annotationId": "6512ff06bcf86cd7994390f6",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "userId": "6507aaa7bcf86cd799439000",
                  "date": "2025-03-18T00:00:00.000Z",
                  "text": "Shipped the new pricing page",
                  "color": "#3B82F6",
                  "category": "deployment",
                  "createdAt": "2025-03-18T09:00:00.000Z",
                  "updatedAt": "2025-03-18T09:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateAnnotation",
        "summary": "Edit an annotation's date, text, colour or category.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nAUTHOR-ONLY: another user’s annotation returns 404 even for the site owner, mirroring the dashboard. At least one field is required.\n\n- Viewers receive 403.",
        "tags": [
          "Annotations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "annotationId",
            "in": "path",
            "required": true,
            "description": "Identifier of the annotation.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Edit an annotation's date, text, colour or category.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Annotation"
                },
                "example": {
                  "annotationId": "6512ff06bcf86cd7994390f6",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "userId": "6507aaa7bcf86cd799439000",
                  "date": "2025-03-18T00:00:00.000Z",
                  "text": "Shipped the new pricing page (EU rollout)",
                  "color": "#3B82F6",
                  "category": "deployment",
                  "updatedAt": "2025-03-21T09:45:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "date": {
                    "type": "string",
                    "description": "New timeline position."
                  },
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500,
                    "description": "New note body, 1–500 characters."
                  },
                  "color": {
                    "type": "string",
                    "description": "New display colour."
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "deployment",
                      "campaign",
                      "incident",
                      "milestone",
                      "other"
                    ],
                    "description": "New category."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "text": "Shipped the new pricing page (EU rollout)"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteAnnotation",
        "summary": "Permanently delete an annotation you authored.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<annotationId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nRequires `confirm=<annotationId>` as a query parameter or a `confirm` body field. Another user’s annotation returns 404.\n\n- Viewers receive 403.",
        "tags": [
          "Annotations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "annotationId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "annotationId",
            "in": "path",
            "required": true,
            "description": "Identifier of the annotation.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `annotationId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently delete an annotation you authored.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "annotationId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "annotationId"
                  ]
                },
                "example": {
                  "deleted": true,
                  "annotationId": "6512ff06bcf86cd7994390f6"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional — send `{ \"confirm\": \"<annotationId>\" }` instead of the query parameter if you prefer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must equal the annotationId in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/reports": {
      "get": {
        "operationId": "listReports",
        "summary": "Scheduled report subscriptions the key's owner has for the site.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nSorted by creation date descending.",
        "tags": [
          "Scheduled reports"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "enabled",
            "in": "query",
            "required": false,
            "description": "Filter to only enabled (`true`) or only disabled (`false`) records. Omit for all.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return. Default 50, minimum 1, maximum 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, for paging. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Scheduled report subscriptions the key's owner has for the site.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reports": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReportConfig"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows before pagination."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "reports": [
                    {
                      "reportId": "65120007bcf86cd799439007",
                      "siteId": "6507f1f77bcf86cd799439011",
                      "frequency": "weekly",
                      "enabled": true,
                      "email": "team@example.com",
                      "lastSentAt": "2025-03-17T06:00:00.000Z",
                      "createdAt": "2025-02-01T08:00:00.000Z",
                      "updatedAt": "2025-02-01T08:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createReport",
        "summary": "Schedule a recurring analytics summary email.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\n`email` defaults to the key owner’s account email when omitted.\n\n- Viewers receive 403.",
        "tags": [
          "Scheduled reports"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Schedule a recurring analytics summary email.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportConfig"
                },
                "example": {
                  "reportId": "65120007bcf86cd799439007",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "frequency": "weekly",
                  "enabled": true,
                  "email": "team@example.com",
                  "lastSentAt": null,
                  "createdAt": "2025-03-21T09:00:00.000Z",
                  "updatedAt": "2025-03-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "frequency": {
                    "type": "string",
                    "enum": [
                      "daily",
                      "weekly",
                      "monthly"
                    ],
                    "description": "How often the summary is sent."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Recipient. Defaults to the key owner's account email."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Start active or paused."
                  }
                },
                "required": [
                  "frequency"
                ],
                "additionalProperties": false
              },
              "example": {
                "frequency": "weekly",
                "email": "team@example.com"
              }
            }
          }
        }
      }
    },
    "/api/v2/sites/{siteId}/reports/{reportId}": {
      "get": {
        "operationId": "getReport",
        "summary": "One report configuration owned by the key's owner.",
        "description": "**Required scope: `read`.** (Scopes are hierarchical: admin > write > read.)\n\nReturns 404 \"Report config not found\" otherwise.",
        "tags": [
          "Scheduled reports"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reportId",
            "in": "path",
            "required": true,
            "description": "Identifier of the report config.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One report configuration owned by the key's owner.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportConfig"
                },
                "example": {
                  "reportId": "65120007bcf86cd799439007",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "frequency": "weekly",
                  "enabled": true,
                  "email": "team@example.com",
                  "lastSentAt": "2025-03-17T06:00:00.000Z",
                  "createdAt": "2025-02-01T08:00:00.000Z",
                  "updatedAt": "2025-02-01T08:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateReport",
        "summary": "Change frequency, recipient, or pause/resume the schedule.",
        "description": "**Required scope: `write`.** (Scopes are hierarchical: admin > write > read.)\n\nAt least one field is required. Setting `enabled` to false pauses the schedule without deleting it.\n\n- Viewers receive 403.",
        "tags": [
          "Scheduled reports"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "write",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reportId",
            "in": "path",
            "required": true,
            "description": "Identifier of the report config.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Change frequency, recipient, or pause/resume the schedule.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportConfig"
                },
                "example": {
                  "reportId": "65120007bcf86cd799439007",
                  "siteId": "6507f1f77bcf86cd799439011",
                  "frequency": "monthly",
                  "enabled": true,
                  "email": "team@example.com",
                  "updatedAt": "2025-03-21T09:50:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "frequency": {
                    "type": "string",
                    "enum": [
                      "daily",
                      "weekly",
                      "monthly"
                    ],
                    "description": "New cadence."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "New recipient."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "false pauses the schedule."
                  }
                },
                "minProperties": 1,
                "additionalProperties": false
              },
              "example": {
                "frequency": "monthly"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteReport",
        "summary": "Permanently delete a scheduled report; no further emails are sent.",
        "description": "**Required scope: `admin`.** (Scopes are hierarchical: admin > write > read.)\n\n**DESTRUCTIVE AND IRREVERSIBLE.** In addition to the `admin` scope this call requires an explicit confirmation: pass `confirm=<reportId>` either as a query parameter or as a `confirm` field in the JSON body, repeating the exact identifier used in the URL. Without it the call fails with 400. The confirmation is checked after the scope and access checks but before the identifier is validated, so a wrong `confirm` on a non-existent resource returns 400 rather than 404.\n\nRequires `confirm=<reportId>` as a query parameter or a `confirm` body field. Consider `enabled: false` via PATCH instead — it is reversible.\n\n- Viewers receive 403.",
        "tags": [
          "Scheduled reports"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scope": "admin",
        "x-destructive": true,
        "x-confirm-parameter": "reportId",
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reportId",
            "in": "path",
            "required": true,
            "description": "Identifier of the report config.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Destructive-operation confirmation. Must exactly equal the `reportId` in the URL. May be sent as a `confirm` field in the JSON body instead.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently delete a scheduled report; no further emails are sent.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "reportId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "reportId"
                  ]
                },
                "example": {
                  "deleted": true,
                  "reportId": "65120007bcf86cd799439007"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — validation failed, an analytics window is unusable (unparseable or inverted `from`/`to`), or a destructive call is missing/mismatching its `confirm` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the key lacks the required scope, is restricted to other sites, your role on the site is 'viewer', or the field is owner-only (`isPublic`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the resource does not exist, or the key has no access to it (access failures are reported as 404 on purpose).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — either the 100 req/min budget for the API-key owner, or the tighter per-IP budget applied to failed authentications (20/min, 200/min for the shared bucket used when no usable client-IP header reaches the app). See the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Never negative.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp in MILLISECONDS at which the window resets.",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional — send `{ \"confirm\": \"<reportId>\" }` instead of the query parameter if you prefer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must equal the reportId in the URL."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key issued from Dashboard → Settings → API Keys, sent as `Authorization: Bearer <api key>`. The key carries its own scopes, optional site restriction and optional expiry."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every non-2xx response uses this shape.",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "SiteSummary": {
        "type": "object",
        "description": "Compact site representation returned by the list endpoint.",
        "properties": {
          "siteId": {
            "type": "string",
            "description": "Mongo _id. Use this (or trackingId) as the {siteId} path parameter."
          },
          "trackingId": {
            "type": "string",
            "description": "Public 10-character id embedded in the tracking snippet."
          },
          "name": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          },
          "isPublic": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Site": {
        "type": "object",
        "description": "Full site representation returned by create / detail / update.",
        "properties": {
          "siteId": {
            "type": "string"
          },
          "trackingId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          },
          "isPublic": {
            "type": "boolean"
          },
          "publicSlug": {
            "type": [
              "string",
              "null"
            ]
          },
          "publicUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Relative share URL while isPublic is true, otherwise null."
          },
          "dataRetentionDays": {
            "type": "integer"
          },
          "maxRetentionDays": {
            "type": "integer",
            "description": "Cap imposed by the site OWNER's plan."
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "growth",
              "business"
            ]
          },
          "botDetectionEnabled": {
            "type": "boolean"
          },
          "botLogEnabled": {
            "type": "boolean"
          },
          "recordingEnabled": {
            "type": "boolean"
          },
          "recordingSamplingRate": {
            "type": "number"
          },
          "recordingMaxDuration": {
            "type": "integer"
          },
          "recordingPrivacyLevel": {
            "type": "string",
            "enum": [
              "standard",
              "strict"
            ]
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "viewer"
            ],
            "description": "The API key owner's role on this site."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "tracking": {
            "type": "object",
            "properties": {
              "scriptUrl": {
                "type": "string",
                "format": "uri"
              },
              "snippet": {
                "type": "string",
                "description": "Ready-to-paste HTML snippet."
              }
            }
          }
        }
      },
      "Goal": {
        "type": "object",
        "properties": {
          "goalId": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "pageview",
              "event"
            ]
          },
          "target": {
            "type": "string",
            "description": "Pathname (type=pageview) or custom event name (type=event)."
          },
          "conversions": {
            "type": "integer",
            "description": "Unique converting sessions in the window. Omitted when includeConversions=false."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FunnelStep": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "pathname": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "pathname"
        ]
      },
      "Funnel": {
        "type": "object",
        "properties": {
          "funnelId": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "steps": {
            "type": "array",
            "minItems": 2,
            "items": {
              "$ref": "#/components/schemas/FunnelStep"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SegmentFilters": {
        "type": "object",
        "description": "All keys optional; unknown keys are stripped. Supplied keys are ANDed.",
        "properties": {
          "dateRange": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "browser": {
            "type": "string"
          },
          "os": {
            "type": "string"
          },
          "device": {
            "type": "string"
          },
          "referrer": {
            "type": "string"
          },
          "utm_source": {
            "type": "string"
          },
          "utm_medium": {
            "type": "string"
          },
          "utm_campaign": {
            "type": "string"
          },
          "pathname": {
            "type": "string"
          }
        }
      },
      "Segment": {
        "type": "object",
        "properties": {
          "segmentId": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "filters": {
            "$ref": "#/components/schemas/SegmentFilters"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Alert": {
        "type": "object",
        "properties": {
          "alertId": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "traffic_spike",
              "traffic_drop",
              "goal_completed",
              "error_rate"
            ]
          },
          "threshold": {
            "type": "number"
          },
          "comparison": {
            "type": "string",
            "enum": [
              "previous_day",
              "previous_week",
              "absolute"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "lastTriggeredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "webhookId": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "pageview",
                "goal_completed",
                "alert_triggered"
              ]
            }
          },
          "enabled": {
            "type": "boolean"
          },
          "secret": {
            "type": "string",
            "description": "HMAC signing secret. Returned in FULL only in the 201 create response (alongside secretRevealed:true); every later read returns ****<last 4>."
          },
          "secretRevealed": {
            "type": "boolean",
            "description": "Present and true only on the create response."
          },
          "lastTriggeredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Annotation": {
        "type": "object",
        "properties": {
          "annotationId": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "userId": {
            "type": "string",
            "description": "Author. Only the author may edit or delete."
          },
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "text": {
            "type": "string",
            "maxLength": 500
          },
          "color": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "deployment",
              "campaign",
              "incident",
              "milestone",
              "other"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ReportConfig": {
        "type": "object",
        "properties": {
          "reportId": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "frequency": {
            "type": "string",
            "enum": [
              "daily",
              "weekly",
              "monthly"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "lastSentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VitalSummaryEntry": {
        "type": "object",
        "properties": {
          "p50": {
            "type": [
              "number",
              "null"
            ]
          },
          "p75": {
            "type": [
              "number",
              "null"
            ]
          },
          "p99": {
            "type": [
              "number",
              "null"
            ]
          },
          "good": {
            "type": "number",
            "description": "Percentage of samples in the \"good\" bucket (core metrics only)."
          },
          "needsImprovement": {
            "type": "number"
          },
          "poor": {
            "type": "number"
          }
        }
      }
    }
  },
  "x-rate-limit": {
    "requestsPerMinute": 100,
    "window": "fixed 60s",
    "scope": "api-key owner (all keys and both REST and MCP traffic share one budget)",
    "storage": "mongodb",
    "distributed": true,
    "degradedFallback": "in-process counter, still enforced but per replica, when the database is unreachable",
    "failedAuthPerMinute": 20,
    "failedAuthUnknownIpPerMinute": 200,
    "failedAuthChargedWhen": "only after a presented credential has been looked up and found bad; a credential-less request is rejected up front once the bucket is empty",
    "failedAuthClientIpSource": "x-forwarded-for, counting TRUSTED_PROXY_HOPS entries from the end (default 1) and clamping to the leftmost entry when the chain is shorter; x-real-ip is consulted ONLY when TRUST_X_REAL_IP is enabled, which it is not by default. Non-IP values, and requests carrying no usable header at all, fall into the shared \"unknown\" bucket",
    "headers": [
      "X-RateLimit-Limit",
      "X-RateLimit-Remaining",
      "X-RateLimit-Reset",
      "Retry-After"
    ],
    "headersEmittedOn": "successful responses and 429 only; other error responses carry no rate headers",
    "resetUnit": "epoch-milliseconds"
  },
  "x-mcp": {
    "endpoint": "https://analytics.appfor.you/api/mcp",
    "transport": "streamable-http",
    "protocol": "json-rpc-2.0",
    "stateless": true,
    "authentication": "Authorization: Bearer <api key>",
    "toolsListCursorlessReturnsAll": true,
    "toolsListPageSize": 20,
    "toolsListPaginated": "only when the client supplies params.cursor",
    "maxBatchSize": 20,
    "batchConcurrency": 4,
    "batchRateLimit": "the first element rides on the unit the transport already charged; every further element charges one more, on top of whatever its own method costs",
    "oversizedBatchRejection": "rejected with JSON-RPC -32600 after transport authentication, so it still costs one API-key lookup and one unit of budget",
    "maxToolResultBytes": 24000,
    "maxToolErrorBytes": 2000,
    "truncationMarker": "--- MCP_RESULT_TRUNCATED ---",
    "documentation": "https://analytics.appfor.you/docs#mcp"
  },
  "x-deployment-env": [
    {
      "name": "MCP_INTERNAL_ORIGIN",
      "required": false,
      "default": "http://127.0.0.1:$PORT (PORT defaults to 3000)",
      "description": "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."
    },
    {
      "name": "TRUSTED_PROXY_HOPS",
      "required": false,
      "default": "1 — correct for a single reverse proxy (Traefik/nginx) directly in front of the app",
      "description": "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)."
    },
    {
      "name": "TRUST_X_REAL_IP",
      "required": false,
      "default": "false — x-real-ip is ignored and the request falls into the shared unknown bucket",
      "description": "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."
    },
    {
      "name": "GEO_EDGE_PROVIDER",
      "required": false,
      "default": "netlify — correct for this repo’s Netlify deployment",
      "description": "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."
    },
    {
      "name": "GEO_TRUST_PROXY",
      "required": false,
      "default": "false — x-forwarded-for is ignored by geo resolution",
      "description": "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."
    },
    {
      "name": "MAXMIND_LICENSE_KEY",
      "required": false,
      "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)",
      "description": "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()."
    },
    {
      "name": "SITE_CLAIM_ALLOW_LOOPBACK_PROOF",
      "required": false,
      "default": "false — the ownership-proof fetcher refuses loopback destinations, in every environment including development",
      "description": "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."
    },
    {
      "name": "SITE_CLAIM_REQUIRE_PROOF_ALWAYS",
      "required": false,
      "default": "false — production semantics still apply automatically when NODE_ENV=production",
      "description": "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."
    },
    {
      "name": "SITE_CLAIM_IP_PEPPER",
      "required": false,
      "default": "NEXTAUTH_SECRET, then a fixed development string if that is unset too",
      "description": "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."
    },
    {
      "name": "MONGODB_AUTO_INDEX",
      "required": false,
      "default": "off when NODE_ENV=production, on otherwise — Mongoose’s own default (always on) is deliberately not used",
      "description": "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."
    }
  ],
  "x-api-base-url": "https://analytics.appfor.you/api/v2"
}