{
 "openapi": "3.1.0",
 "info": {
  "title": "Clicktag API",
  "version": "2026-10-04",
  "description": "Current unversioned API; the document version is a reference revision, not a URL prefix. Product management uses Firebase ID bearer tokens or management keys (ManagementKey, tt_mk_...) limited by scopes. Verified public site metadata, web overview, breakdown and retention can be read anonymously; private reads require the owner and otherwise return 404. A management key sent to these reads is still checked first: an unknown or revoked key returns 401 and a key without read returns 403. Other reports remain owner-only. Collector routes require no token. Product API responses have no cross-origin CORS headers, so use a server-side or same-origin client. Collector CORS is separate. Every path answers OPTIONS with the empty 204 Preflight response, also where no OPTIONS operation is listed. Requests that match no product API route return 404 {\"error\": \"not found\"}; routes that need sign-in can answer 401 first. Successful collection is receipt, not durable storage confirmation. Write examples use placeholder hostnames; demo.bitgate.dev is for read-only examples. Site lookups use a per-instance cache for up to 30 seconds for verified sites or 15 seconds for pending/missing sites, plus a shared cache for up to 120 seconds or 15 seconds respectively. Mutations invalidate the handling instance and local data-center shared entries; other data centers may retain their entries until expiry. Historical-import routes require the site owner even for public sites; source discovery requires sign-in. Import creation is asynchronous; verify progress and actual reports before considering a migration complete. Site API keys (SiteApiKey) only authenticate POST /api/ingest for API analytics; API analytics stats and key management use the owner's Firebase ID token or a management key."
 },
 "servers": [
  {
   "url": "https://clicktag.io"
  }
 ],
 "externalDocs": {
  "description": "Developer documentation",
  "url": "https://clicktag.io/docs"
 },
 "tags": [
  {
   "name": "Sites",
   "description": "Site registration, ownership and visibility."
  },
  {
   "name": "Stats",
   "description": "Overview, dimension breakdowns and account summaries."
  },
  {
   "name": "Goals",
   "description": "Owner-only goals and funnels over identified visitor-days, computed at query time from existing data."
  },
  {
   "name": "Imports",
   "description": "Owner-only historical import jobs. Source discovery requires sign-in or a management key with read; management keys can read jobs but not start, cancel or retry them. Asynchronous jobs, not an exactly-once data migration guarantee."
  },
  {
   "name": "Collector",
   "description": "Browser tracking and ingestion. No authentication."
  },
  {
   "name": "Health",
   "description": "HTTP liveness and global preflight behavior."
  },
  {
   "name": "Reports",
   "description": "Scheduled email digests per recipient."
  },
  {
   "name": "Alerts",
   "description": "API alert rules, checked every minute and emailed when they fire and resolve."
  },
  {
   "name": "API analytics",
   "description": "Request metrics from your own API: ingest with a site API key or a management key, key management and owner-only request stats."
  },
  {
   "name": "Management keys",
   "description": "Owner-level keys for scripts and CI: one credential for every site, limited by scopes."
  },
  {
   "name": "AI crawlers",
   "description": "AI and search engine crawler hits on your site, read from your Cloudflare zone's analytics."
  },
  {
   "name": "Live",
   "description": "Hits as they arrive, for the signed-in owner: stream tickets, the WebSocket stream and the last 15 minutes of history. Firebase ID tokens only."
  },
  {
   "name": "Search Console",
   "description": "Owner-only Google Search Console connection, property link, sync and search performance data."
  }
 ],
 "security": [],
 "paths": {
  "/api/sites": {
   "get": {
    "tags": [
     "Sites"
    ],
    "operationId": "listSites",
    "summary": "List your sites",
    "description": "Returns all caller-owned sites, pinned sites first (most recently pinned first), then by creation time. Requires a Firebase ID token or a management key; no pagination.",
    "responses": {
     "200": {
      "description": "Owned site list.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SitesResponse"
        },
        "example": {
         "sites": []
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "post": {
    "tags": [
     "Sites"
    ],
    "operationId": "createSite",
    "summary": "Register a hostname",
    "description": "Registers a globally unique hostname to the caller's account. New sites are private with an empty display name. Maximum 50 sites per account. No hostname verification workflow is performed by this route. Set kind to api for an API-only site, which can create API keys before ownership verification.",
    "responses": {
     "200": {
      "description": "Hostname already registered to this account; returns the stored registration unchanged.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CreatedSite"
        }
       }
      }
     },
     "201": {
      "description": "Site registered.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CreatedSite"
        },
        "example": {
         "hostname": "your-domain.example",
         "verified_at": null,
         "verify_token": "tt-verify-0123456789abcdef0123456789abcdef",
         "kind": "web"
        }
       }
      }
     },
     "400": {
      "description": "Hostname validation, kind validation or account site limit failed.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "hostname": {
          "value": {
           "error": "Enter a valid website address, such as example.com"
          }
         },
         "limit": {
          "value": {
           "error": "site limit reached"
          }
         },
         "kind": {
          "value": {
           "error": "kind must be web or api"
          }
         },
         "apiHostname": {
          "value": {
           "error": "Enter a valid API hostname, such as api.example.com"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "409": {
      "$ref": "#/components/responses/SiteExists"
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateSite"
       },
       "example": {
        "hostname": "your-domain.example"
       }
      }
     }
    }
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/summary": {
   "get": {
    "tags": [
     "Stats"
    ],
    "operationId": "getSitesSummary",
    "summary": "Summarize your sites",
    "description": "Authenticated account-wide report, even for public sites. Sparse daily rollups from a rolling cutoff 31 days ago plus independent five-minute live counts. Includes verified sites and every API site (kind api), verified or not, pinned sites first (most recently pinned first), then in creation order; unverified websites are left out. Inactive sites have days: [] and live: 0. API sites also carry api_days with daily requests and 5xx errors, counted from the site's registration; days and live only count website traffic, so they stay empty for API sites that are not verified. The first day can be partial. Only tz is read; from, to and limit are ignored.",
    "responses": {
     "200": {
      "description": "Owned-site summary.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SummaryResponse"
        },
        "example": {
         "sites": [
          {
           "hostname": "your-domain.example",
           "pinned_at": null,
           "live": 0,
           "days": [
            {
             "day": "2026-09-16",
             "pageviews": 3,
             "visitors": 2
            }
           ]
          },
          {
           "hostname": "api.your-domain.example",
           "pinned_at": null,
           "live": 0,
           "days": [],
           "api_days": [
            {
             "day": "2026-09-16",
             "requests": 18240,
             "server_errors": 12
            }
           ]
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/Timezone"
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_summary",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Sites"
    ],
    "operationId": "getSite",
    "summary": "Read site metadata",
    "description": "Verified public sites allow anonymous metadata reads. Owners can also read their unverified sites and receive verified_at, verify_token, reports_mode and pinned_at. Unknown sites and inaccessible private or unverified sites return 404 unknown site, including absent or invalid owner tokens. Owner metadata reads refresh a missing or differently owned cached registration before denying access. A management key is checked before the site lookup: an unknown or revoked key returns 401, a key without read returns 403.",
    "responses": {
     "200": {
      "description": "Site metadata, without owner_uid.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Site"
        },
        "example": {
         "hostname": "demo.bitgate.dev",
         "display_name": "Demo site",
         "is_public": true,
         "created_at": "2026-09-17T17:28:42.089Z",
         "kind": "web"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/ManagementKeyRejected"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {},
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "name": "hostname",
      "in": "path",
      "required": true,
      "description": "Use the normalized hostname returned at registration. Lookups do not lowercase path values.",
      "schema": {
       "type": "string"
      },
      "example": "demo.bitgate.dev"
     }
    ]
   },
   "patch": {
    "tags": [
     "Sites"
    ],
    "operationId": "updateSite",
    "summary": "Update name, visibility and pin",
    "description": "Owner only, even if the site is public. Changes display_name, is_public, reports_mode and the owner's pin; no rename or ownership transfer. Enabling public access requires verification. Successful mutations invalidate the handling instance and local data-center shared site cache. Other instances can retain their local entry for up to 30 seconds; shared entries in other data centers can remain for up to 120 seconds.",
    "responses": {
     "200": {
      "description": "Current name, visibility, reports mode and pin.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/UpdatedSite"
        },
        "example": {
         "hostname": "your-domain.example",
         "display_name": "My website",
         "is_public": false,
         "reports_mode": "inherit",
         "pinned_at": null
        }
       }
      }
     },
     "400": {
      "description": "Invalid JSON, settings object, field type, reports mode, pin or public-sharing request.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "settings": {
          "value": {
           "error": "invalid settings"
          }
         },
         "displayName": {
          "value": {
           "error": "invalid display name"
          }
         },
         "sharing": {
          "value": {
           "error": "invalid sharing setting"
          }
         },
         "verification": {
          "value": {
           "error": "verify domain ownership before making the site public"
          }
         },
         "reportsMode": {
          "value": {
           "error": "invalid reports mode"
          }
         },
         "pin": {
          "value": {
           "error": "invalid pin setting"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/UpdateSite"
       },
       "example": {
        "display_name": "My website",
        "is_public": false
       }
      }
     }
    }
   },
   "delete": {
    "tags": [
     "Sites"
    ],
    "operationId": "deleteSite",
    "summary": "Remove a site registration",
    "description": "Owner only. Deletes the registry entry, not historical analytics. Once cached registrations expire, reports return 404 and new collection is normally discarded. Does not provide data erasure. A later deletion of a missing registration returns 404. Its import jobs, email report subscriptions, goals, API alert rules, site API keys, Search Console link, Cloudflare crawler link and stored icon are deleted with it; previously imported analytics remain. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Registration removed.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/DeletedSite"
        },
        "example": {
         "deleted": "your-domain.example"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyNotAllowed"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/overview": {
   "parameters": [
    {
     "name": "hostname",
     "in": "path",
     "required": true,
     "description": "Use the normalized hostname returned at registration. Lookups do not lowercase path values.",
     "schema": {
      "type": "string"
     },
     "example": "demo.bitgate.dev"
    }
   ],
   "get": {
    "tags": [
     "Stats"
    ],
    "operationId": "getOverview",
    "summary": "Read traffic overview",
    "description": "Public verified site or owner; access is checked before query validation. Returns canonical human pageview totals, distinct daily visitors, parent-page engagement, the previous period (the same clock times, moved back by as many calendar days as the range covers in tz, bounds in previous_range), zero-filled timezone-aware series and independent five-minute live activity. Exact range boundaries include partial hours. avg_duration_s is the median of summed page durations at least five seconds; avg_scroll is the mean of per-page maximum measured scroll. Missing engagement is null. Inaccessible sites return 404 even with an absent or invalid token. An unverified owner receives 403. Successful responses to anyone other than the owner may be edge-cached for 60 seconds; the owner's reads bypass this response cache. A management key is checked before the site lookup: an unknown or revoked key returns 401, a key without read returns 403.",
    "responses": {
     "200": {
      "description": "Traffic overview.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/PublicStatsCache"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/OverviewResponse"
        },
        "example": {
         "totals": {
          "pageviews": 3,
          "visitors": 3,
          "avg_duration_s": 42,
          "avg_scroll": 75
         },
         "previous": {
          "pageviews": 2,
          "visitors": 2,
          "avg_duration_s": 30,
          "avg_scroll": 50
         },
         "series": [
          {
           "t": 1789516800,
           "pageviews": 1,
           "visitors": 1,
           "duration_s": 36,
           "scrolled": 80
          },
          {
           "t": 1789520400,
           "pageviews": 2,
           "visitors": 2,
           "duration_s": 48,
           "scrolled": 70
          }
         ],
         "forecast": {
          "bucket": 1789520400,
          "pageviews": 4,
          "visitors": 4,
          "basis": "elapsed",
          "elapsed": 0.5
         },
         "live": 0,
         "granularity": "hour",
         "previous_range": {
          "from": 1789430400,
          "to": 1789435800
         }
        }
       }
      }
     },
     "400": {
      "description": "The rounded range is non-finite, has from >= to, or exceeds 400 days, or a filter is malformed.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "range": {
          "value": {
           "error": "invalid range"
          }
         },
         "filter": {
          "value": {
           "error": "invalid filter"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/ManagementKeyRejected"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {},
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone"
     },
     {
      "$ref": "#/components/parameters/Filter"
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_overview",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/overview/summary": {
   "parameters": [
    {
     "name": "hostname",
     "in": "path",
     "required": true,
     "description": "Use the normalized hostname returned at registration. Lookups do not lowercase path values.",
     "schema": {
      "type": "string"
     },
     "example": "demo.bitgate.dev"
    }
   ],
   "get": {
    "tags": [
     "Stats"
    ],
    "operationId": "getOverviewSummary",
    "summary": "Read the overview summary",
    "description": "Public verified site or owner; access is checked before query validation. Returns the top devices, browsers and operating systems (pageviews and visitors, names normalized like the breakdowns, blanks omitted), custom event totals for the range and the previous period (the overview's previous_range) with the top event names, and the top pages by distinct visitors over the last 30 minutes, independent of the selected range. Filters apply to every list. tz sets the calendar days of the previous period. Inaccessible sites return 404 even with an absent or invalid token. An unverified owner receives 403. Successful responses to anyone other than the owner may be edge-cached for 60 seconds; the owner's reads bypass this response cache. A management key is checked before the site lookup: an unknown or revoked key returns 401, a key without read returns 403.",
    "responses": {
     "200": {
      "description": "Overview summary.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/PublicStatsCache"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/OverviewSummaryResponse"
        },
        "example": {
         "events": {
          "totals": {
           "count": 1204,
           "visitors": 310
          },
          "previous": {
           "count": 980,
           "visitors": 251
          },
          "top": [
           {
            "name": "signup",
            "value": 42,
            "visitors": 40
           }
          ]
         },
         "tech": {
          "devices": [
           {
            "name": "desktop",
            "value": 900,
            "visitors": 612
           }
          ],
          "browsers": [
           {
            "name": "Chrome",
            "value": 640,
            "visitors": 410
           }
          ],
          "os": [
           {
            "name": "macOS",
            "value": 380,
            "visitors": 250
           }
          ]
         },
         "live": {
          "pages": [
           {
            "name": "/pricing",
            "visitors": 3
           }
          ]
         }
        }
       }
      }
     },
     "400": {
      "description": "The rounded range is non-finite, has from >= to, or exceeds 400 days, or a filter is malformed.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "range": {
          "value": {
           "error": "invalid range"
          }
         },
         "filter": {
          "value": {
           "error": "invalid filter"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/ManagementKeyRejected"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {},
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone"
     },
     {
      "name": "limit",
      "in": "query",
      "description": "Rows per list. Default 5; numeric input is clamped to 1-20 and fractions round down. Zero or non-numeric input uses 5.",
      "schema": {
       "type": "integer",
       "default": 5,
       "minimum": 1,
       "maximum": 20
      },
      "example": 5
     },
     {
      "$ref": "#/components/parameters/Filter"
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_overview_summary",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/breakdown": {
   "parameters": [
    {
     "name": "hostname",
     "in": "path",
     "required": true,
     "description": "Use the normalized hostname returned at registration. Lookups do not lowercase path values.",
     "schema": {
      "type": "string"
     },
     "example": "demo.bitgate.dev"
    }
   ],
   "get": {
    "tags": [
     "Stats"
    ],
    "operationId": "getBreakdown",
    "summary": "Read a traffic breakdown",
    "description": "Public verified site or owner. Counts canonical non-bot pageviews (events dimension counts event rows) within exact timestamp boundaries. Visitors are distinct daily visitor hashes in every dimension; pageviews without a hash (mainly imports and older data) count their unique-entry flag instead, and events without a hash add nothing. The events dimension also returns last_seen. Sources use campaign precedence then normalized referring host; internal navigation is excluded, not relabeled Direct. Empty labels are omitted except pages/referrers. Sorted by value descending, then name; tz is ignored. No metadata filter, cursor, offset or total-row count. Inaccessible sites return 404 even with an absent or invalid token. An unverified owner receives 403. Successful responses to anyone other than the owner may be edge-cached for 60 seconds; the owner's reads bypass this response cache. A management key is checked before the site lookup: an unknown or revoked key returns 401, a key without read returns 403.",
    "responses": {
     "200": {
      "description": "Ranked groups.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/PublicStatsCache"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/BreakdownResponse"
        },
        "example": {
         "rows": [
          {
           "name": "/pricing",
           "value": 3,
           "visitors": 2
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "Missing/unknown dimension, invalid range, or malformed filter.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "dimension": {
          "value": {
           "error": "unknown dimension"
          }
         },
         "range": {
          "value": {
           "error": "invalid range"
          }
         },
         "filter": {
          "value": {
           "error": "invalid filter"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/ManagementKeyRejected"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {},
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/Dimension"
     },
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Limit"
     },
     {
      "$ref": "#/components/parameters/Filter"
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_breakdown",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/latest.js": {
   "get": {
    "tags": [
     "Collector"
    ],
    "operationId": "getTrackerScript",
    "summary": "Load the browser tracker",
    "description": "Vendored tracker with Clicktag collector destination and isolated default tt namespace: tt_event, tt_settings, tt_metadata and manual-mode tt_pageview. Default guard tt_tt_loaded. Supports data-namespace and data-event-global (data-sa-global legacy alias). SA globals and queues are never consumed. An existing event function is replaced only when it carries a q array (the preload queue); otherwise it stays and no events are sent. Does not host the SA auto-events helper. GET and HEAD only. Scripts served from clicktag.io or clicktag.app send beacons to clicktag.io. Existing Totallytics-hosted scripts keep their totallytics.com beacon destination.",
    "responses": {
     "200": {
      "description": "Browser JavaScript.",
      "headers": {
       "Access-Control-Allow-Origin": {
        "$ref": "#/components/headers/AllowOrigin"
       },
       "Access-Control-Allow-Methods": {
        "$ref": "#/components/headers/AllowMethods"
       },
       "Access-Control-Allow-Headers": {
        "$ref": "#/components/headers/AllowHeaders"
       },
       "Access-Control-Max-Age": {
        "$ref": "#/components/headers/PreflightMaxAge"
       },
       "Cache-Control": {
        "schema": {
         "type": "string",
         "const": "public, max-age=300, stale-while-revalidate=3600"
        }
       }
      },
      "content": {
       "application/javascript": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/CollectorGetRequired"
     }
    },
    "security": []
   },
   "options": {
    "tags": [
     "Collector"
    ],
    "operationId": "preflight_latest_js",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/simple.gif": {
   "get": {
    "tags": [
     "Collector"
    ],
    "operationId": "collectPixel",
    "summary": "Collect via an image beacon",
    "description": "Collection, not a read-only report. GET and HEAD only; HEAD records nothing. Reads the shared payload fields from query parameters. Metadata is a URL-encoded string. A valid payload returns a GIF; validation failures return text. No authentication. Only registered hostnames that are verified or pass automatic verification are stored. Registry lookup failures fail closed and are logged; the caller still receives the normal GIF response. DNT: 1 or X-Do-Not-Track: 1 skips before payload validation unless collect-dnt is true.",
    "responses": {
     "200": {
      "$ref": "#/components/responses/PixelOk"
     },
     "400": {
      "$ref": "#/components/responses/CollectorBadRequest"
     }
    },
    "security": [],
    "parameters": [
     {
      "name": "hostname",
      "in": "query",
      "description": "Required after scalar-to-string conversion. Normalized the way site registration does: trimmed and lowercased, with an http:// or https:// scheme, path and trailing dot dropped and internationalized names in xn-- form. A value registration would reject, such as one with a port or an IP address, can't match a site: the hit is accepted and nothing is stored. The result must match a registered hostname exactly; www and apex are different hostnames.",
      "schema": {
       "type": "string"
      },
      "example": "example.com",
      "required": true
     },
     {
      "$ref": "#/components/parameters/CollectType"
     },
     {
      "$ref": "#/components/parameters/CollectEvent"
     },
     {
      "$ref": "#/components/parameters/CollectPath"
     },
     {
      "$ref": "#/components/parameters/CollectQuery"
     },
     {
      "$ref": "#/components/parameters/CollectReferrer"
     },
     {
      "$ref": "#/components/parameters/CollectMetadata"
     },
     {
      "$ref": "#/components/parameters/CollectId"
     },
     {
      "$ref": "#/components/parameters/CollectPageId"
     },
     {
      "$ref": "#/components/parameters/CollectSessionId"
     },
     {
      "$ref": "#/components/parameters/CollectOriginalId"
     },
     {
      "$ref": "#/components/parameters/CollectDuration"
     },
     {
      "$ref": "#/components/parameters/CollectScrolled"
     },
     {
      "$ref": "#/components/parameters/CollectViewportWidth"
     },
     {
      "$ref": "#/components/parameters/CollectViewportHeight"
     },
     {
      "$ref": "#/components/parameters/CollectScreenWidth"
     },
     {
      "$ref": "#/components/parameters/CollectScreenHeight"
     },
     {
      "$ref": "#/components/parameters/CollectError"
     },
     {
      "$ref": "#/components/parameters/CollectUa"
     },
     {
      "$ref": "#/components/parameters/CollectTimezone"
     },
     {
      "$ref": "#/components/parameters/CollectLanguage"
     },
     {
      "$ref": "#/components/parameters/CollectOsName"
     },
     {
      "$ref": "#/components/parameters/CollectOsVersion"
     },
     {
      "$ref": "#/components/parameters/CollectBrands"
     },
     {
      "$ref": "#/components/parameters/CollectVersion"
     },
     {
      "$ref": "#/components/parameters/CollectHostnameOriginal"
     },
     {
      "$ref": "#/components/parameters/CollectUnique"
     },
     {
      "$ref": "#/components/parameters/CollectMobile"
     },
     {
      "$ref": "#/components/parameters/CollectBot"
     },
     {
      "$ref": "#/components/parameters/CollectBrave"
     },
     {
      "$ref": "#/components/parameters/CollectDuck"
     },
     {
      "$ref": "#/components/parameters/CollectHttps"
     },
     {
      "$ref": "#/components/parameters/CollectCollectDnt"
     },
     {
      "$ref": "#/components/parameters/CollectUtmSource"
     },
     {
      "$ref": "#/components/parameters/CollectUtmMedium"
     },
     {
      "$ref": "#/components/parameters/CollectUtmCampaign"
     },
     {
      "$ref": "#/components/parameters/CollectUtmTerm"
     },
     {
      "$ref": "#/components/parameters/CollectUtmContent"
     },
     {
      "$ref": "#/components/parameters/CollectSource"
     },
     {
      "$ref": "#/components/parameters/CollectRef"
     },
     {
      "$ref": "#/components/parameters/CollectMedium"
     },
     {
      "$ref": "#/components/parameters/CollectCampaign"
     },
     {
      "$ref": "#/components/parameters/CollectTerm"
     },
     {
      "$ref": "#/components/parameters/CollectContent"
     },
     {
      "$ref": "#/components/parameters/Dnt"
     },
     {
      "$ref": "#/components/parameters/XDoNotTrack"
     }
    ]
   },
   "options": {
    "tags": [
     "Collector"
    ],
    "operationId": "preflight_simple_gif",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/noscript.gif": {
   "get": {
    "tags": [
     "Collector"
    ],
    "operationId": "collectNoscriptPixel",
    "summary": "Collect without JavaScript",
    "description": "Same payload and response as simple.gif, GET and HEAD only; HEAD records nothing. Derives missing hostname, path, https and query from the page URL in Referer. Explicit query parameters win even when empty. type defaults pageview. Referer does not supply the acquisition referrer field; browser policies can omit it or reduce it to an origin. No authentication; DNT and background-write semantics are the same as simple.gif.",
    "responses": {
     "200": {
      "$ref": "#/components/responses/PixelOk"
     },
     "400": {
      "$ref": "#/components/responses/CollectorBadRequest"
     }
    },
    "security": [],
    "parameters": [
     {
      "$ref": "#/components/parameters/CollectHostname"
     },
     {
      "$ref": "#/components/parameters/CollectType"
     },
     {
      "$ref": "#/components/parameters/CollectEvent"
     },
     {
      "$ref": "#/components/parameters/CollectPath"
     },
     {
      "$ref": "#/components/parameters/CollectQuery"
     },
     {
      "$ref": "#/components/parameters/CollectReferrer"
     },
     {
      "$ref": "#/components/parameters/CollectMetadata"
     },
     {
      "$ref": "#/components/parameters/CollectId"
     },
     {
      "$ref": "#/components/parameters/CollectPageId"
     },
     {
      "$ref": "#/components/parameters/CollectSessionId"
     },
     {
      "$ref": "#/components/parameters/CollectOriginalId"
     },
     {
      "$ref": "#/components/parameters/CollectDuration"
     },
     {
      "$ref": "#/components/parameters/CollectScrolled"
     },
     {
      "$ref": "#/components/parameters/CollectViewportWidth"
     },
     {
      "$ref": "#/components/parameters/CollectViewportHeight"
     },
     {
      "$ref": "#/components/parameters/CollectScreenWidth"
     },
     {
      "$ref": "#/components/parameters/CollectScreenHeight"
     },
     {
      "$ref": "#/components/parameters/CollectError"
     },
     {
      "$ref": "#/components/parameters/CollectUa"
     },
     {
      "$ref": "#/components/parameters/CollectTimezone"
     },
     {
      "$ref": "#/components/parameters/CollectLanguage"
     },
     {
      "$ref": "#/components/parameters/CollectOsName"
     },
     {
      "$ref": "#/components/parameters/CollectOsVersion"
     },
     {
      "$ref": "#/components/parameters/CollectBrands"
     },
     {
      "$ref": "#/components/parameters/CollectVersion"
     },
     {
      "$ref": "#/components/parameters/CollectHostnameOriginal"
     },
     {
      "$ref": "#/components/parameters/CollectUnique"
     },
     {
      "$ref": "#/components/parameters/CollectMobile"
     },
     {
      "$ref": "#/components/parameters/CollectBot"
     },
     {
      "$ref": "#/components/parameters/CollectBrave"
     },
     {
      "$ref": "#/components/parameters/CollectDuck"
     },
     {
      "$ref": "#/components/parameters/CollectHttps"
     },
     {
      "$ref": "#/components/parameters/CollectCollectDnt"
     },
     {
      "$ref": "#/components/parameters/CollectUtmSource"
     },
     {
      "$ref": "#/components/parameters/CollectUtmMedium"
     },
     {
      "$ref": "#/components/parameters/CollectUtmCampaign"
     },
     {
      "$ref": "#/components/parameters/CollectUtmTerm"
     },
     {
      "$ref": "#/components/parameters/CollectUtmContent"
     },
     {
      "$ref": "#/components/parameters/CollectSource"
     },
     {
      "$ref": "#/components/parameters/CollectRef"
     },
     {
      "$ref": "#/components/parameters/CollectMedium"
     },
     {
      "$ref": "#/components/parameters/CollectCampaign"
     },
     {
      "$ref": "#/components/parameters/CollectTerm"
     },
     {
      "$ref": "#/components/parameters/CollectContent"
     },
     {
      "$ref": "#/components/parameters/Dnt"
     },
     {
      "$ref": "#/components/parameters/XDoNotTrack"
     },
     {
      "$ref": "#/components/parameters/Referer"
     }
    ]
   },
   "options": {
    "tags": [
     "Collector"
    ],
    "operationId": "preflight_noscript_gif",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/events": {
   "post": {
    "tags": [
     "Collector"
    ],
    "operationId": "collectJson",
    "summary": "Collect one JSON payload",
    "description": "Unauthenticated JSON collector. Both routes default to pageview; set type explicitly. Append original_id links the canonical parent pageview, while each independent increment gets its own id. Bounded per-isolate replay suppression and report reconciliation do not guarantee delivery. Bodies over 64 KiB return 413. JSON must parse before DNT skipping. Writes run in the background; 200 is not durable confirmation. Unregistered/unverified sites and registry failures fail closed. Bots may be stored but are excluded from reports. Country comes from timezone, not IP; no client timestamp/IP override.",
    "responses": {
     "200": {
      "$ref": "#/components/responses/CollectorOk"
     },
     "400": {
      "$ref": "#/components/responses/CollectorBadRequest"
     },
     "413": {
      "$ref": "#/components/responses/CollectorPayloadTooLarge"
     }
    },
    "security": [],
    "parameters": [
     {
      "$ref": "#/components/parameters/Dnt"
     },
     {
      "$ref": "#/components/parameters/XDoNotTrack"
     }
    ],
    "requestBody": {
     "$ref": "#/components/requestBodies/Collector"
    }
   },
   "options": {
    "tags": [
     "Collector"
    ],
    "operationId": "preflight_events",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/append": {
   "post": {
    "tags": [
     "Collector"
    ],
    "operationId": "collectAppend",
    "summary": "Collect a duration or scroll append",
    "description": "Unauthenticated JSON collector. Both routes default to pageview; set type explicitly. Append original_id links the canonical parent pageview, while each independent increment gets its own id. Bounded per-isolate replay suppression and report reconciliation do not guarantee delivery. Bodies over 64 KiB return 413. JSON must parse before DNT skipping. Writes run in the background; 200 is not durable confirmation. Unregistered/unverified sites and registry failures fail closed. Bots may be stored but are excluded from reports. Country comes from timezone, not IP; no client timestamp/IP override.",
    "responses": {
     "200": {
      "$ref": "#/components/responses/CollectorOk"
     },
     "400": {
      "$ref": "#/components/responses/CollectorBadRequest"
     },
     "413": {
      "$ref": "#/components/responses/CollectorPayloadTooLarge"
     }
    },
    "security": [],
    "parameters": [
     {
      "$ref": "#/components/parameters/Dnt"
     },
     {
      "$ref": "#/components/parameters/XDoNotTrack"
     }
    ],
    "requestBody": {
     "$ref": "#/components/requestBodies/Collector"
    }
   },
   "options": {
    "tags": [
     "Collector"
    ],
    "operationId": "preflight_append",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/healthz": {
   "get": {
    "tags": [
     "Health"
    ],
    "operationId": "getHealth",
    "summary": "Check HTTP liveness",
    "description": "Returns ok without querying storage or the registry. Not a readiness or durability check. No collector CORS headers on this response. GET and HEAD only.",
    "responses": {
     "200": {
      "description": "The HTTP handler responded.",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string",
         "const": "ok"
        },
        "example": "ok"
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/CollectorGetRequired"
     }
    },
    "security": []
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_healthz",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/import-sources": {
   "get": {
    "tags": [
     "Imports"
    ],
    "operationId": "listImportSources",
    "summary": "List historical import sources",
    "description": "Requires a Firebase ID token or a management key of any Clicktag account; no site target or owner check. Returns source metadata and configuration field definitions, never credentials. The only current source is simpleanalytics. Product API responses have no cross-origin CORS headers.",
    "responses": {
     "200": {
      "description": "Source definitions.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ImportSourcesResponse"
        },
        "example": {
         "sources": [
          {
           "id": "simpleanalytics",
           "label": "SimpleAnalytics",
           "description": "Import pageviews and events from a SimpleAnalytics export.",
           "configFields": [
            {
             "key": "user_id",
             "label": "User ID",
             "type": "text",
             "required": true,
             "placeholder": "sa_user_id_…",
             "help": "SimpleAnalytics dashboard → Account → API."
            },
            {
             "key": "api_key",
             "label": "API key",
             "type": "password",
             "required": true,
             "placeholder": "sa_api_key_…"
            },
            {
             "key": "source_hostname",
             "label": "Source hostname",
             "type": "text",
             "required": true,
             "help": "The hostname as registered in SimpleAnalytics. Usually the same as this site."
            }
           ]
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the read scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "scope": {
          "value": {
           "error": "scope read required"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_import_sources",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/imports": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Imports"
    ],
    "operationId": "listImports",
    "summary": "List recent site imports",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Returns at most 50 jobs ordered by created_at descending, across all statuses. No pagination, cursor, offset or limit parameters. Password config fields are never echoed.",
    "responses": {
     "200": {
      "description": "Latest jobs, or an empty array.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ImportsResponse"
        },
        "example": {
         "imports": [
          {
           "id": "746b4998-643d-44c0-848d-4d8fe4c6ebef",
           "site": "your-domain.example",
           "source": "simpleanalytics",
           "source_label": "SimpleAnalytics",
           "status": "queued",
           "range_start": "2026-08-01",
           "range_end": "2026-08-31",
           "chunks_total": 2,
           "chunks_done": 0,
           "rows_imported": 0,
           "rows_skipped": 0,
           "error": null,
           "config": {
            "user_id": "${SA_USER_ID}",
            "source_hostname": "your-domain.example"
           },
           "created_at": "2026-09-18T12:00:00.000Z",
           "finished_at": null
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the read scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "scope": {
          "value": {
           "error": "scope read required"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "post": {
    "tags": [
     "Imports"
    ],
    "operationId": "createImport",
    "summary": "Create and queue a historical import",
    "description": "Requires the site owner Firebase ID bearer token even when the site is public. Authentication is checked before site lookup. Checks run in this order: site owner (404), verification (403), dates (400, also for a missing or non-JSON body), source and configuration (400), the daily limit (429), the active job (409), then the credential test (400). The credential test makes one request for a recent SimpleAnalytics pageview export and gives up after 20 seconds. 201 means queued, not finished. At most 20 new jobs per owner in the rolling previous 24 hours across all sites and statuses, with one queued/running job per site. The planner preserves the selected inclusive date boundaries in monthly pageview and event chunks. New imports skip repeated stable SA source IDs across jobs. Live TT rows have independent IDs, so overlap with live coverage still requires a deliberate cutoff. Import creation has no request idempotency key. Verify progress and actual reports before considering the migration complete. Do not automatically repeat POST after an ambiguous failure. Not available to management keys.",
    "responses": {
     "201": {
      "description": "Job created and queued. No historical data completion guarantee.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ImportJob"
        },
        "example": {
         "id": "746b4998-643d-44c0-848d-4d8fe4c6ebef",
         "site": "your-domain.example",
         "source": "simpleanalytics",
         "source_label": "SimpleAnalytics",
         "status": "queued",
         "range_start": "2026-08-01",
         "range_end": "2026-08-31",
         "chunks_total": 2,
         "chunks_done": 0,
         "rows_imported": 0,
         "rows_skipped": 0,
         "error": null,
         "config": {
          "user_id": "${SA_USER_ID}",
          "source_hostname": "your-domain.example"
         },
         "created_at": "2026-09-18T12:00:00.000Z",
         "finished_at": null
        }
       }
      }
     },
     "400": {
      "description": "Date, source or source-configuration validation failed. A failed credential test returns `SimpleAnalytics returned <status>: ` or `SimpleAnalytics error: ` followed by up to 200 characters of the response, or the network error.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "range": {
          "value": {
           "error": "invalid date range"
          }
         },
         "source": {
          "value": {
           "error": "unknown import source"
          }
         },
         "userId": {
          "value": {
           "error": "User ID is required"
          }
         },
         "apiKey": {
          "value": {
           "error": "API key is required"
          }
         },
         "hostnameRequired": {
          "value": {
           "error": "Source hostname is required"
          }
         },
         "hostname": {
          "value": {
           "error": "Source hostname is invalid"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the call used a management key.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "managementKey": {
          "value": {
           "error": "management keys cannot call this endpoint"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "409": {
      "description": "Another queued or running job already exists for this site.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "active": {
          "value": {
           "error": "an import is already running for this site"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Owner has already created at least 20 jobs in the rolling previous 24 hours, across all sites and statuses. Same-job retry is not a new creation.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "limit": {
          "value": {
           "error": "too many imports today, try again tomorrow"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "Queue submission failed after job creation. The handler cancels the new job, which still counts toward the daily limit and keeps its credentials for 7 days, so it can be retried; inspect the site job list before another action.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "queue": {
          "value": {
           "error": "import queue unavailable, please try again"
          }
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateImport"
       },
       "example": {
        "source": "simpleanalytics",
        "config": {
         "user_id": "${SA_USER_ID}",
         "api_key": "${SA_API_KEY}",
         "source_hostname": "your-domain.example"
        },
        "start": "2026-08-01",
        "end": "2026-08-31"
       }
      }
     }
    }
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_imports",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/imports/{jobId}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "$ref": "#/components/parameters/ImportJobId"
    }
   ],
   "get": {
    "tags": [
     "Imports"
    ],
    "operationId": "getImport",
    "summary": "Read import state and progress",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Returns the job directly, not wrapped. Progress counters are checkpointed only after an entire chunk; records can already be written before counters advance. Verify progress and actual reports before considering the migration complete.",
    "responses": {
     "200": {
      "description": "Current job snapshot.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ImportJob"
        },
        "example": {
         "id": "746b4998-643d-44c0-848d-4d8fe4c6ebef",
         "site": "your-domain.example",
         "source": "simpleanalytics",
         "source_label": "SimpleAnalytics",
         "status": "queued",
         "range_start": "2026-08-01",
         "range_end": "2026-08-31",
         "chunks_total": 2,
         "chunks_done": 0,
         "rows_imported": 0,
         "rows_skipped": 0,
         "error": null,
         "config": {
          "user_id": "${SA_USER_ID}",
          "source_hostname": "your-domain.example"
         },
         "created_at": "2026-09-18T12:00:00.000Z",
         "finished_at": null
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the read scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "scope": {
          "value": {
           "error": "scope read required"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownImport"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_imports_jobId",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/imports/{jobId}/cancel": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "$ref": "#/components/parameters/ImportJobId"
    }
   ],
   "post": {
    "tags": [
     "Imports"
    ],
    "operationId": "cancelImport",
    "summary": "Cancel an active import",
    "description": "Requires the site owner Firebase ID bearer token even when the site is public. Authentication is checked before site lookup. No request body. Only queued/running jobs may be canceled. A chunk still being exported is discarded; a chunk already being written finishes first, and the cancel waits for it. Rows already written are not rolled back. Stored source configuration is kept for retry; the API key is deleted 7 days after the job ends. The response uses the prior job snapshot with status set to canceled, so finished_at and the counters can be stale; poll GET for persisted state. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Cancellation recorded. Rows already written stay; poll GET for the final counters.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ImportJob"
        },
        "example": {
         "id": "746b4998-643d-44c0-848d-4d8fe4c6ebef",
         "site": "your-domain.example",
         "source": "simpleanalytics",
         "source_label": "SimpleAnalytics",
         "status": "canceled",
         "range_start": "2026-08-01",
         "range_end": "2026-08-31",
         "chunks_total": 2,
         "chunks_done": 0,
         "rows_imported": 0,
         "rows_skipped": 0,
         "error": null,
         "config": {
          "user_id": "${SA_USER_ID}",
          "source_hostname": "your-domain.example"
         },
         "created_at": "2026-09-18T12:00:00.000Z",
         "finished_at": null
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "Called with a management key; sign in instead.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "managementKey": {
          "value": {
           "error": "management keys cannot call this endpoint"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownImport"
     },
     "409": {
      "description": "Job is not queued or running.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "state": {
          "value": {
           "error": "import is not active"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_imports_jobId_cancel",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/imports/{jobId}/retry": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "$ref": "#/components/parameters/ImportJobId"
    }
   ],
   "post": {
    "tags": [
     "Imports"
    ],
    "operationId": "retryImport",
    "summary": "Resume a failed or canceled import",
    "description": "Requires the site owner Firebase ID bearer token even when the site is public. Authentication is checked before site lookup. No request body; credentials and dates cannot be replaced. Only failed/canceled jobs may be retried, not completed jobs. Resumes the same job from chunks_done with existing counters and config; checkpointed chunks are skipped, and the sink skips stable SA source records already inserted by a partial attempt. This does not reconcile unrelated live collector IDs or reconstruct interrupted counters exactly. The creation-rate cap is not reapplied. If queue submission fails, the job is marked failed with error `import queue unavailable` and the call returns 502. Another queued or running job on the site makes the retry return 409. The API key is deleted 7 days after a job fails or is canceled; retrying after that returns 409, so start a new import. The response uses the prior snapshot with status queued and error null; finished_at can be stale. Poll GET before further actions. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Same job re-queued, not import finished.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ImportJob"
        },
        "example": {
         "id": "746b4998-643d-44c0-848d-4d8fe4c6ebef",
         "site": "your-domain.example",
         "source": "simpleanalytics",
         "source_label": "SimpleAnalytics",
         "status": "queued",
         "range_start": "2026-08-01",
         "range_end": "2026-08-31",
         "chunks_total": 2,
         "chunks_done": 0,
         "rows_imported": 0,
         "rows_skipped": 0,
         "error": null,
         "config": {
          "user_id": "${SA_USER_ID}",
          "source_hostname": "your-domain.example"
         },
         "created_at": "2026-09-18T12:00:00.000Z",
         "finished_at": "2026-09-18T12:01:00.000Z"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "Called with a management key; sign in instead.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "managementKey": {
          "value": {
           "error": "management keys cannot call this endpoint"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownImport"
     },
     "409": {
      "description": "Job is neither failed nor canceled, its API key was already deleted, or another job on the site is queued or running.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "state": {
          "value": {
           "error": "only failed or canceled imports can be retried"
          }
         },
         "keyDeleted": {
          "value": {
           "error": "The API key for this import was deleted. Start a new import."
          }
         },
         "active": {
          "value": {
           "error": "an import is already running for this site"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "Queue submission failed after the job was re-queued. The job is marked failed with error `import queue unavailable` and can be retried again.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "queue": {
          "value": {
           "error": "import queue unavailable, please try again"
          }
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_imports_jobId_retry",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/reports": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Reports"
    ],
    "operationId": "listReportSubscriptions",
    "summary": "List email report subscriptions",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Returns subscriptions ordered by creation time. Unsubscribe tokens are never echoed. Rows created from the workspace defaults are not listed; effective says what the site sends.",
    "responses": {
     "200": {
      "description": "All subscriptions, or an empty array.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ReportsResponse"
        },
        "example": {
         "mode": "custom",
         "subscriptions": [
          {
           "id": "9d3b1c2e-1234-4f56-8abc-0def12345678",
           "email": "teammate@company.com",
           "frequency": "weekly",
           "timezone": "Europe/Amsterdam",
           "send_hour": 9,
           "enabled": true,
           "sections": {
            "overview": true,
            "pages": true,
            "referrers": true,
            "countries": false,
            "events": false,
            "alerts": true,
            "api": true
           },
           "last_sent_at": "2026-09-14T07:07:11.000Z",
           "fail_count": 0,
           "created_at": "2026-09-10T12:00:00.000Z"
          }
         ],
         "effective": {
          "layer": "custom",
          "sends": true,
          "recipients": [
           "teammate@company.com"
          ],
          "frequency": "weekly",
          "timezone": "Europe/Amsterdam",
          "send_hour": 9,
          "next_send_at": "2026-10-05T07:00:00.000Z"
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "post": {
    "tags": [
     "Reports"
    ],
    "operationId": "createReportSubscription",
    "summary": "Add an email report recipient",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. The site must be verified (403) and set to custom (409). One subscription per site, email and frequency; duplicates return 409. At most 25 subscriptions per site, paused ones included (429). Owner-created subscriptions are confirmed immediately, without a confirmation email.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateReportSubscription"
       },
       "example": {
        "email": "teammate@company.com",
        "frequency": "weekly",
        "timezone": "Europe/Amsterdam",
        "send_hour": 9
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Subscription created.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ReportSubscription"
        },
        "example": {
         "id": "9d3b1c2e-1234-4f56-8abc-0def12345678",
         "email": "teammate@company.com",
         "frequency": "weekly",
         "timezone": "Europe/Amsterdam",
         "send_hour": 9,
         "enabled": true,
         "sections": {
          "overview": true,
          "pages": true,
          "referrers": true,
          "countries": false,
          "events": false,
          "alerts": true,
          "api": true
         },
         "last_sent_at": null,
         "fail_count": 0,
         "created_at": "2026-09-10T12:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "Validation failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "email": {
          "value": {
           "error": "invalid email address"
          }
         },
         "frequency": {
          "value": {
           "error": "frequency must be daily, weekly, or monthly"
          }
         },
         "timezone": {
          "value": {
           "error": "invalid timezone"
          }
         },
         "sendHour": {
          "value": {
           "error": "send_hour must be an integer between 0 and 23"
          }
         },
         "sections": {
          "value": {
           "error": "invalid sections"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/SiteManageForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "409": {
      "description": "That email already has this report for the site, or the site does not use custom reports.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "duplicate": {
          "value": {
           "error": "that email already gets this report"
          }
         },
         "notCustom": {
          "value": {
           "error": "switch this site to custom reports first"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Subscription limit reached (25 per site, paused ones included), or too many changes from one management key.",
      "headers": {
       "Retry-After": {
        "description": "Only on the management key rate limit; currently always 1.",
        "schema": {
         "type": "integer",
         "minimum": 1
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "limit": {
          "value": {
           "error": "too many recipients for this site"
          }
         },
         "rateLimited": {
          "value": {
           "error": "rate limited, retry in 1s"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/reports/preview": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Reports"
    ],
    "operationId": "previewReport",
    "summary": "Render a report email as HTML",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. The site must be verified. Builds the email for the last fully elapsed period and returns it as text/html. `frequency` defaults to weekly and an unknown `tz` falls back to UTC; `sections` is a comma list of pages,referrers,countries,events,alerts,api (omit for all), overview is always included and unknown keys are ignored.",
    "responses": {
     "200": {
      "description": "Rendered email HTML.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "Validation failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "invalid frequency"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "name": "frequency",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "daily",
        "weekly",
        "monthly"
       ],
       "default": "weekly"
      }
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "description": "IANA timezone; an unknown value falls back to UTC."
     },
     {
      "name": "sections",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Comma-separated keys from pages, referrers, countries, events, alerts, api. Omit for every section.",
      "example": "pages,referrers,alerts"
     }
    ]
   }
  },
  "/api/sites/{hostname}/reports/{subscriptionId}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "name": "subscriptionId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "description": "Subscription id returned at creation."
    }
   ],
   "patch": {
    "tags": [
     "Reports"
    ],
    "operationId": "updateReportSubscription",
    "summary": "Update a subscription",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Accepts any subset of frequency, timezone, send_hour, enabled, sections. Disabling pauses without deleting; enabling resets fail_count and needs a verified site (403). Moving the address to a frequency it already has returns 409. Rows created from the workspace defaults return 404.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/UpdateReportSubscription"
       },
       "example": {
        "enabled": false
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Updated subscription.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ReportSubscription"
        },
        "example": {
         "id": "9d3b1c2e-1234-4f56-8abc-0def12345678",
         "email": "teammate@company.com",
         "frequency": "weekly",
         "timezone": "Europe/Amsterdam",
         "send_hour": 9,
         "enabled": false,
         "sections": {
          "overview": true,
          "pages": true,
          "referrers": true,
          "countries": false,
          "events": false,
          "alerts": true,
          "api": true
         },
         "last_sent_at": "2026-09-14T07:07:11.000Z",
         "fail_count": 0,
         "created_at": "2026-09-10T12:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "Validation failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "empty": {
          "value": {
           "error": "nothing to update"
          }
         },
         "enabled": {
          "value": {
           "error": "enabled must be a boolean"
          }
         },
         "frequency": {
          "value": {
           "error": "frequency must be daily, weekly, or monthly"
          }
         },
         "timezone": {
          "value": {
           "error": "invalid timezone"
          }
         },
         "sendHour": {
          "value": {
           "error": "send_hour must be an integer between 0 and 23"
          }
         },
         "sections": {
          "value": {
           "error": "invalid sections"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/SiteManageForbidden"
     },
     "404": {
      "description": "Unknown site, or no subscription with this id among the site's own subscriptions.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "subscription": {
          "value": {
           "error": "subscription not found"
          }
         }
        }
       }
      }
     },
     "409": {
      "description": "The address already has a subscription with that frequency.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "duplicate": {
          "value": {
           "error": "that email already gets this report"
          }
         }
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   },
   "delete": {
    "tags": [
     "Reports"
    ],
    "operationId": "deleteReportSubscription",
    "summary": "Delete a subscription",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Only the site's own subscriptions; rows created from the workspace defaults return 404.",
    "responses": {
     "200": {
      "description": "Deleted.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "example": {
         "ok": true
        },
        "schema": {
         "type": "object",
         "properties": {
          "ok": {
           "type": "boolean",
           "const": true
          }
         },
         "required": [
          "ok"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "404": {
      "description": "Unknown site, or no subscription with this id among the site's own subscriptions.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "subscription": {
          "value": {
           "error": "subscription not found"
          }
         }
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/reports/{subscriptionId}/test": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "name": "subscriptionId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "description": "Subscription id returned at creation."
    }
   ],
   "post": {
    "tags": [
     "Reports"
    ],
    "operationId": "testReportSubscription",
    "summary": "Queue a test email",
    "description": "Requires the site owner Firebase ID bearer token even when the site is public. Authentication is checked before site lookup. The site must be verified. Generates a real email to the subscription's address for the last fully elapsed window and queues it immediately. Test sends never block the scheduled window for the same period. Sends at most one test per 30 seconds per subscription, and a paused subscription returns 409. A delivered test resets fail_count and sets last_sent_at. Not available to management keys.",
    "responses": {
     "202": {
      "description": "Test email queued.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "example": {
         "queued": true,
         "period_start": "2026-09-07T22:00:00.000Z",
         "period_end": "2026-09-13T22:00:00.000Z"
        },
        "schema": {
         "type": "object",
         "properties": {
          "queued": {
           "type": "boolean",
           "const": true
          },
          "period_start": {
           "type": "string",
           "format": "date-time"
          },
          "period_end": {
           "type": "string",
           "format": "date-time"
          }
         },
         "required": [
          "queued",
          "period_start",
          "period_end"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/VerificationRequired"
     },
     "404": {
      "description": "Unknown site, or no subscription with this id among the site's own subscriptions.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "subscription": {
          "value": {
           "error": "subscription not found"
          }
         }
        }
       }
      }
     },
     "409": {
      "description": "The subscription is paused.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "this report is paused, resume it to send a test"
        }
       }
      }
     },
     "429": {
      "description": "A test was sent less than 30 seconds ago.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "a test was just sent, try again in a moment"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "Queue unavailable; nothing was scheduled.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "failed to queue test report"
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/reports/unsubscribe/{token}": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "pattern": "^[a-f0-9]{32}$"
     },
     "description": "Unsubscribe capability token from the email footer or List-Unsubscribe header."
    }
   ],
   "get": {
    "tags": [
     "Reports"
    ],
    "operationId": "unsubscribeConfirm",
    "summary": "Unsubscribe confirmation page",
    "description": "Public, no authentication. Renders an HTML confirmation page with a POST form. Links from a site's own recipients name the site; links from workspace default reports say the address leaves the workspace list; digest links name the workspace digest.",
    "responses": {
     "200": {
      "description": "Confirmation page.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "404": {
      "description": "Invalid or expired token.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Reports"
    ],
    "operationId": "unsubscribePost",
    "summary": "One-click unsubscribe",
    "description": "Public, no authentication. A site's own recipient is paused for that site; a workspace default recipient is removed from the owner's default list, which stops it for every inheriting site; a digest recipient is removed from the digest list. Removed addresses show up in the owner's dropped list until the next save. Renders an HTML confirmation. Supports RFC 8058 one-click unsubscription from mail clients.",
    "responses": {
     "200": {
      "description": "Unsubscribed.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "404": {
      "description": "Invalid or expired token.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/api/sites/{hostname}/alerts": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Alerts"
    ],
    "operationId": "listApiAlertRules",
    "summary": "List API alert rules",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. Returns rules ordered by creation time.",
    "responses": {
     "200": {
      "description": "All rules, or an empty array.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiAlertRulesResponse"
        },
        "example": {
         "rules": [
          {
           "id": "5b0f8a8e-3c1d-4d7e-9a61-2f4c8e1b7d90",
           "metric": "error_rate",
           "method": "POST",
           "route": "/v1/orders",
           "threshold": 2,
           "window_minutes": 15,
           "min_requests": 20,
           "recipients": [
            "oncall@company.com"
           ],
           "enabled": true,
           "state": "firing",
           "state_changed_at": "2026-09-28T09:42:00.000Z",
           "last_value": 6.4,
           "created_at": "2026-09-20T12:00:00.000Z"
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "post": {
    "tags": [
     "Alerts"
    ],
    "operationId": "createApiAlertRule",
    "summary": "Add an API alert rule",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified. At most 20 rules per site.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateApiAlertRule"
       },
       "example": {
        "metric": "error_rate",
        "threshold": 2,
        "window_minutes": 15,
        "method": "POST",
        "route": "/v1/orders",
        "recipients": [
         "oncall@company.com"
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Rule created.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiAlertRule"
        },
        "example": {
         "id": "5b0f8a8e-3c1d-4d7e-9a61-2f4c8e1b7d90",
         "metric": "error_rate",
         "method": "POST",
         "route": "/v1/orders",
         "threshold": 2,
         "window_minutes": 15,
         "min_requests": 20,
         "recipients": [
          "oncall@company.com"
         ],
         "enabled": true,
         "state": "ok",
         "state_changed_at": null,
         "last_value": null,
         "created_at": "2026-09-20T12:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "Validation failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "managementKey": {
          "value": {
           "error": "recipients is required when using a management key"
          }
         },
         "metric": {
          "value": {
           "error": "metric must be error_rate, p95_ms or silence"
          }
         },
         "scope": {
          "value": {
           "error": "method and route go together"
          }
         },
         "method": {
          "value": {
           "error": "invalid method"
          }
         },
         "route": {
          "value": {
           "error": "invalid route"
          }
         },
         "window": {
          "value": {
           "error": "window_minutes must be 5, 15 or 60"
          }
         },
         "minRequests": {
          "value": {
           "error": "min_requests must be a whole number from 1 to 1000000"
          }
         },
         "enabled": {
          "value": {
           "error": "enabled must be true or false"
          }
         },
         "silence": {
          "value": {
           "error": "silence alerts take no threshold"
          }
         },
         "errorRate": {
          "value": {
           "error": "threshold must be a percentage below 100"
          }
         },
         "p95": {
          "value": {
           "error": "threshold must be below 600000 ms"
          }
         },
         "recipients": {
          "value": {
           "error": "recipients must be a list of email addresses"
          }
         },
         "noRecipients": {
          "value": {
           "error": "add at least one recipient"
          }
         },
         "tooManyRecipients": {
          "value": {
           "error": "at most 10 recipients"
          }
         },
         "email": {
          "value": {
           "error": "invalid email address"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet (API-only sites get `Verify your domain to turn on alerts`), or the management key lacks the manage scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "apiSite": {
          "value": {
           "error": "Verify your domain to turn on alerts"
          }
         },
         "scope": {
          "value": {
           "error": "scope manage required"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "429": {
      "description": "Rule limit reached (20 per site), or too many changes from one management key.",
      "headers": {
       "Retry-After": {
        "description": "Only on the management key rate limit; currently always 1.",
        "schema": {
         "type": "integer",
         "minimum": 1
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "limit": {
          "value": {
           "error": "too many alerts for this site"
          }
         },
         "rateLimited": {
          "value": {
           "error": "rate limited, retry in 1s"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/alerts/{alertId}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "name": "alertId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "description": "Rule id returned at creation."
    }
   ],
   "patch": {
    "tags": [
     "Alerts"
    ],
    "operationId": "updateApiAlertRule",
    "summary": "Pause or resume a rule",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/UpdateApiAlertRule"
       },
       "example": {
        "enabled": false
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Updated rule.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiAlertRule"
        },
        "example": {
         "id": "5b0f8a8e-3c1d-4d7e-9a61-2f4c8e1b7d90",
         "metric": "error_rate",
         "method": "POST",
         "route": "/v1/orders",
         "threshold": 2,
         "window_minutes": 15,
         "min_requests": 20,
         "recipients": [
          "oncall@company.com"
         ],
         "enabled": false,
         "state": "ok",
         "state_changed_at": "2026-09-28T09:42:00.000Z",
         "last_value": 6.4,
         "created_at": "2026-09-20T12:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "Body is not exactly { enabled }.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "onlyEnabled": {
          "value": {
           "error": "only enabled can be changed"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet (API-only sites get `Verify your domain to turn on alerts`), or the management key lacks the manage scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "apiSite": {
          "value": {
           "error": "Verify your domain to turn on alerts"
          }
         },
         "scope": {
          "value": {
           "error": "scope manage required"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "Unknown site or rule.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "unknown alert"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   },
   "delete": {
    "tags": [
     "Alerts"
    ],
    "operationId": "deleteApiAlertRule",
    "summary": "Delete a rule",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404.",
    "responses": {
     "200": {
      "description": "Deleted.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "example": {
         "deleted": "5b0f8a8e-3c1d-4d7e-9a61-2f4c8e1b7d90"
        },
        "schema": {
         "type": "object",
         "properties": {
          "deleted": {
           "type": "string",
           "description": "Id of the deleted rule."
          }
         },
         "required": [
          "deleted"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "404": {
      "description": "Unknown site or rule.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "unknown alert"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/alerts/{alertId}/test": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "name": "alertId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "description": "Rule id returned at creation."
    }
   ],
   "post": {
    "tags": [
     "Alerts"
    ],
    "operationId": "testApiAlertRule",
    "summary": "Send a test alert",
    "description": "Requires the site owner Firebase ID bearer token even when the site is public. Sites you don't own return 404. Emails a sample alert only to you, the signed-in owner, without changing the rule's state. Once per 30 seconds per rule. Not available to management keys.",
    "responses": {
     "200": {
      "description": "The address the test went to.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "sent_to": {
           "type": "string",
           "format": "email"
          }
         },
         "required": [
          "sent_to"
         ],
         "additionalProperties": false
        },
        "example": {
         "sent_to": "you@company.com"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet. API-only sites get `Verify your domain to turn on alerts`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Install your tracking script to verify this site first"
        }
       }
      }
     },
     "404": {
      "description": "Unknown site or rule.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "unknown alert"
        }
       }
      }
     },
     "429": {
      "description": "A test was sent less than 30 seconds ago.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "a test was just sent, try again in a moment"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "The test email could not be sent.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "could not send the test email"
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/alerts/unsubscribe/{alertId}/{token}": {
   "parameters": [
    {
     "name": "alertId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "pattern": "^[0-9a-f-]{36}$"
     },
     "description": "Rule id from the opt-out link."
    },
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "pattern": "^[0-9a-f]{64}$"
     },
     "description": "Per-recipient capability token from the email footer or List-Unsubscribe header."
    }
   ],
   "get": {
    "tags": [
     "Alerts"
    ],
    "operationId": "alertOptoutConfirm",
    "summary": "Alert opt-out confirmation page",
    "description": "Public, no authentication. Renders an HTML confirmation page with a POST form.",
    "responses": {
     "200": {
      "description": "Confirmation page.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "404": {
      "description": "Invalid link, or the address was already removed.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Alerts"
    ],
    "operationId": "alertOptoutPost",
    "summary": "One-click alert opt-out",
    "description": "Public, no authentication. Removes the link's recipient from the rule, or pauses the rule when it was the last recipient, and renders an HTML confirmation. Supports RFC 8058 one-click unsubscription from mail clients.",
    "responses": {
     "200": {
      "description": "Opted out.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "404": {
      "description": "Invalid link, or the address was already removed.",
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/api/import-oauth/google/start": {
   "post": {
    "tags": [
     "Search Console"
    ],
    "operationId": "startGoogleOauth",
    "summary": "Start the Google OAuth consent flow",
    "description": "Requires the site owner's Firebase ID bearer token. Returns the Google consent URL and sets a nonce cookie the callback checks, so send the same browser to the URL. Google returns to the unchanged callback on https://totallytics.com/api/import-oauth/google/callback, which hands off to the host the flow started on; the flow ends at {origin}/app/{site}/settings?gsc=connected or ?gsc=error&reason=<reason>. Not available to management keys.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "site": {
          "type": "string",
          "example": "example.com"
         },
         "provider": {
          "type": "string",
          "enum": [
           "google-search-console"
          ],
          "default": "google-search-console"
         },
         "origin": {
          "type": "string",
          "example": "https://clicktag.app",
          "description": "Optional. Must equal the origin of the host receiving this request."
         }
        },
        "required": [
         "site"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Consent URL.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       },
       "Set-Cookie": {
        "description": "__Host-gsc_oauth_nonce, HttpOnly, Secure and SameSite=Lax, valid for 10 minutes.",
        "schema": {
         "type": "string"
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "url": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "Invalid site, provider, or origin.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "invalid site"
          }
         },
         "provider": {
          "value": {
           "error": "unknown provider"
          }
         },
         "origin": {
          "value": {
           "error": "invalid origin"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "404": {
      "description": "Site not found.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "site not found"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/import-oauth/google/callback": {
   "get": {
    "tags": [
     "Search Console"
    ],
    "operationId": "googleOauthCallback",
    "summary": "Google OAuth redirect target",
    "description": "Public endpoint: the encrypted state blob, valid for 10 minutes, is the capability. Google returns here on the legacy https://totallytics.com host; when the flow started on another host, this redirects to the same path there with state, code and error. That host checks the nonce cookie set by start, exchanges the code, stores the encrypted refresh token and redirects to /app/{site}/settings. There is one connection per account and provider: reconnecting replaces its token and resumes links in error, after checking that the Google account can still read every linked property.",
    "parameters": [
     {
      "name": "state",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "code",
      "in": "query",
      "description": "Authorization code from Google.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "error",
      "in": "query",
      "description": "Set by Google when consent is refused; the flow ends with reason=denied.",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "302": {
      "description": "Redirect to the host the flow started on, or to {origin}/app/{site}/settings with ?gsc=connected or ?gsc=error&reason=<reason>: session (a different browser, or the cookie expired or was replaced by a newer attempt), site (the site is no longer yours), denied (consent refused), configuration or exchange (the code exchange failed), refresh (Google sent no refresh token), scope (Search Console access wasn't granted), unavailable (Google couldn't list the properties), account (the Google account can't read a property you already linked) or queue (connected, but syncing didn't restart).",
      "headers": {
       "Location": {
        "description": "Where the browser goes next.",
        "schema": {
         "type": "string",
         "format": "uri"
        }
       },
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      }
     },
     "400": {
      "description": "The state is missing, invalid or expired, or reached a host other than https://totallytics.com and the one the flow started on.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        },
        "example": "This Google connection attempt expired or is invalid. Return to site settings and connect again."
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": []
   }
  },
  "/api/import-oauth/google/connections": {
   "get": {
    "tags": [
     "Search Console"
    ],
    "operationId": "listOauthConnections",
    "summary": "List Google OAuth connections",
    "description": "Requires a Firebase ID bearer token. Refresh tokens are never echoed. Not available to management keys.",
    "responses": {
     "200": {
      "description": "All connections for the account.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "connections": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "id": {
              "type": "string"
             },
             "provider": {
              "type": "string"
             },
             "label": {
              "type": "string"
             },
             "created_at": {
              "type": "string",
              "format": "date-time"
             }
            }
           },
           "description": "Newest first, at most one per provider."
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/import-oauth/google/connections/{connectionId}": {
   "parameters": [
    {
     "name": "connectionId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "delete": {
    "tags": [
     "Search Console"
    ],
    "operationId": "deleteOauthConnection",
    "summary": "Delete a Google OAuth connection",
    "description": "Requires a Firebase ID bearer token. Deletes the connection, unlinks every site linked through it and deletes their synced Search Console data, then revokes the token at Google; a failed revocation is only logged. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Connection deleted.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "deleted": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "404": {
      "description": "Connection not found.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "connection not found"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/sites/{hostname}/gsc/properties": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Search Console"
    ],
    "operationId": "listGscProperties",
    "summary": "List Search Console properties of a connection",
    "description": "Requires the site owner Firebase ID bearer token. connection_id is an id from GET /api/import-oauth/google/connections. matches is true for sc-domain:{hostname} and for URL-prefix properties under https://{hostname}/. Properties the account only has unverified access to are left out. Not available to management keys.",
    "parameters": [
     {
      "name": "connection_id",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Properties, best matches first.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "properties": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "site_url": {
              "type": "string",
              "example": "sc-domain:example.com"
             },
             "permission_level": {
              "type": "string",
              "example": "siteOwner"
             },
             "matches": {
              "type": "boolean"
             }
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "connection_id is missing, or the connection expired (revoked: true).",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "$ref": "#/components/schemas/Error"
          },
          {
           "$ref": "#/components/schemas/GscRevoked"
          }
         ]
        },
        "examples": {
         "missing": {
          "value": {
           "error": "connection_id required"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "404": {
      "description": "Site or connection not found.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "site not found"
          }
         },
         "connection": {
          "value": {
           "error": "connection not found"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "Google refused or failed to list the properties.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "denied": {
          "value": {
           "error": "Google denied Search Console access. Reconnect Google and approve read-only access."
          }
         },
         "unavailable": {
          "value": {
           "error": "Google could not list your properties. Please try again."
          }
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/sites/{hostname}/gsc/link": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "post": {
    "tags": [
     "Search Console"
    ],
    "operationId": "linkGscProperty",
    "summary": "Link a Search Console property to this site",
    "description": "Requires the site owner Firebase ID bearer token. Checks that the connected Google account can read the property, then queues a backfill that walks back month by month through Google's 16 months of data. Re-linking restarts the backfill without deleting synced data. Not available to management keys.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "connection_id": {
          "type": "string"
         },
         "property": {
          "type": "string",
          "example": "sc-domain:example.com",
          "description": "An sc-domain: or http(s):// property from GET .../gsc/properties, at most 512 characters after trimming."
         }
        },
        "required": [
         "connection_id",
         "property"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Linked; backfill queued.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "linked": {
           "type": "boolean"
          },
          "property": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "connection_id is missing, the property is invalid, the connected Google account can't read it, or the connection expired (revoked: true).",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "$ref": "#/components/schemas/Error"
          },
          {
           "$ref": "#/components/schemas/GscRevoked"
          }
         ]
        },
        "examples": {
         "missing": {
          "value": {
           "error": "connection_id required"
          }
         },
         "property": {
          "value": {
           "error": "invalid property"
          }
         },
         "access": {
          "value": {
           "error": "the connected Google account has no access to that property"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/VerificationRequired"
     },
     "404": {
      "description": "Site or connection not found.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "connection": {
          "value": {
           "error": "connection not found"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "503": {
      "description": "The property was linked, but queue delivery failed. Retry with Sync now.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Property linked, but sync could not start. Use Sync now to retry."
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   },
   "delete": {
    "tags": [
     "Search Console"
    ],
    "operationId": "unlinkGscProperty",
    "summary": "Unlink Search Console from this site",
    "description": "Requires the site owner Firebase ID bearer token. Removes the link and deletes the site's synced Search Console data. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Unlinked; synced data deleted.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "unlinked": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "404": {
      "description": "Site not found or no link present.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "site not found"
          }
         },
         "link": {
          "value": {
           "error": "no Search Console link"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/sites/{hostname}/gsc/status": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Search Console"
    ],
    "operationId": "getGscStatus",
    "summary": "Get the Search Console link and sync state",
    "description": "Requires the site owner's Firebase ID token or a management key.",
    "responses": {
     "200": {
      "description": "Link state.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/GscStatus"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "description": "Site not found.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "site not found"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/gsc/sync": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "post": {
    "tags": [
     "Search Console"
    ],
    "operationId": "queueGscSync",
    "summary": "Resume the backfill or refresh the recent days",
    "description": "Requires the site owner's Firebase ID token or a management key. While the backfill runs, this queues the month at backfill_cursor again; once live, it refreshes the last 10 Search Console days.",
    "responses": {
     "202": {
      "description": "Sync queued.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "queued": {
           "type": "boolean"
          }
         }
        }
       }
      },
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "404": {
      "description": "Site not found or no link present.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "site not found"
          }
         },
         "link": {
          "value": {
           "error": "no Search Console link"
          }
         }
        }
       }
      }
     },
     "409": {
      "description": "The Google connection expired and must be reconnected before syncing.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          },
          "revoked": {
           "type": "boolean",
           "const": true
          }
         },
         "required": [
          "error",
          "revoked"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "503": {
      "description": "Sync could not be queued; retry later.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Sync queue unavailable. Please try again."
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/gsc/overview": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "$ref": "#/components/parameters/From"
    },
    {
     "$ref": "#/components/parameters/To"
    },
    {
     "$ref": "#/components/parameters/GscTimezone"
    },
    {
     "$ref": "#/components/parameters/GscFilter"
    }
   ],
   "get": {
    "tags": [
     "Search Console"
    ],
    "operationId": "getGscOverview",
    "summary": "Search Console totals, daily series, and pageviews from Google search",
    "description": "Requires the site owner's Firebase ID token or a management key. The range maps to calendar dates in tz, read as Search Console (Pacific Time) days. totals stop at final_through when the range runs past it (through says so), and previous covers as many days from the start of the equally sized period before the range. overlay compares GSC clicks with same-day pageviews from Google search measured by the tracker (the google.com source, bots and retries excluded). With f, totals, previous and both series are that query's or page's own Search Console rows on the same day axis, and overlay is empty.",
    "responses": {
     "200": {
      "description": "Overview.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/GscOverview"
        }
       }
      }
     },
     "400": {
      "description": "Invalid range or filter.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "range": {
          "value": {
           "error": "invalid range"
          }
         },
         "filters": {
          "value": {
           "error": "one Search filter at a time"
          }
         },
         "filter": {
          "value": {
           "error": "unknown Search filter"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/gsc/breakdown": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "$ref": "#/components/parameters/From"
    },
    {
     "$ref": "#/components/parameters/To"
    },
    {
     "$ref": "#/components/parameters/GscTimezone"
    },
    {
     "name": "dim",
     "in": "query",
     "required": true,
     "schema": {
      "type": "string",
      "enum": [
       "queries",
       "pages",
       "countries",
       "devices"
      ]
     }
    },
    {
     "name": "limit",
     "in": "query",
     "description": "Rows per page. Numbers are rounded down and clamped to 1-500; anything else uses 100.",
     "schema": {
      "type": "number",
      "default": 100,
      "minimum": 1,
      "maximum": 500
     }
    },
    {
     "name": "offset",
     "in": "query",
     "description": "Rows to skip, for paging.",
     "schema": {
      "type": "number",
      "default": 0,
      "minimum": 0,
      "maximum": 1000000
     }
    },
    {
     "name": "sort",
     "in": "query",
     "schema": {
      "type": "string",
      "enum": [
       "clicks",
       "impressions",
       "ctr",
       "position"
      ],
      "default": "clicks"
     }
    },
    {
     "name": "dir",
     "in": "query",
     "schema": {
      "type": "string",
      "enum": [
       "desc",
       "asc"
      ],
      "default": "desc"
     }
    },
    {
     "name": "q",
     "in": "query",
     "description": "Keeps rows whose name contains this text, ignoring case. Longer values are cut to 256 characters.",
     "schema": {
      "type": "string"
     }
    },
    {
     "$ref": "#/components/parameters/GscFilter"
    }
   ],
   "get": {
    "tags": [
     "Search Console"
    ],
    "operationId": "getGscBreakdown",
    "summary": "Queries, pages, countries, or devices, with the previous period",
    "description": "Requires the site owner's Firebase ID token or a management key. One sorted page of rows plus total, the number of rows matching q. Ties sort by clicks, impressions, then name. Rows stop at final_through when the range runs past it (through says so), and previous is the same row over as many days from the start of the equally sized period before the range, or null when it had no impressions then. Pages merge protocol, fragment and query-string variants into one row per host and path. With f, rows are the query x page pairs: a query's pages, grouped like the Pages tab, or a page's queries. GSC anonymizes rare queries, so query rows undershoot their totals; for dim=queries, anonymized is the exact remainder (the property's daily totals, or the filtered page's own rows, minus every query row over the same days), regardless of q and paging, or null when it can't be exact.",
    "responses": {
     "200": {
      "description": "Breakdown rows.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/GscBreakdownResponse"
        }
       }
      }
     },
     "400": {
      "description": "Invalid range, dim, sort, dir or filter.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "range": {
          "value": {
           "error": "invalid range"
          }
         },
         "dim": {
          "value": {
           "error": "unknown dim"
          }
         },
         "sort": {
          "value": {
           "error": "unknown sort"
          }
         },
         "dir": {
          "value": {
           "error": "unknown dir"
          }
         },
         "pair": {
          "value": {
           "error": "a query filter lists pages"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  },
  "/api/cloudflare/connection": {
   "get": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "getCloudflareConnection",
    "summary": "Get the saved Cloudflare connection",
    "description": "Requires a Firebase ID token. The token itself is never returned. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Connection state.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CloudflareConnection"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   },
   "put": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "connectCloudflare",
    "summary": "Save a Cloudflare API token",
    "description": "Requires a Firebase ID token. The token needs Zone Read and Analytics Read. Cloudflare checks it first; it is stored encrypted and replaces any saved token. Sites that lost access resume syncing, and every verified site of the caller on a zone the token can read starts tracking. Not available to management keys.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "token": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{30,200}$"
         }
        },
        "required": [
         "token"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Connected.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CloudflareConnection"
        }
       }
      }
     },
     "400": {
      "description": "Cloudflare refused the token, or it can't see any zones or read analytics.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "format": {
          "value": {
           "error": "That doesn't look like a Cloudflare API token."
          }
         },
         "rejected": {
          "value": {
           "error": "Cloudflare rejected this token. Create one with Zone Read and Analytics Read."
          }
         },
         "noZones": {
          "value": {
           "error": "This token can't see any zones."
          }
         },
         "noAnalytics": {
          "value": {
           "error": "This token can list zones but can't read analytics. Add Analytics Read."
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "Cloudflare failed or timed out while checking the token. Nothing was saved; try again.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Could not reach Cloudflare. Please try again."
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   },
   "delete": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "disconnectCloudflare",
    "summary": "Remove the Cloudflare token",
    "description": "Requires a Firebase ID token. Unlinks every site that used the token and deletes their crawler data. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Disconnected.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "disconnected": {
           "type": "boolean",
           "const": true
          }
         },
         "required": [
          "disconnected"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "404": {
      "description": "No Cloudflare token is saved.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Cloudflare is not connected."
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/sites/{hostname}/crawlers/status": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "getCrawlerStatus",
    "summary": "Get the crawler tracking link and sync state",
    "description": "Requires the site owner's Firebase ID token or a management key. Before the site is linked, zone comes from the token's zone list, which is kept for up to 5 minutes or until the token is saved again.",
    "responses": {
     "200": {
      "description": "Link state.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CrawlerStatus"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/crawlers/link": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "post": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "linkCrawlers",
    "summary": "Start tracking crawlers from the site's Cloudflare zone",
    "description": "Requires the site owner Firebase ID bearer token and a saved Cloudflare token that can read the site's zone. Queues a backfill of the zone's retained analytics, up to 30 days. Re-linking restarts the backfill. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Linked; backfill queued.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CrawlerStatus"
        }
       }
      }
     },
     "400": {
      "description": "No Cloudflare token is saved, Cloudflare rejects it, it can't see a zone for this hostname or read its analytics, or analytics are off for the zone.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "notConnected": {
          "value": {
           "error": "Connect Cloudflare first."
          }
         },
         "noZone": {
          "value": {
           "error": "Your Cloudflare token can't see a zone for example.com."
          }
         },
         "analyticsOff": {
          "value": {
           "error": "Cloudflare analytics is turned off for example.com."
          }
         },
         "rejected": {
          "value": {
           "error": "Cloudflare rejected this token. Create one with Zone Read and Analytics Read."
          }
         },
         "noAnalytics": {
          "value": {
           "error": "This token can list zones but can't read analytics. Add Analytics Read."
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/VerificationRequired"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "Cloudflare failed or timed out while checking the zone. Nothing was linked; try again.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Could not reach Cloudflare. Please try again."
        }
       }
      }
     },
     "503": {
      "description": "The site was linked, but queue delivery failed. Retry with Sync now.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Linked, but sync could not start. Use Sync now to retry."
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   },
   "delete": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "unlinkCrawlers",
    "summary": "Stop tracking crawlers and delete the site's crawler data",
    "description": "Requires the site owner Firebase ID bearer token. Not available to management keys.",
    "responses": {
     "200": {
      "description": "Unlinked; crawler data deleted.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "unlinked": {
           "type": "boolean",
           "const": true
          }
         },
         "required": [
          "unlinked"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "404": {
      "description": "Site not found, or crawler tracking is not set up for this site.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Crawler tracking is not set up for this site."
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/sites/{hostname}/crawlers/sync": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "post": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "queueCrawlerSync",
    "summary": "Queue a crawler sync now",
    "description": "Requires the site owner's Firebase ID token or a management key. During the backfill this continues it from where it stopped.",
    "responses": {
     "202": {
      "description": "Sync queued.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "queued": {
           "type": "boolean",
           "const": true
          }
         },
         "required": [
          "queued"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "404": {
      "description": "Site not found, or crawler tracking is not set up for this site.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Crawler tracking is not set up for this site."
        }
       }
      }
     },
     "409": {
      "description": "Cloudflare no longer grants access to the zone; save a new token first.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          },
          "revoked": {
           "type": "boolean",
           "const": true
          }
         },
         "required": [
          "error",
          "revoked"
         ],
         "additionalProperties": false
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "503": {
      "description": "Sync could not be queued; retry later.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Sync queue unavailable. Please try again."
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/crawlers/overview": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "AI crawlers"
    ],
    "operationId": "getCrawlerOverview",
    "summary": "Get crawler hits for a date range",
    "description": "Requires the site owner's Firebase ID token or a management key. Hits are counted per UTC hour, and every hour that overlaps the range counts in full.",
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone"
     },
     {
      "$ref": "#/components/parameters/CrawlerFilter"
     }
    ],
    "responses": {
     "200": {
      "description": "Crawler totals, chart series and top bots, pages and statuses.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CrawlerOverview"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/ApiStatsBadRequest"
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "description": "Site not found, or crawler tracking is not set up for this site.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Crawler tracking is not set up for this site."
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  },
  "/api/sites/{hostname}/retention": {
   "parameters": [
    {
     "name": "hostname",
     "in": "path",
     "required": true,
     "description": "Use the normalized hostname returned at registration. Lookups do not lowercase path values.",
     "schema": {
      "type": "string"
     },
     "example": "demo.bitgate.dev"
    }
   ],
   "get": {
    "tags": [
     "Stats"
    ],
    "operationId": "getRetention",
    "summary": "Read returning visitors",
    "description": "Public verified site or owner. Counts browsers from the tracker's once-a-day return check, by UTC day. The requested range selects whole date buckets from from and to minus one second interpreted in tz; it does not clip partial days. Each browser counts once in totals and previous; cohorts group first visits by week (Monday) and follow them for up to 12 weeks. The last cohort value can belong to the current, unfinished week. Inaccessible sites return 404 even with an absent or invalid token. An unverified owner receives 403. Successful responses to anyone other than the owner may be edge-cached for 60 seconds; the owner's reads bypass this response cache. A management key is checked before the site lookup: an unknown or revoked key returns 401, a key without read returns 403.",
    "responses": {
     "200": {
      "description": "Returning-visitor totals, daily series and weekly cohorts.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/PublicStatsCache"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RetentionResponse"
        },
        "example": {
         "totals": {
          "visitors": 99,
          "new": 92,
          "returning": 7
         },
         "previous": {
          "visitors": 20,
          "new": 20,
          "returning": 0
         },
         "series": [
          {
           "day": "2026-10-01",
           "new": 10,
           "returning": 1
          }
         ],
         "cohorts": [
          {
           "week": "2026-09-28",
           "size": 20,
           "visitors": [
            20,
            5,
            3,
            1
           ]
          },
          {
           "week": "2026-10-05",
           "size": 30,
           "visitors": [
            30,
            12,
            4
           ]
          },
          {
           "week": "2026-10-12",
           "size": 40,
           "visitors": [
            40,
            5
           ]
          },
          {
           "week": "2026-10-19",
           "size": 12,
           "visitors": [
            12
           ]
          }
         ],
         "week": "2026-10-19",
         "curve": [
          {
           "week": 1,
           "retained_share": 0.34,
           "best_share": 0.4,
           "worst_share": 0.25,
           "cohorts": 2,
           "size": 50
          },
          {
           "week": 2,
           "retained_share": 0.15,
           "best_share": 0.15,
           "worst_share": 0.15,
           "cohorts": 1,
           "size": 20
          }
         ],
         "window": {
          "from": "2026-10-01",
          "to": "2026-10-21",
          "previous_from": "2026-09-10",
          "previous_to": "2026-09-30",
          "first_day": "2026-09-26"
         }
        }
       }
      }
     },
     "400": {
      "description": "The rounded range is non-finite, has from >= to, or exceeds 400 days.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "invalid range"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/ManagementKeyRejected"
     },
     "403": {
      "$ref": "#/components/responses/SiteReadForbidden"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {},
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone"
     }
    ]
   }
  },
  "/api/sites/{hostname}/goals": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Goals"
    ],
    "operationId": "listGoals",
    "summary": "List goals and funnels",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified. Ordered by creation time. Rows that no longer pass validation come back with `invalid: true` and are skipped by the stats routes.",
    "responses": {
     "200": {
      "description": "All goals and funnels, or an empty array.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/GoalsResponse"
        },
        "example": {
         "goals": [
          {
           "id": "3f6c2a9e-8b41-4d0f-9c1e-5a7b2d4e6f80",
           "name": "Pricing viewed",
           "kind": "goal",
           "steps": [
            {
             "match": "pageview",
             "filters": [
              {
               "key": "page",
               "op": "eq",
               "value": "/pricing"
              }
             ]
            }
           ],
           "created_at": "2026-09-30T10:00:00.000Z",
           "updated_at": "2026-09-30T10:00:00.000Z"
          },
          {
           "id": "9a1d4c7b-2e5f-4b8a-a3c6-0d9e8f7a6b51",
           "name": "Deploy",
           "kind": "funnel",
           "steps": [
            {
             "name": "Landing",
             "match": "pageview",
             "filters": [
              {
               "key": "page",
               "op": "eq",
               "value": "/"
              }
             ]
            },
            {
             "match": "event",
             "filters": [
              {
               "key": "event",
               "op": "eq",
               "value": "deploy_attempt"
              }
             ]
            },
            {
             "match": "event",
             "filters": [
              {
               "key": "event",
               "op": "eq",
               "value": "deploy_success"
              }
             ]
            }
           ],
           "created_at": "2026-09-30T10:00:00.000Z",
           "updated_at": "2026-09-30T10:00:00.000Z"
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the management key lacks the read scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope read required"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "post": {
    "tags": [
     "Goals"
    ],
    "operationId": "createGoal",
    "summary": "Add a goal or funnel",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified. At most 20 goals and funnels per site. Values are normalized before saving: trimmed, and page paths lose trailing slashes. Goals are computed at query time, so a new goal covers past data too.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/GoalInput"
       },
       "example": {
        "name": "Deploy",
        "kind": "funnel",
        "steps": [
         {
          "name": "Landing",
          "match": "pageview",
          "filters": [
           {
            "key": "page",
            "op": "eq",
            "value": "/"
           }
          ]
         },
         {
          "match": "event",
          "filters": [
           {
            "key": "event",
            "op": "eq",
            "value": "deploy_attempt"
           }
          ]
         },
         {
          "match": "event",
          "filters": [
           {
            "key": "event",
            "op": "eq",
            "value": "deploy_success"
           }
          ]
         }
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Goal created.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Goal"
        },
        "example": {
         "id": "9a1d4c7b-2e5f-4b8a-a3c6-0d9e8f7a6b51",
         "name": "Deploy",
         "kind": "funnel",
         "steps": [
          {
           "name": "Landing",
           "match": "pageview",
           "filters": [
            {
             "key": "page",
             "op": "eq",
             "value": "/"
            }
           ]
          },
          {
           "match": "event",
           "filters": [
            {
             "key": "event",
             "op": "eq",
             "value": "deploy_attempt"
            }
           ]
          },
          {
           "match": "event",
           "filters": [
            {
             "key": "event",
             "op": "eq",
             "value": "deploy_success"
            }
           ]
          }
         ],
         "created_at": "2026-09-30T10:00:00.000Z",
         "updated_at": "2026-09-30T10:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "The body is not a JSON object, a field fails validation (step and filter errors start with their position), or the goal's filters make its query too long to run.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "validation": {
          "value": {
           "error": "step 2 filter 1: event names only use letters, digits and _"
          }
         },
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "tooLarge": {
          "value": {
           "error": "this goal has too many filters to measure, remove some"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the management key lacks the manage scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope manage required"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "429": {
      "description": "The site already has 20 goals and funnels, or a management key sent changes too fast; wait the seconds in `Retry-After`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "limit": {
          "value": {
           "error": "too many goals for this site"
          }
         },
         "rateLimited": {
          "value": {
           "error": "rate limited, retry in 3s"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_goals",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/goals/stats": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "Goals"
    ],
    "operationId": "goalStats",
    "summary": "Conversion cards and funnel levels",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified. Every number counts identified, non-bot pageviews and events of visitor-days: a visitor id rotates at UTC midnight, so one person on two UTC days counts twice. Funnel steps must happen in order within that visitor-day, and one pageview can satisfy consecutive steps. Events sent from a server with `POST /events` get a visitor id built from the server's IP address, so they don't join the visitor's funnel or entry page. `coverage` counts all non-bot pageviews and how many carry a visitor id. The previous window is the range moved back by as many calendar days as it covers in `tz`, like the overview's `previous_range`; `previous_visitors` and every `previous` are null when under 99% of its pageviews are identified, or none are. Series are hits per hour for ranges up to 4 days, else per day, in `tz`, with empty buckets filled with zero.",
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone"
     },
     {
      "$ref": "#/components/parameters/Filter"
     }
    ],
    "responses": {
     "200": {
      "description": "Stats for every valid goal and funnel.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/GoalStatsResponse"
        },
        "example": {
         "granularity": "day",
         "visitors": 2093,
         "previous_visitors": 1874,
         "coverage": {
          "pageviews": 5120,
          "identified": 5120,
          "previous": {
           "pageviews": 4870,
           "identified": 4870
          }
         },
         "goals": [
          {
           "id": "3f6c2a9e-8b41-4d0f-9c1e-5a7b2d4e6f80",
           "name": "Pricing viewed",
           "kind": "goal",
           "conversions": 412,
           "converting_visitors": 338,
           "rate": 0.1615,
           "previous": {
            "conversions": 380,
            "converting_visitors": 301,
            "rate": 0.1606
           },
           "series": [
            {
             "t": 1790208000,
             "conversions": 14
            },
            {
             "t": 1790294400,
             "conversions": 0
            }
           ]
          }
         ],
         "funnels": [
          {
           "id": "9a1d4c7b-2e5f-4b8a-a3c6-0d9e8f7a6b51",
           "name": "Deploy",
           "kind": "funnel",
           "steps": [
            {
             "name": "Landing",
             "visitors": 2093
            },
            {
             "name": "Step 2",
             "visitors": 304
            },
            {
             "name": "Step 3",
             "visitors": 298
            }
           ],
           "previous": [
            1874,
            251,
            247
           ]
          }
         ],
         "invalid": []
        }
       }
      }
     },
     "400": {
      "description": "Invalid range or filter.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "invalid range"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the management key lacks the read scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope read required"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "504": {
      "description": "ClickHouse stopped the query at its 10 second limit.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "date range too large for goals, narrow it"
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_goals_stats",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/goals/{goalId}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "name": "goalId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "description": "Goal id returned at creation."
    }
   ],
   "get": {
    "tags": [
     "Goals"
    ],
    "operationId": "getGoal",
    "summary": "Get a goal or funnel",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified.",
    "responses": {
     "200": {
      "description": "The goal.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Goal"
        },
        "example": {
         "id": "3f6c2a9e-8b41-4d0f-9c1e-5a7b2d4e6f80",
         "name": "Pricing viewed",
         "kind": "goal",
         "steps": [
          {
           "match": "pageview",
           "filters": [
            {
             "key": "page",
             "op": "eq",
             "value": "/pricing"
            }
           ]
          }
         ],
         "created_at": "2026-09-30T10:00:00.000Z",
         "updated_at": "2026-09-30T10:00:00.000Z"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the management key lacks the read scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope read required"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No site with this hostname is registered to your account, or the site has no goal with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "goal": {
          "value": {
           "error": "unknown goal"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "put": {
    "tags": [
     "Goals"
    ],
    "operationId": "replaceGoal",
    "summary": "Replace a goal or funnel",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified. Replaces name, kind and steps. Stats are computed at query time, so the change applies to past data too.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/GoalInput"
       },
       "example": {
        "name": "Pricing viewed",
        "kind": "goal",
        "steps": [
         {
          "match": "pageview",
          "filters": [
           {
            "key": "page",
            "op": "eq",
            "value": "/pricing"
           }
          ]
         }
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Goal replaced.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Goal"
        },
        "example": {
         "id": "3f6c2a9e-8b41-4d0f-9c1e-5a7b2d4e6f80",
         "name": "Pricing viewed",
         "kind": "goal",
         "steps": [
          {
           "match": "pageview",
           "filters": [
            {
             "key": "page",
             "op": "eq",
             "value": "/pricing"
            }
           ]
          }
         ],
         "created_at": "2026-09-30T10:00:00.000Z",
         "updated_at": "2026-09-30T11:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "The body is not a JSON object, a field fails validation (step and filter errors start with their position), or the goal's filters make its query too long to run.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "validation": {
          "value": {
           "error": "step 1 filter 1: page must start with /"
          }
         },
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "tooLarge": {
          "value": {
           "error": "this goal has too many filters to measure, remove some"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the management key lacks the manage scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope manage required"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No site with this hostname is registered to your account, or the site has no goal with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "goal": {
          "value": {
           "error": "unknown goal"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "A management key sent changes too fast; wait the seconds in `Retry-After`.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "rateLimited": {
          "value": {
           "error": "rate limited, retry in 3s"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   },
   "delete": {
    "tags": [
     "Goals"
    ],
    "operationId": "deleteGoal",
    "summary": "Delete a goal or funnel",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified.",
    "responses": {
     "200": {
      "description": "Deleted.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/DeletedGoal"
        },
        "example": {
         "deleted": "3f6c2a9e-8b41-4d0f-9c1e-5a7b2d4e6f80"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the management key lacks the manage scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope manage required"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No site with this hostname is registered to your account, or the site has no goal with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "goal": {
          "value": {
           "error": "unknown goal"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "A management key sent changes too fast; wait the seconds in `Retry-After`.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "rateLimited": {
          "value": {
           "error": "rate limited, retry in 3s"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_goals_goalId",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/sites/{hostname}/goals/{goalId}/stats": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "name": "goalId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "description": "Goal id returned at creation."
    }
   ],
   "get": {
    "tags": [
     "Goals"
    ],
    "operationId": "goalDetail",
    "summary": "Goal breakdown and time to convert",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Sites you don't own return 404. The site must be verified. Splits visitor-days by the dimension value of their first pageview; self-referrals count as no referrer. `steps` counts how many of them reached each step. Rows are sorted by visitors, most first. `timing` is funnels only: seconds between consecutive steps, following each visitor-day's earliest path through the steps. There is no previous period and no `tz` parameter.",
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Filter"
     },
     {
      "name": "dim",
      "in": "query",
      "description": "Dimension of the entry pageview.",
      "schema": {
       "type": "string",
       "default": "referrers",
       "enum": [
        "pages",
        "referrers",
        "countries",
        "devices",
        "browsers",
        "os",
        "utm_sources",
        "utm_mediums",
        "utm_campaigns"
       ]
      },
      "example": "referrers"
     },
     {
      "name": "limit",
      "in": "query",
      "description": "Rows to return, clamped to 1-100; 0 or a value that is not a number returns 10.",
      "schema": {
       "type": "integer",
       "default": 10
      },
      "example": 10
     }
    ],
    "responses": {
     "200": {
      "description": "Breakdown and timing.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/GoalDetailResponse"
        },
        "example": {
         "dim": "referrers",
         "breakdown": [
          {
           "name": "Direct / none",
           "visitors": 1581,
           "steps": [
            1581,
            248,
            242
           ]
          },
          {
           "name": "chatgpt.com",
           "visitors": 153,
           "steps": [
            153,
            51,
            51
           ]
          },
          {
           "name": "google.com",
           "visitors": 75,
           "steps": [
            75,
            2,
            2
           ]
          }
         ],
         "timing": [
          {
           "p50_s": 24,
           "p90_s": 132,
           "n": 304
          },
          {
           "p50_s": 2.6,
           "p90_s": 7.9,
           "n": 298
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "Unknown dimension, invalid range or filter, or a stored goal that no longer passes validation.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "unknown dimension"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is not verified yet, or the management key lacks the read scope.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope read required"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No site with this hostname is registered to your account, or the site has no goal with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "goal": {
          "value": {
           "error": "unknown goal"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "504": {
      "description": "ClickHouse stopped the query at its 10 second limit.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "date range too large for goals, narrow it"
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_goals_goalId_stats",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/api/ingest": {
   "post": {
    "tags": [
     "API analytics"
    ],
    "operationId": "ingestApiRequests",
    "summary": "Send request metrics",
    "description": "Accepts a batch of per-minute request aggregates and sampled failed requests for the key's site. A management key with the ingest scope can send for any of the owner's API sites and verified websites; name the site in X-Totallytics-Site. Bodies over 4 MiB return 413. Invalid rows are skipped and counted in rejected; the rest is stored. Retry 408, 429, 5xx and network errors with the byte-identical body and batch_id, which is deduplicated. Only POST is accepted (405 POST required).",
    "security": [
     {
      "SiteApiKey": []
     },
     {
      "ManagementKey": [
       "ingest"
      ]
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/IngestBatch"
       },
       "example": {
        "v": 1,
        "batch_id": "b_3f9a2c71d04e5b6a",
        "sdk": "clicktag-js/0.3.0 hono",
        "metrics": [
         {
          "minute": 1790565420,
          "method": "GET",
          "route": "/users/:id",
          "status": 200,
          "user_agent": "python-requests/2.32.3",
          "consumer": "acct_4821",
          "count": 12,
          "duration_ms_sum": 845.2,
          "histogram": {
           "52": 3,
           "56": 7,
           "61": 2
          }
         }
        ],
        "errors": [
         {
          "ts": 1790565431482,
          "method": "POST",
          "route": "/v1/uploads",
          "path": "/v1/uploads",
          "status": 500,
          "duration_ms": 812.4,
          "user_agent": "python-requests/2.32.3",
          "consumer": "acct_4821",
          "message": "upstream timeout"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "202": {
      "description": "Batch queued for storage.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/IngestAccepted"
        },
        "example": {
         "accepted": {
          "metrics": 1,
          "errors": 1
         },
         "rejected": 0
        }
       }
      }
     },
     "400": {
      "description": "The body is not a valid batch, or a management key request has no valid X-Totallytics-Site header. Do not retry it.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "json": {
          "value": {
           "error": "invalid JSON"
          }
         },
         "object": {
          "value": {
           "error": "body must be a JSON object"
          }
         },
         "version": {
          "value": {
           "error": "unsupported wire version, expected v: 1"
          }
         },
         "batchId": {
          "value": {
           "error": "batch_id must be 8-64 characters of A-Z, a-z, 0-9, _ or -"
          }
         },
         "metrics": {
          "value": {
           "error": "metrics must be an array"
          }
         },
         "errors": {
          "value": {
           "error": "errors must be an array"
          }
         },
         "siteHeader": {
          "value": {
           "error": "X-Totallytics-Site header required"
          }
         },
         "site": {
          "value": {
           "error": "invalid site"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "The Authorization header is missing or not a tt_ or tt_mk_ key, or the key is unknown or revoked. Do not retry.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "missing": {
          "value": {
           "error": "missing or malformed API key"
          }
         },
         "revoked": {
          "value": {
           "error": "invalid or revoked API key"
          }
         },
         "managementKey": {
          "value": {
           "error": "invalid or revoked management key"
          }
         }
        }
       }
      }
     },
     "403": {
      "description": "Management key only: the key lacks the ingest scope, or the site is a website that is not verified yet.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "scope": {
          "value": {
           "error": "scope ingest required"
          }
         },
         "verification": {
          "value": {
           "error": "verify this site before sending API data"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "Management key only: X-Totallytics-Site names a site that is not registered to the key's account.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "unknown site"
        }
       }
      }
     },
     "413": {
      "description": "The body exceeds 4 MiB. Split it into smaller batches, each with a new batch_id.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "payload too large"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "503": {
      "description": "The batch could not be queued. Retry the identical body with the same batch_id.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "ingest temporarily unavailable, retry the same batch"
        }
       }
      }
     }
    },
    "parameters": [
     {
      "name": "X-Totallytics-Site",
      "in": "header",
      "required": false,
      "description": "Hostname of the site the batch belongs to. Required with a management key; ignored with a site API key.",
      "schema": {
       "type": "string"
      },
      "example": "api.example.com"
     }
    ]
   }
  },
  "/api/sites/{hostname}/keys": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "API analytics"
    ],
    "operationId": "listApiKeys",
    "summary": "List API keys",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Returns active keys only; secrets are never returned again.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "responses": {
     "200": {
      "description": "Active keys, newest first.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiKeysResponse"
        },
        "example": {
         "keys": [
          {
           "id": "0b6f3f7e-5d1c-4f5e-9a41-2f8c6d7e9b10",
           "label": "Production",
           "prefix": "tt_3f9a2c71",
           "created_at": "2026-09-26T14:02:11.482Z",
           "last_used_at": "2026-09-28T03:12:40.117Z",
           "created_with": null
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   },
   "post": {
    "tags": [
     "API analytics"
    ],
    "operationId": "createApiKey",
    "summary": "Create an API key",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. The body is optional. API sites (kind api) can create keys right after registration; websites must be verified first. At most 10 active keys per site.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateApiKey"
       },
       "example": {
        "label": "Production"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Key created. Store secret now; it is not returned again.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CreatedApiKey"
        },
        "example": {
         "key": {
          "id": "0b6f3f7e-5d1c-4f5e-9a41-2f8c6d7e9b10",
          "label": "Production",
          "prefix": "tt_3f9a2c71",
          "created_at": "2026-09-28T09:30:00.000Z",
          "last_used_at": null,
          "created_with": null
         },
         "secret": "tt_3f9a2c71d04e5b6a8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5"
        }
       }
      }
     },
     "400": {
      "description": "The body or label is invalid, or the site already has 10 active keys.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "body": {
          "value": {
           "error": "invalid request body"
          }
         },
         "label": {
          "value": {
           "error": "label must be text"
          }
         },
         "limit": {
          "value": {
           "error": "A site can have up to 10 active keys. Revoke one first."
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The site is a website (kind web) that is not verified yet, or the management key lacks the manage scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "verification": {
          "value": {
           "error": "Install your tracking script to verify this site first"
          }
         },
         "scope": {
          "value": {
           "error": "scope manage required"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/sites/{hostname}/keys/{keyId}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "$ref": "#/components/parameters/ApiKeyId"
    }
   ],
   "delete": {
    "tags": [
     "API analytics"
    ],
    "operationId": "revokeApiKey",
    "summary": "Revoke an API key",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Ingest rejects the key within a minute.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ],
    "responses": {
     "200": {
      "description": "Key revoked.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RevokedApiKey"
        },
        "example": {
         "revoked": "0b6f3f7e-5d1c-4f5e-9a41-2f8c6d7e9b10"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the manage scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "scope manage required"
        }
       }
      }
     },
     "404": {
      "description": "The site is not registered to this account, or the key does not exist on it or is already revoked.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "key": {
          "value": {
           "error": "unknown key"
          }
         }
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/sites/{hostname}/keys/{keyId}/rotate": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    },
    {
     "$ref": "#/components/parameters/ApiKeyId"
    }
   ],
   "post": {
    "tags": [
     "API analytics"
    ],
    "operationId": "rotateApiKey",
    "summary": "Rotate an API key",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Creates a new key with the same label and revokes the old one in one transaction. Ingest rejects the old key within a minute; deploy the new secret right away. The limit of 10 active keys does not apply, because the count stays the same.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ],
    "responses": {
     "201": {
      "description": "New key created and the old key revoked. Store secret now; it is not returned again.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RotatedApiKey"
        },
        "example": {
         "key": {
          "id": "7c1e9a52-3b4d-4f6e-8a90-1b2c3d4e5f60",
          "label": "Production",
          "prefix": "tt_9d4e7a10",
          "created_at": "2026-10-04T09:41:12.004Z",
          "last_used_at": null,
          "created_with": {
           "id": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10",
           "label": "CI deploy"
          }
         },
         "secret": "tt_9d4e7a10c2b3a4f5e6d7c8b9a0f1e2d3c4b5a6f7e8d9c0b1",
         "revoked": "0b6f3f7e-5d1c-4f5e-9a41-2f8c6d7e9b10"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the manage scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "scope manage required"
        }
       }
      }
     },
     "404": {
      "description": "The site is not registered to this account, or the key does not exist on it or is already revoked.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "site": {
          "value": {
           "error": "unknown site"
          }
         },
         "key": {
          "value": {
           "error": "unknown key"
          }
         }
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/sites/{hostname}/requests/overview": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "API analytics"
    ],
    "operationId": "getApiRequestsOverview",
    "summary": "Read API request totals and series",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Only counts requests timestamped after the site's created_at. Range edges more than 34 days back round to whole hours.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone"
     },
     {
      "$ref": "#/components/parameters/ApiFilter"
     }
    ],
    "responses": {
     "200": {
      "description": "Totals, previous period and series.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiOverviewResponse"
        },
        "example": {
         "totals": {
          "requests": 48210,
          "client_errors": 1204,
          "server_errors": 96,
          "avg_ms": 38.4,
          "p50_ms": 21.6,
          "p95_ms": 142,
          "p99_ms": 388
         },
         "previous": {
          "requests": 45877,
          "client_errors": 1310,
          "server_errors": 141,
          "avg_ms": 41.2,
          "p50_ms": 22.3,
          "p95_ms": 157,
          "p99_ms": 420
         },
         "series": [
          {
           "t": 1790460000,
           "s2xx": 1980,
           "s3xx": 12,
           "s4xx": 51,
           "s5xx": 4,
           "p50_ms": 21.1,
           "p95_ms": 139
          }
         ],
         "granularity": "day",
         "has_data": true
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/ApiStatsBadRequest"
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/sites/{hostname}/requests/breakdown": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "API analytics"
    ],
    "operationId": "getApiRequestsBreakdown",
    "summary": "Break down API requests",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Only counts requests timestamped after the site's created_at. Range edges more than 34 days back round to whole hours.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/ApiDimension"
     },
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/ApiFilter"
     },
     {
      "$ref": "#/components/parameters/ApiRowsLimit"
     }
    ],
    "responses": {
     "200": {
      "description": "Rows for the dimension.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiBreakdownResponse"
        },
        "example": {
         "rows": [
          {
           "name": "GET /users/:id",
           "method": "GET",
           "route": "/users/:id",
           "requests": 20412,
           "client_errors": 311,
           "server_errors": 12,
           "p50_ms": 18.9,
           "p95_ms": 121,
           "p99_ms": 344
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "The range, a filter or dim is invalid.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "range": {
          "value": {
           "error": "invalid range"
          }
         },
         "filter": {
          "value": {
           "error": "invalid filter"
          }
         },
         "dim": {
          "value": {
           "error": "dim must be endpoints, statuses, clients or consumers"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/sites/{hostname}/requests/errors": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "API analytics"
    ],
    "operationId": "getApiRequestErrors",
    "summary": "Group API errors per endpoint",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. One row per status, method and route with at least one 4xx or 5xx response. The error rate is requests / endpoint_requests; both come from metric rows, not from error samples. Only counts requests timestamped after the site's created_at. Range edges more than 34 days back round to whole hours.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/ApiFilter"
     },
     {
      "$ref": "#/components/parameters/ApiRowsLimit"
     }
    ],
    "responses": {
     "200": {
      "description": "Error groups.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiErrorsResponse"
        },
        "example": {
         "rows": [
          {
           "status": 500,
           "method": "POST",
           "route": "/v1/uploads",
           "requests": 42,
           "endpoint_requests": 1985,
           "samples": 17,
           "last_seen": 1790562310
          }
         ]
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/ApiStatsBadRequest"
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/sites/{hostname}/requests/samples": {
   "parameters": [
    {
     "$ref": "#/components/parameters/Hostname"
    }
   ],
   "get": {
    "tags": [
     "API analytics"
    ],
    "operationId": "getApiRequestSamples",
    "summary": "Read sampled failed API requests",
    "description": "Requires the site owner's Firebase ID token or a management key, even when the site is public. Authentication is checked before site lookup. Latest individual 4xx and 5xx requests, newest first. Samples are kept for 14 days. Filter by status and endpoint for the samples behind an error group. Only counts requests timestamped after the site's created_at. Range edges more than 34 days back round to whole hours.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/ApiFilter"
     },
     {
      "$ref": "#/components/parameters/ApiSamplesLimit"
     }
    ],
    "responses": {
     "200": {
      "description": "Samples.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ApiSamplesResponse"
        },
        "example": {
         "rows": [
          {
           "ts": 1790562310482,
           "method": "POST",
           "route": "/v1/uploads",
           "path": "/v1/uploads",
           "status": 500,
           "duration_ms": 812.4,
           "client": "python-requests",
           "client_version": "2.32",
           "consumer": "acct_4821",
           "message": "upstream timeout"
          }
         ]
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/ApiStatsBadRequest"
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/sites/{hostname}/verify": {
   "post": {
    "tags": [
     "Sites"
    ],
    "operationId": "verifySite",
    "summary": "Verify domain ownership",
    "description": "Owner only. No request body. Accepts a matching site-specific tracking tag, a DNS TXT record at _totallytics-verify.<hostname> containing totallytics-verify=<verify_token>, or /.well-known/totallytics-verify.txt containing the token. Any passing method verifies the site. Already verified sites return already: true without running the checks. Each hostname gets one check about every 10 seconds.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/Hostname"
     }
    ],
    "responses": {
     "200": {
      "description": "Verify domain ownership.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SiteVerification"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "409": {
      "description": "The registration changed during verification; reload before retrying.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "Site registration changed. Reload and try again."
        }
       }
      }
     },
     "422": {
      "description": "None of the ownership checks passed yet; inspect each method detail.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SiteVerificationPending"
        }
       }
      }
     },
     "429": {
      "description": "This hostname was checked less than 10 seconds ago, or a management key sent changes too fast; wait the seconds in `Retry-After`.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       },
       "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
         "type": "integer",
         "minimum": 1
        },
        "example": 10
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "hostname": {
          "value": {
           "error": "rate limited, retry in 10s"
          }
         },
         "rateLimited": {
          "value": {
           "error": "rate limited, retry in 1s"
          }
         }
        }
       }
      }
     },
     "500": {
      "description": "The verification token is missing or an internal error occurred.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "missingToken": {
          "value": {
           "error": "verification token missing"
          }
         },
         "internal": {
          "value": {
           "error": "internal error"
          }
         }
        }
       }
      }
     }
    }
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_verify",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": [],
    "parameters": [
     {
      "$ref": "#/components/parameters/Hostname"
     }
    ]
   }
  },
  "/api/sites/{hostname}/setup-check": {
   "get": {
    "tags": [
     "Sites"
    ],
    "operationId": "checkSiteSetup",
    "summary": "Check the tracking tag and CSP",
    "description": "Owner-only origin-page diagnostics. Checks reachability, tracking-tag presence and Content-Security-Policy permissions for scripts, beacons and pixels. Does not perform DNS/file verification, verify the site, or confirm that analytics events arrived. Origin fetch failures return 200 with reachable: false and an error detail. Results are shared for 60 seconds and refresh=true skips them. Each hostname's page is fetched at most about once every 10 seconds; a call that needs a fetch sooner gets 429.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "$ref": "#/components/parameters/Hostname"
     },
     {
      "name": "refresh",
      "in": "query",
      "required": false,
      "schema": {
       "type": "boolean",
       "default": false
      },
      "description": "Fetch the page again instead of returning the shared result from the last 60 seconds."
     }
    ],
    "responses": {
     "200": {
      "description": "Check the tracking tag and CSP.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SetupCheck"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "404": {
      "$ref": "#/components/responses/UnknownOwnedSite"
     },
     "429": {
      "description": "This hostname's page was fetched less than 10 seconds ago and the call needs a fresh fetch; wait the seconds in `Retry-After`.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       },
       "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
         "type": "integer",
         "minimum": 1
        },
        "example": 10
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "rateLimited": {
          "value": {
           "error": "rate limited, retry in 10s"
          }
         }
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_api_sites_hostname_setup_check",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": [],
    "parameters": [
     {
      "$ref": "#/components/parameters/Hostname"
     }
    ]
   }
  },
  "/r": {
   "get": {
    "tags": [
     "Collector"
    ],
    "operationId": "collectReturnVisit",
    "summary": "Record a browser return check",
    "description": "Return check the browser script sends with the first pageview of a page load, for retention. Stores one visitor-day row in the background when the browser's state shows a new UTC day and the visitor hash (UTC day, IP, user agent, hostname) has not checked in that day; the same registration and verification gate as other hits applies, and unregistered hostnames still get the GIF. If-Modified-Since from the current UTC day returns 304. Missing or invalid h or a bot User-Agent returns 204. Do Not Track headers and the bot flag are not checked. Registry lookup failures fail closed for storage but do not change the successful response. A GIF response is not durable-storage confirmation.",
    "security": [],
    "parameters": [
     {
      "name": "h",
      "in": "query",
      "description": "Hostname, normalized the way site registration does. Missing, empty or invalid returns 204.",
      "schema": {
       "type": "string"
      },
      "example": "example.com"
     },
     {
      "name": "If-Modified-Since",
      "in": "header",
      "description": "Last-Modified value from the preceding return check. It encodes the first and last visit days; missing or unreadable values count as a first visit.",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A 1x1 GIF. Also returned without storing a row when the visitor already checked in today, for example from another tab, and for unregistered hostnames.",
      "headers": {
       "Access-Control-Allow-Origin": {
        "$ref": "#/components/headers/AllowOrigin"
       },
       "Access-Control-Allow-Methods": {
        "$ref": "#/components/headers/AllowMethods"
       },
       "Access-Control-Allow-Headers": {
        "$ref": "#/components/headers/AllowHeaders"
       },
       "Access-Control-Max-Age": {
        "$ref": "#/components/headers/PreflightMaxAge"
       },
       "Cache-Control": {
        "description": "Private browser cache until the next UTC midnight; max-age is the remaining seconds, 1 to 86400.",
        "schema": {
         "type": "string"
        },
        "example": "private, max-age=86400"
       },
       "Last-Modified": {
        "description": "Browser-held return-check state encoding the first and last visit days, not a modification time; send it back as If-Modified-Since on later checks.",
        "schema": {
         "type": "string"
        }
       }
      },
      "content": {
       "image/gif": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "204": {
      "description": "No valid hostname or a bot user agent; no body.",
      "headers": {
       "Access-Control-Allow-Origin": {
        "$ref": "#/components/headers/AllowOrigin"
       },
       "Access-Control-Allow-Methods": {
        "$ref": "#/components/headers/AllowMethods"
       },
       "Access-Control-Allow-Headers": {
        "$ref": "#/components/headers/AllowHeaders"
       },
       "Access-Control-Max-Age": {
        "$ref": "#/components/headers/PreflightMaxAge"
       },
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      }
     },
     "304": {
      "description": "If-Modified-Since is from the current UTC day, so nothing is stored; no body.",
      "headers": {
       "Access-Control-Allow-Origin": {
        "$ref": "#/components/headers/AllowOrigin"
       },
       "Access-Control-Allow-Methods": {
        "$ref": "#/components/headers/AllowMethods"
       },
       "Access-Control-Allow-Headers": {
        "$ref": "#/components/headers/AllowHeaders"
       },
       "Access-Control-Max-Age": {
        "$ref": "#/components/headers/PreflightMaxAge"
       },
       "Cache-Control": {
        "description": "Private browser cache until the next UTC midnight; max-age is the remaining seconds, 1 to 86400.",
        "schema": {
         "type": "string"
        },
        "example": "private, max-age=86400"
       },
       "Last-Modified": {
        "description": "Browser-held return-check state encoding the first and last visit days, not a modification time; send it back as If-Modified-Since on later checks.",
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/CollectorBadRequest"
     }
    }
   },
   "options": {
    "tags": [
     "Collector"
    ],
    "operationId": "preflight_r",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": []
   }
  },
  "/favicons/{hostname}": {
   "get": {
    "tags": [
     "Sites"
    ],
    "operationId": "getSiteFavicon",
    "summary": "Read a cached site icon",
    "description": "Public image asset for a registered hostname. Returns a cached icon or discovers one on demand. No authentication or CORS headers. Unregistered and invalid hostnames or unavailable icons return an empty 404 response.",
    "security": [],
    "parameters": [
     {
      "$ref": "#/components/parameters/Hostname"
     }
    ],
    "responses": {
     "200": {
      "description": "Icon bytes with their stored image content type.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string",
         "const": "public, max-age=21600"
        }
       },
       "ETag": {
        "schema": {
         "type": "string"
        }
       },
       "Content-Security-Policy": {
        "schema": {
         "type": "string",
         "const": "default-src 'none'; img-src data:; style-src 'unsafe-inline'"
        }
       },
       "X-Content-Type-Options": {
        "schema": {
         "type": "string",
         "const": "nosniff"
        }
       }
      },
      "content": {
       "image/*": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       },
       "application/octet-stream": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "404": {
      "description": "Icon unavailable; empty body. Invalid/unknown hostnames and missing stored bytes are cached for 60 seconds; cached or fresh discovery failures for 300 seconds.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string",
         "enum": [
          "public, max-age=60",
          "public, max-age=300"
         ]
        }
       }
      }
     }
    }
   },
   "options": {
    "tags": [
     "Health"
    ],
    "operationId": "preflight_favicons_hostname",
    "summary": "Handle a preflight request",
    "description": "The global OPTIONS handler responds independently of route authorization. Advertised methods are GET, POST, OPTIONS and the only allowed request header is Content-Type. This does not enable authenticated cross-origin product API calls; actual product API and health responses have no CORS headers.",
    "responses": {
     "204": {
      "$ref": "#/components/responses/Preflight"
     }
    },
    "security": [],
    "parameters": [
     {
      "$ref": "#/components/parameters/Hostname"
     }
    ]
   }
  },
  "/api/reports/defaults": {
   "get": {
    "tags": [
     "Reports"
    ],
    "operationId": "getReportDefaults",
    "summary": "Read workspace report defaults",
    "description": "Owner only, by Firebase ID token or management key; covers every site of the owner. Sites on inherit send these settings, sites on custom send their own recipients, sites on off send nothing.",
    "responses": {
     "200": {
      "description": "Workspace defaults and how the owner's sites use them.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ReportDefaults"
        },
        "example": {
         "configured": true,
         "enabled": true,
         "recipients": [
          "you@company.com",
          "teammate@company.com"
         ],
         "frequency": "weekly",
         "timezone": "Europe/Amsterdam",
         "send_hour": 9,
         "sections": {
          "overview": true,
          "pages": true,
          "referrers": true,
          "countries": false,
          "events": false,
          "alerts": true,
          "api": true
         },
         "sites": {
          "inherit": 7,
          "custom": [
           "shop.company.com"
          ],
          "off": [
           "old.company.com"
          ]
         },
         "stalled": [],
         "dropped": [
          {
           "email": "alice@company.com",
           "reason": "unsubscribed",
           "at": "2026-10-03T07:12:00.000Z"
          }
         ],
         "updated_at": "2026-10-01T18:00:00.000Z"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "put": {
    "tags": [
     "Reports"
    ],
    "operationId": "updateReportDefaults",
    "summary": "Save workspace report defaults",
    "description": "Owner only, by Firebase ID token or management key. Saves the fields sent, clears the dropped list, resets failure counts on default and digest recipients and creates or removes the default rows for every site of the owner. Inheriting verified sites send to the new list from the next matching hour.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/UpdateReportDefaults"
       },
       "example": {
        "enabled": true,
        "recipients": [
         "you@company.com",
         "teammate@company.com"
        ],
        "frequency": "weekly",
        "timezone": "Europe/Amsterdam",
        "send_hour": 9
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Saved defaults, same shape as the read.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ReportDefaults"
        },
        "example": {
         "configured": true,
         "enabled": true,
         "recipients": [
          "you@company.com",
          "teammate@company.com"
         ],
         "frequency": "weekly",
         "timezone": "Europe/Amsterdam",
         "send_hour": 9,
         "sections": {
          "overview": true,
          "pages": true,
          "referrers": true,
          "countries": false,
          "events": false,
          "alerts": true,
          "api": true
         },
         "sites": {
          "inherit": 7,
          "custom": [
           "shop.company.com"
          ],
          "off": [
           "old.company.com"
          ]
         },
         "stalled": [],
         "dropped": [],
         "updated_at": "2026-10-01T18:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "Validation failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "enabled": {
          "value": {
           "error": "enabled must be a boolean"
          }
         },
         "recipients": {
          "value": {
           "error": "recipients must be a list of email addresses"
          }
         },
         "email": {
          "value": {
           "error": "invalid email address: not-an-email"
          }
         },
         "cap": {
          "value": {
           "error": "too many recipients (max 10)"
          }
         },
         "frequency": {
          "value": {
           "error": "frequency must be daily, weekly, or monthly"
          }
         },
         "timezone": {
          "value": {
           "error": "invalid timezone"
          }
         },
         "sendHour": {
          "value": {
           "error": "send_hour must be an integer between 0 and 23"
          }
         },
         "sections": {
          "value": {
           "error": "invalid sections"
          }
         },
         "updatedAt": {
          "value": {
           "error": "invalid updated_at"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "409": {
      "description": "The defaults changed since the updated_at sent with the save.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "these settings changed since you opened them; reload and try again"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/workspace/overview": {
   "get": {
    "tags": [
     "Stats"
    ],
    "operationId": "getWorkspaceOverview",
    "summary": "Add up every site you own",
    "description": "Owner-only report across all your sites. Traffic counts every verified website exactly like its own overview, so totals equal the sum of the per-site overviews; unverified websites only appear in counts. previous covers the same clock times, moved back by as many calendar days as the range covers in tz. API traffic covers every site with API requests, API sites included. An account without verified websites or API sites gets zeros and empty arrays.",
    "parameters": [
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone"
     }
    ],
    "responses": {
     "200": {
      "description": "Workspace totals, chart series and per-site rows.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkspaceOverviewResponse"
        },
        "example": {
         "range": {
          "from": 1789509600,
          "to": 1789682400,
          "granularity": "hour"
         },
         "counts": {
          "web": 2,
          "api": 1,
          "unverified": 0
         },
         "totals": {
          "visitors": 9,
          "pageviews": 14,
          "live": 1,
          "active_sites": 2
         },
         "previous": {
          "visitors": 6,
          "pageviews": 8,
          "active_sites": 1
         },
         "buckets": [
          1789509600,
          1789513200
         ],
         "spark": {
          "visitors": [
           5,
           4
          ],
          "pageviews": [
           9,
           5
          ],
          "active_sites": [
           2,
           1
          ]
         },
         "series": [
          {
           "key": "all",
           "label": "All sites",
           "visitors": [
            5,
            4
           ],
           "pageviews": [
            9,
            5
           ]
          },
          {
           "key": "your-domain.example",
           "label": "Your site",
           "visitors": [
            4,
            4
           ],
           "pageviews": [
            7,
            5
           ]
          },
          {
           "key": "blog.your-domain.example",
           "label": "blog.your-domain.example",
           "visitors": [
            1,
            0
           ],
           "pageviews": [
            2,
            0
           ]
          }
         ],
         "sites": [
          {
           "hostname": "your-domain.example",
           "display_name": "Your site",
           "pinned": true,
           "visitors": 8,
           "pageviews": 12,
           "previous": {
            "visitors": 6,
            "pageviews": 8
           },
           "live": 1
          },
          {
           "hostname": "blog.your-domain.example",
           "display_name": "blog.your-domain.example",
           "pinned": false,
           "visitors": 1,
           "pageviews": 2,
           "previous": {
            "visitors": 0,
            "pageviews": 0
           },
           "live": 0
          }
         ],
         "api": [
          {
           "hostname": "api.your-domain.example",
           "display_name": "api.your-domain.example",
           "requests": 18240,
           "server_errors": 12,
           "previous": {
            "requests": 16410,
            "server_errors": 3
           }
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "Invalid range.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "range": {
          "value": {
           "error": "invalid range"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  },
  "/api/workspace/breakdown": {
   "get": {
    "tags": [
     "Stats"
    ],
    "operationId": "getWorkspaceBreakdown",
    "summary": "Break down every site you own",
    "description": "Owner-only ranking across your verified websites. pages and events rank per site; referrers and countries add every site up and say how many sites a row was seen on; goals list every valid goal and funnel with current-window conversions and no previous period; sites returns the overview's sites rows. pages, referrers, countries and events rows are sorted by value descending, then name; goals by converting_visitors descending, then name; sites rows like the overview: pinned first, then visitors descending, then hostname. tz only sets the previous period of sites rows. An account without verified websites gets rows: [].",
    "parameters": [
     {
      "name": "dim",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "enum": [
        "sites",
        "pages",
        "referrers",
        "countries",
        "events",
        "goals"
       ]
      },
      "example": "pages"
     },
     {
      "$ref": "#/components/parameters/From"
     },
     {
      "$ref": "#/components/parameters/To"
     },
     {
      "$ref": "#/components/parameters/Timezone",
      "description": "Only sets the previous period of sites rows. Defaults UTC; invalid or unknown zones fall back to UTC."
     },
     {
      "name": "limit",
      "in": "query",
      "description": "Default 25; numeric input is clamped to 1-100 and fractions round down. Zero or non-numeric input uses 25.",
      "schema": {
       "type": "integer",
       "default": 25
      },
      "example": 25
     }
    ],
    "responses": {
     "200": {
      "description": "Ranked rows for the dimension.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkspaceBreakdownResponse"
        },
        "example": {
         "dim": "referrers",
         "rows": [
          {
           "name": "Direct / none",
           "value": 41,
           "visitors": 30,
           "sites": 3
          },
          {
           "name": "google.com",
           "value": 18,
           "visitors": 15,
           "sites": 2
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "Unknown dimension or invalid range.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "dimension": {
          "value": {
           "error": "unknown dimension"
          }
         },
         "range": {
          "value": {
           "error": "invalid range"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  },
  "/api/live/ticket": {
   "post": {
    "tags": [
     "Live"
    ],
    "operationId": "createLiveTicket",
    "summary": "Get a live stream ticket",
    "description": "Signed-in owners only: requires a Firebase ID token, and management keys get 401. Returns a single-use ticket for GET /api/live/ws that expires after 60 seconds. With site, the stream carries that verified website's hits; without it, every verified website you own. A missing, empty or non-string site, or a body that is not JSON, asks for every website. An account can hold at most 32 unexpired, unused tickets.",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LiveTicketRequest"
       },
       "example": {
        "site": "your-domain.example"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Ticket issued.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/LiveTicket"
        },
        "example": {
         "ticket": "owner-1.9f1c2e8a4b7d6c5e0a1b2c3d4e5f6071",
         "expires_in": 60,
         "url": "/api/live/ws?ticket=owner-1.9f1c2e8a4b7d6c5e0a1b2c3d4e5f6071"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/LiveSignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/VerificationRequired"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "429": {
      "description": "The account already holds 32 unexpired, unused tickets.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "too many live tickets"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/live/ws": {
   "get": {
    "tags": [
     "Live"
    ],
    "operationId": "openLiveStream",
    "summary": "Open the live WebSocket",
    "description": "WebSocket upgrade for the live stream, authorized only by the ticket from POST /api/live/ticket; no Authorization header is read, and a ticket opens one stream at most. Connect to wss://clicktag.io followed by the url from the ticket response. The server sends JSON text frames: `hello` once, with `scope` (site or all), `site` (hostname or null), `since` (Unix milliseconds) and `sockets` (open streams on the account, this one included); `hits` as hits arrive, with `hits` (LiveHit objects) and `dropped`; and `bye` with `reason` too_many_tabs, sent to the oldest stream before it closes with code 4001 when a ninth stream opens on the account. The first hit goes out at once and hits arriving within the next 50 ms go out together, split over frames under 64 KB; when more than 200 hits wait for one send, the oldest are left out and the first frame's `dropped` counts the ones this stream would have received (otherwise 0); that frame can have empty `hits`. Send the text ping to get pong; a message over 1024 bytes closes the socket with code 1009. A data center that saw nobody watching a site stays quiet about it for 30 seconds, so hits can be missing right after connecting; GET /api/live/recent fills the gap.",
    "parameters": [
     {
      "name": "ticket",
      "in": "query",
      "required": true,
      "description": "Ticket from POST /api/live/ticket.",
      "schema": {
       "type": "string"
      },
      "example": "owner-1.9f1c2e8a4b7d6c5e0a1b2c3d4e5f6071"
     },
     {
      "name": "Upgrade",
      "in": "header",
      "required": true,
      "description": "websocket, in any letter case.",
      "schema": {
       "type": "string"
      },
      "example": "websocket"
     }
    ],
    "responses": {
     "101": {
      "description": "Switching Protocols: the stream is open and starts with a hello frame."
     },
     "400": {
      "description": "The ticket is missing or not shaped like a ticket.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "invalid live ticket"
        }
       }
      }
     },
     "403": {
      "description": "The ticket is unknown, expired or already used.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "invalid live ticket"
        }
       }
      }
     },
     "426": {
      "description": "The request is not a WebSocket upgrade.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "websocket upgrade required"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": []
   }
  },
  "/api/live/recent": {
   "get": {
    "tags": [
     "Live"
    ],
    "operationId": "listRecentLiveHits",
    "summary": "List recent live hits",
    "description": "Signed-in owners only: requires a Firebase ID token, and management keys get 401. Returns stored human hits with since < ts <= until, newest first; a hit id repeated within one site and type counts once, at its first copy. Without site, hits from every verified website you own; an account without verified websites gets an empty hits array. History hits have me false and no loc.",
    "parameters": [
     {
      "name": "site",
      "in": "query",
      "description": "Hostname of a verified website you own, exactly as registered. Without it, hits from every verified website you own.",
      "schema": {
       "type": "string"
      },
      "example": "your-domain.example"
     },
     {
      "name": "since",
      "in": "query",
      "description": "Exclusive lower bound in Unix milliseconds. Defaults to 5 minutes ago; anything earlier than 15 minutes ago is raised to 15 minutes ago. Fractions round down; empty or non-numeric input uses the default.",
      "schema": {
       "type": "integer",
       "format": "int64"
      },
      "example": 1789520100000
     },
     {
      "name": "limit",
      "in": "query",
      "description": "Most hits to return. Default 200 with site and 100 without; numeric input is clamped to 1-500 and fractions round down. Zero or non-numeric input uses the default.",
      "schema": {
       "type": "integer"
      },
      "example": 100
     }
    ],
    "responses": {
     "200": {
      "description": "Recent hits.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/LiveRecentResponse"
        },
        "example": {
         "since": 1789520100000,
         "until": 1789520400000,
         "hits": [
          {
           "site": "your-domain.example",
           "ts": 1789520391250,
           "type": "pageview",
           "id": "a1b2c3d4e5",
           "orig": "",
           "path": "/pricing",
           "query": "",
           "ref": "news.example",
           "event": "",
           "meta": "",
           "error": "",
           "dur": null,
           "scroll": null,
           "country": "NL",
           "device": "desktop",
           "browser": "Firefox",
           "os": "Linux",
           "unique": 1,
           "visitor": "3f9a1c0e",
           "me": false
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/LiveSignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/VerificationRequired"
     },
     "404": {
      "$ref": "#/components/responses/UnknownSite"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/me": {
   "get": {
    "tags": [
     "Management keys"
    ],
    "operationId": "getMe",
    "summary": "Check the current credential",
    "description": "Works with a Firebase ID token or any management key, whatever its scopes. Returns who the credential belongs to and how many sites the account has; use it to check a key before a deploy.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The authenticated principal and the account's site count.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Me"
        },
        "example": {
         "principal": {
          "uid": "Xb3kQ9rT2mPz8vLw",
          "via": "management_key",
          "key_id": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10",
          "label": "CI deploy",
          "scopes": [
           "read",
           "manage"
          ]
         },
         "sites": 4
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/management-keys": {
   "get": {
    "tags": [
     "Management keys"
    ],
    "operationId": "listManagementKeys",
    "summary": "List management keys",
    "description": "Requires a Firebase ID token or a management key. Returns the account's active management keys; secrets are never returned again.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "responses": {
     "200": {
      "description": "Active management keys, newest first.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ManagementKeysResponse"
        },
        "example": {
         "keys": [
          {
           "id": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10",
           "label": "CI deploy",
           "prefix": "tt_mk_4e8b1d27",
           "scopes": [
            "read",
            "manage"
           ],
           "created_at": "2026-10-04T09:30:00.000Z",
           "last_used_at": "2026-10-04T11:02:45.120Z"
          }
         ]
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the read scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "scope read required"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   },
   "post": {
    "tags": [
     "Management keys"
    ],
    "operationId": "createManagementKey",
    "summary": "Create a management key",
    "description": "Requires a Firebase ID token. Pick at least one scope. At most 10 active management keys per account. Not available to management keys.",
    "security": [
     {
      "FirebaseIdToken": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateManagementKey"
       },
       "example": {
        "label": "CI deploy",
        "scopes": [
         "read",
         "manage"
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Key created. Store secret now; it is not returned again.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CreatedManagementKey"
        },
        "example": {
         "key": {
          "id": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10",
          "label": "CI deploy",
          "prefix": "tt_mk_4e8b1d27",
          "scopes": [
           "read",
           "manage"
          ],
          "created_at": "2026-10-04T09:30:00.000Z",
          "last_used_at": null
         },
         "secret": "tt_mk_4e8b1d27a9c03f5e6b7d8c9a0e1f2b3c4d5e6f7a8b9c0d1e"
        }
       }
      }
     },
     "400": {
      "description": "The body, label or scopes are invalid.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "body": {
          "value": {
           "error": "invalid request body"
          }
         },
         "label": {
          "value": {
           "error": "label must be text"
          }
         },
         "scopes": {
          "value": {
           "error": "scopes must include at least one of read, manage, ingest"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "Management keys cannot call this endpoint; sign in instead.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "management keys cannot call this endpoint"
        }
       }
      }
     },
     "409": {
      "description": "The account already has 10 active management keys.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "You can have up to 10 active management keys. Revoke one first."
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/management-keys/self": {
   "delete": {
    "tags": [
     "Management keys"
    ],
    "operationId": "revokeCurrentManagementKey",
    "summary": "Revoke the calling management key",
    "description": "Works with any management key, whatever its scopes, and revokes the key that makes the request. Requests with it fail within about a minute. Site keys it created keep working. A Firebase ID token gets 400.",
    "security": [
     {
      "ManagementKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Key revoked.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RevokedApiKey"
        },
        "example": {
         "revoked": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10"
        }
       }
      }
     },
     "400": {
      "description": "The request used a Firebase ID token.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "self is only valid with a management key"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/management-keys/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Management key id.",
     "schema": {
      "type": "string"
     },
     "example": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10"
    }
   ],
   "delete": {
    "tags": [
     "Management keys"
    ],
    "operationId": "revokeManagementKey",
    "summary": "Revoke a management key",
    "description": "Requires a Firebase ID token or a management key with manage. A key can also revoke itself by its own id without manage, like DELETE /api/management-keys/self. Requests with a revoked key fail within about a minute. Site keys it created keep working.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ],
    "responses": {
     "200": {
      "description": "Key revoked.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RevokedApiKey"
        },
        "example": {
         "revoked": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10"
        }
       }
      }
     },
     "400": {
      "description": "self was used with a Firebase ID token.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "self is only valid with a management key"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the manage scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "scope manage required"
        }
       }
      }
     },
     "404": {
      "description": "No active management key with this id on the account.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "unknown key"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/keys": {
   "get": {
    "tags": [
     "Management keys"
    ],
    "operationId": "listOwnerSiteKeys",
    "summary": "List site API keys across sites",
    "description": "Requires a Firebase ID token or a management key. Returns every active site API key of the account with its site and, when a management key minted it, that key's id and label. Secrets are never returned.",
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ],
    "parameters": [
     {
      "name": "site",
      "in": "query",
      "required": false,
      "description": "Only return keys of this hostname.",
      "schema": {
       "type": "string"
      },
      "example": "api.example.com"
     }
    ],
    "responses": {
     "200": {
      "description": "Active site API keys.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/OwnerSiteKeysResponse"
        },
        "example": {
         "keys": [
          {
           "site": "api.example.com",
           "id": "7c1e9a52-3b4d-4f6e-8a90-1b2c3d4e5f60",
           "label": "Production",
           "prefix": "tt_9d4e7a10",
           "created_at": "2026-10-04T09:41:12.004Z",
           "last_used_at": null,
           "created_with": {
            "id": "5d2c1f0e-8a7b-4c3d-9e1f-6a5b4c3d2e10",
            "label": "CI deploy"
           }
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "The site filter is not a valid hostname.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "invalid site"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "The management key lacks the read scope.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "scope read required"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    }
   }
  },
  "/api/reports/workspace": {
   "get": {
    "tags": [
     "Reports"
    ],
    "operationId": "getWorkspaceDigest",
    "summary": "Read the workspace digest",
    "description": "Owner only, by Firebase ID token or management key. One email per recipient that sums up every verified site of the owner.",
    "responses": {
     "200": {
      "description": "Digest settings and recipients.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkspaceDigest"
        },
        "example": {
         "configured": true,
         "enabled": true,
         "recipients": [
          {
           "email": "you@company.com",
           "last_sent_at": "2026-09-28T07:00:14.000Z",
           "fail_count": 0
          },
          {
           "email": "teammate@company.com",
           "last_sent_at": null,
           "fail_count": 0
          }
         ],
         "frequency": "weekly",
         "timezone": "Europe/Amsterdam",
         "send_hour": 9,
         "sites": 8,
         "next_send_at": "2026-10-05T07:00:00.000Z",
         "dropped": [],
         "updated_at": "2026-10-01T18:00:00.000Z"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   },
   "put": {
    "tags": [
     "Reports"
    ],
    "operationId": "updateWorkspaceDigest",
    "summary": "Save the workspace digest",
    "description": "Owner only, by Firebase ID token or management key. Saves the fields sent, clears the digest's dropped list, resets failure counts on digest and default recipients and creates or removes digest recipient rows. The default report settings are not changed.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/UpdateWorkspaceDigest"
       },
       "example": {
        "enabled": true,
        "recipients": [
         "you@company.com",
         "teammate@company.com"
        ],
        "frequency": "weekly",
        "timezone": "Europe/Amsterdam",
        "send_hour": 9,
        "updated_at": "2026-10-01T18:00:00.000Z"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Saved digest, same shape as the read.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkspaceDigest"
        },
        "example": {
         "configured": true,
         "enabled": true,
         "recipients": [
          {
           "email": "you@company.com",
           "last_sent_at": "2026-09-28T07:00:14.000Z",
           "fail_count": 0
          },
          {
           "email": "teammate@company.com",
           "last_sent_at": null,
           "fail_count": 0
          }
         ],
         "frequency": "weekly",
         "timezone": "Europe/Amsterdam",
         "send_hour": 9,
         "sites": 8,
         "next_send_at": "2026-10-05T07:00:00.000Z",
         "dropped": [],
         "updated_at": "2026-10-01T18:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "Validation failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "json": {
          "value": {
           "error": "invalid json"
          }
         },
         "enabled": {
          "value": {
           "error": "enabled must be a boolean"
          }
         },
         "recipients": {
          "value": {
           "error": "recipients must be a list of email addresses"
          }
         },
         "email": {
          "value": {
           "error": "invalid email address: not-an-email"
          }
         },
         "cap": {
          "value": {
           "error": "too many recipients (max 10)"
          }
         },
         "frequency": {
          "value": {
           "error": "frequency must be daily, weekly, or monthly"
          }
         },
         "timezone": {
          "value": {
           "error": "invalid timezone"
          }
         },
         "sendHour": {
          "value": {
           "error": "send_hour must be an integer between 0 and 23"
          }
         },
         "updatedAt": {
          "value": {
           "error": "invalid updated_at"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyManageScope"
     },
     "409": {
      "description": "The digest or the report defaults changed since the updated_at sent with the save.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "these settings changed since you opened them; reload and try again"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/ManagementKeyRateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "manage"
      ]
     }
    ]
   }
  },
  "/api/reports/workspace/test": {
   "post": {
    "tags": [
     "Reports"
    ],
    "operationId": "testWorkspaceDigest",
    "summary": "Send a test digest to yourself",
    "description": "Signed-in owner only; management keys cannot call it. Sends the digest for the last full period to the signed-in owner's address only, never to the list. At most one test per 30 seconds.",
    "responses": {
     "200": {
      "description": "The address the test went to and the period it covers.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "sent_to": {
           "type": "string",
           "format": "email"
          },
          "period_start": {
           "type": "string",
           "format": "date-time"
          },
          "period_end": {
           "type": "string",
           "format": "date-time"
          }
         },
         "required": [
          "sent_to",
          "period_start",
          "period_end"
         ],
         "additionalProperties": false
        },
        "example": {
         "sent_to": "you@company.com",
         "period_start": "2026-09-20T22:00:00.000Z",
         "period_end": "2026-09-27T22:00:00.000Z"
        }
       }
      }
     },
     "400": {
      "description": "Nothing to send yet.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "recipients": {
          "value": {
           "error": "Add a recipient first"
          }
         },
         "sites": {
          "value": {
           "error": "Verify a site first"
          }
         },
         "email": {
          "value": {
           "error": "your account has no email address"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "description": "Called with a management key.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "management keys cannot call this endpoint"
        }
       }
      }
     },
     "429": {
      "description": "A test was sent less than 30 seconds ago.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "a test was just sent, try again in a moment"
        }
       }
      }
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "502": {
      "description": "The email provider refused the message.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "could not send the test digest"
        }
       }
      }
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     }
    ]
   }
  },
  "/api/reports/workspace/preview": {
   "get": {
    "tags": [
     "Reports"
    ],
    "operationId": "previewWorkspaceDigest",
    "summary": "Render the workspace digest as HTML",
    "description": "Owner only, by Firebase ID token or management key. Builds the digest for the last fully elapsed period over the owner's verified sites and returns it as text/html. An unknown `tz` falls back to UTC.",
    "parameters": [
     {
      "name": "frequency",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "daily",
        "weekly",
        "monthly"
       ],
       "default": "weekly"
      }
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "description": "IANA timezone."
     }
    ],
    "responses": {
     "200": {
      "description": "Rendered email HTML.",
      "headers": {
       "Cache-Control": {
        "$ref": "#/components/headers/NoStore"
       }
      },
      "content": {
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "Validation failed or nothing to show.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "frequency": {
          "value": {
           "error": "invalid frequency"
          }
         },
         "sites": {
          "value": {
           "error": "Verify a site first"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/SignInRequired"
     },
     "403": {
      "$ref": "#/components/responses/ManagementKeyReadScope"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     }
    },
    "security": [
     {
      "FirebaseIdToken": []
     },
     {
      "ManagementKey": [
       "read"
      ]
     }
    ]
   }
  }
 },
 "components": {
  "securitySchemes": {
   "FirebaseIdToken": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT",
    "description": "Current Firebase ID token for a Clicktag user, sent as Authorization: Bearer <token>. Not an API key, Firebase custom token, OAuth provider access token or session cookie. Obtain a current token from the authentication guide."
   },
   "SiteApiKey": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "tt_ followed by 48 lowercase hex characters",
    "description": "Site API key from site settings (API tab) or POST /api/sites/{hostname}/keys, sent as Authorization: Bearer tt_.... Only accepted by POST /api/ingest; it cannot read stats or manage sites or keys. Revoked keys are rejected within a minute."
   },
   "ManagementKey": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "tt_mk_ followed by 48 lowercase hex characters",
    "description": "Owner-level key created under Workspace settings, API keys, sent as Authorization: Bearer tt_mk_.... It works across every site of the account. Scopes decide what it may call and do not include each other: read for the GET operations that list ManagementKey, manage for their other methods, ingest for POST /api/ingest with the X-Totallytics-Site header. GET /api/me and revoking the calling key need no scope. A missing scope returns 403 scope <scope> required; operations that need a signed-in owner return 403 management keys cannot call this endpoint. Non-GET calls other than ingest are rate limited per key on each server instance: a burst of 60, then 1 per second (429 rate limited, retry in 1s, with Retry-After: 1). Live visitor routes (/api/live/...) do not accept management keys and return 401 sign in required. Revoked keys are rejected within about a minute."
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string",
      "description": "Human-readable error message."
     }
    },
    "required": [
     "error"
    ],
    "additionalProperties": false
   },
   "Site": {
    "type": "object",
    "properties": {
     "hostname": {
      "type": "string",
      "description": "Registered hostname."
     },
     "display_name": {
      "type": "string",
      "description": "Display name; initially empty."
     },
     "is_public": {
      "type": "boolean",
      "description": "Allows anonymous metadata, web overview, breakdown and retention reads once verified. Other reports remain owner-only."
     },
     "created_at": {
      "type": "string",
      "description": "ISO 8601 creation timestamp.",
      "format": "date-time"
     },
     "kind": {
      "type": "string",
      "enum": [
       "web",
       "api"
      ],
      "description": "web or api, fixed at registration."
     },
     "verified_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Owner only. ISO 8601 verification timestamp; null until verified."
     },
     "verify_token": {
      "type": [
       "string",
       "null"
      ],
      "description": "Owner only. Ownership challenge for verification."
     },
     "reports_mode": {
      "$ref": "#/components/schemas/ReportsMode",
      "description": "Owner only."
     },
     "pinned_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Owner only. ISO 8601 time the owner pinned the site; null when it is not pinned."
     }
    },
    "required": [
     "hostname",
     "display_name",
     "is_public",
     "created_at",
     "kind"
    ],
    "additionalProperties": false
   },
   "OwnedSite": {
    "type": "object",
    "properties": {
     "hostname": {
      "type": "string",
      "description": "Registered hostname."
     },
     "display_name": {
      "type": "string",
      "description": "Display name; initially empty."
     },
     "is_public": {
      "type": "boolean",
      "description": "Allows anonymous metadata, web overview, breakdown and retention reads once verified. Other reports remain owner-only."
     },
     "created_at": {
      "type": "string",
      "description": "ISO 8601 creation timestamp.",
      "format": "date-time"
     },
     "owner_uid": {
      "type": "string",
      "description": "Owner Firebase user ID; returned only by the authenticated list."
     },
     "verified_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "ISO 8601 verification timestamp; null until verified."
     },
     "kind": {
      "type": "string",
      "enum": [
       "web",
       "api"
      ],
      "description": "web or api, fixed at registration."
     },
     "pinned_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "ISO 8601 time the owner pinned the site; null when it is not pinned."
     }
    },
    "required": [
     "hostname",
     "display_name",
     "is_public",
     "created_at",
     "owner_uid",
     "verified_at",
     "kind",
     "pinned_at"
    ],
    "additionalProperties": false
   },
   "SitesResponse": {
    "type": "object",
    "properties": {
     "sites": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/OwnedSite"
      },
      "description": "All owned sites: pinned sites first (most recently pinned first), then by creation time. No pagination."
     }
    },
    "required": [
     "sites"
    ],
    "additionalProperties": false
   },
   "CreateSite": {
    "type": "object",
    "required": [
     "hostname"
    ],
    "properties": {
     "hostname": {
      "type": "string",
      "description": "Trimmed and parsed as a URL host: a scheme, path, query or default port is dropped, while credentials, another port or a scheme other than http or https return 400. The host is lowercased, converted to punycode and stripped of a trailing dot. The result is at most 253 characters, has two or more dot-separated labels of 1-63 letters, digits or hyphens that start and end with a letter or digit, and is not made of digits and dots only.",
      "examples": [
       "your-domain.example"
      ]
     },
     "kind": {
      "type": "string",
      "enum": [
       "web",
       "api"
      ],
      "default": "web",
      "description": "web for a website with the tracking script, api for API analytics only. An api site can create API keys before ownership verification; website collection, sharing, email reports, imports and Search Console still require verification. Cannot be changed later.",
      "examples": [
       "api"
      ]
     }
    },
    "additionalProperties": true,
    "description": "Only hostname and kind are read. Registration creates a private site with an empty display name. An account can register at most 50 sites; hostname uniqueness is global."
   },
   "CreatedSite": {
    "type": "object",
    "properties": {
     "hostname": {
      "type": "string",
      "description": "Normalized hostname."
     },
     "verified_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "null for a new registration; the stored timestamp when re-registering a verified hostname you already own."
     },
     "verify_token": {
      "type": [
       "string",
       "null"
      ],
      "description": "Ownership challenge for tag, DNS TXT or well-known file verification."
     },
     "kind": {
      "type": "string",
      "enum": [
       "web",
       "api"
      ],
      "description": "Stored kind. Re-registering an owned hostname keeps the original kind."
     }
    },
    "required": [
     "hostname",
     "verified_at",
     "verify_token",
     "kind"
    ],
    "additionalProperties": false
   },
   "UpdateSite": {
    "type": "object",
    "properties": {
     "display_name": {
      "type": "string",
      "description": "Truncated to 128 characters, not rejected for excess length. Empty string clears; omission retains the current name. Null and non-string values return 400."
     },
     "is_public": {
      "type": "boolean",
      "description": "True enables anonymous metadata and web stats reads only after verification. Omission retains the current value; non-boolean input returns 400."
     },
     "reports_mode": {
      "$ref": "#/components/schemas/ReportsMode",
      "description": "Omission retains the current value; other values return 400. Setting custom copies the workspace default recipients into the site's own rows when it has none."
     },
     "pinned": {
      "type": "boolean",
      "description": "True pins the site to the top of the owner's website list; pinning an already pinned site keeps its original pin time. False unpins. Omission retains the current pin; non-boolean input returns 400."
     }
    },
    "additionalProperties": true,
    "description": "Only these fields are read. Empty objects leave current values unchanged. Invalid JSON, null, arrays and non-object bodies return 400, as do incorrectly typed supported fields."
   },
   "UpdatedSite": {
    "type": "object",
    "properties": {
     "hostname": {
      "type": "string",
      "description": "Registered hostname."
     },
     "display_name": {
      "type": "string",
      "description": "Display name; initially empty."
     },
     "is_public": {
      "type": "boolean",
      "description": "Allows anonymous site metadata and stats reads."
     },
     "reports_mode": {
      "$ref": "#/components/schemas/ReportsMode"
     },
     "pinned_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "ISO 8601 time the owner pinned the site; null when it is not pinned."
     }
    },
    "required": [
     "hostname",
     "display_name",
     "is_public",
     "reports_mode",
     "pinned_at"
    ],
    "additionalProperties": false
   },
   "DeletedSite": {
    "type": "object",
    "properties": {
     "deleted": {
      "type": "string",
      "description": "Hostname whose registry entry was removed. Historical analytics are not erased."
     }
    },
    "required": [
     "deleted"
    ],
    "additionalProperties": false
   },
   "OverviewMetrics": {
    "type": "object",
    "properties": {
     "pageviews": {
      "type": "integer",
      "minimum": 0,
      "description": "Canonical non-bot pageview count in the exact requested interval."
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct daily visitor hashes on non-bot pageviews. The hash changes every UTC day, so a returning person counts again on each day. Rows without a hash, such as imports, count their unique-entry flag."
     },
     "avg_duration_s": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "Legacy field name: median per-page duration in seconds after summing increments and excluding page totals below five seconds. Null without eligible measured pageviews."
     },
     "avg_scroll": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "Mean of per-page maximum measured scroll percentages. Explicit zero contributes; missing values do not. Null without measured pageviews.",
      "maximum": 100
     }
    },
    "required": [
     "pageviews",
     "visitors",
     "avg_duration_s",
     "avg_scroll"
    ],
    "additionalProperties": false
   },
   "SeriesPoint": {
    "type": "object",
    "properties": {
     "t": {
      "type": "integer",
      "format": "int64",
      "description": "Bucket start in Unix seconds; daily buckets use local calendar-day starts in the requested timezone."
     },
     "pageviews": {
      "type": "integer",
      "minimum": 0,
      "description": "Non-bot pageviews in this bucket."
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct daily visitor hashes in this bucket, plus the unique-entry flag of pageviews without a hash; someone active in several buckets counts in each."
     },
     "duration_s": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "Median seconds on page in this bucket over pageviews with at least five seconds; null without eligible measured pageviews."
     },
     "scrolled": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "maximum": 100,
      "description": "Mean of per-page maximum measured scroll percentages in this bucket; null without measured pageviews."
     }
    },
    "required": [
     "t",
     "pageviews",
     "visitors",
     "duration_s",
     "scrolled"
    ],
    "additionalProperties": false
   },
   "OverviewResponse": {
    "type": "object",
    "properties": {
     "totals": {
      "$ref": "#/components/schemas/OverviewMetrics"
     },
     "previous": {
      "$ref": "#/components/schemas/OverviewMetrics",
      "description": "Same metrics for previous_range."
     },
     "series": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SeriesPoint"
      },
      "description": "Ascending pageview buckets in tz, zero-filled from the bucket holding from through the bucket holding now or the last second of the range, whichever comes first; a range that starts in the future returns only the bucket holding from. The first bucket can start before from. Quiet buckets have zero counts and null duration_s and scrolled, and a non-null forecast always has its bucket here."
     },
     "forecast": {
      "type": [
       "object",
       "null"
      ],
      "description": "Expected end-of-bucket totals for the day or hour that contains now; null unless the range reaches into it.",
      "properties": {
       "bucket": {
        "type": "integer",
        "format": "int64",
        "description": "Start of the current bucket in Unix seconds, matching series[].t."
       },
       "pageviews": {
        "type": "integer",
        "minimum": 0,
        "description": "Expected pageviews by the end of the bucket, never below the count so far."
       },
       "visitors": {
        "type": "integer",
        "minimum": 0,
        "description": "Expected visitors by the end of the bucket, never below the count so far."
       },
       "basis": {
        "type": "string",
        "enum": [
         "profile",
         "elapsed"
        ],
        "description": "profile: the site's own hour-of-day shape over the same weekday in the last 4 weeks, falling back to the last 14 days. elapsed: share of the bucket's time elapsed, always for hourly buckets."
       },
       "elapsed": {
        "type": "number",
        "minimum": 0,
        "maximum": 1,
        "description": "Elapsed fraction of the bucket at the earlier of to and now."
       }
      },
      "required": [
       "bucket",
       "pageviews",
       "visitors",
       "basis",
       "elapsed"
      ],
      "additionalProperties": false
     },
     "live": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct nonempty visitor hashes active in the last five minutes on human pageviews/events or append updates linked to human parents; independent of the selected range. Blank imported identities and unlinked/bot-parent updates are excluded."
     },
     "granularity": {
      "type": "string",
      "enum": [
       "hour",
       "day"
      ],
      "description": "Hour for a rounded span up to four days; day otherwise."
     },
     "previous_range": {
      "type": "object",
      "properties": {
       "from": {
        "type": "integer",
        "description": "Start in Unix seconds, inclusive."
       },
       "to": {
        "type": "integer",
        "description": "End in Unix seconds, exclusive."
       }
      },
      "required": [
       "from",
       "to"
      ],
      "additionalProperties": false,
      "description": "The window previous covers: the same clock times, moved back by as many calendar days as the range covers in tz. Today until 12:35 compares with yesterday until 12:35, the last 7 days with the 7 days before."
     }
    },
    "required": [
     "totals",
     "previous",
     "series",
     "forecast",
     "live",
     "granularity",
     "previous_range"
    ],
    "additionalProperties": false
   },
   "OverviewSummaryResponse": {
    "type": "object",
    "properties": {
     "events": {
      "type": "object",
      "properties": {
       "totals": {
        "type": "object",
        "properties": {
         "count": {
          "type": "integer",
          "minimum": 0,
          "description": "Canonical non-bot custom events with a name."
         },
         "visitors": {
          "type": "integer",
          "minimum": 0,
          "description": "Distinct nonempty visitor hashes behind those events."
         }
        },
        "required": [
         "count",
         "visitors"
        ],
        "additionalProperties": false,
        "description": "Custom events in the requested range."
       },
       "previous": {
        "type": "object",
        "properties": {
         "count": {
          "type": "integer",
          "minimum": 0,
          "description": "Canonical non-bot custom events with a name."
         },
         "visitors": {
          "type": "integer",
          "minimum": 0,
          "description": "Distinct nonempty visitor hashes behind those events."
         }
        },
        "required": [
         "count",
         "visitors"
        ],
        "additionalProperties": false,
        "description": "Same numbers for the overview's previous_range."
       },
       "top": {
        "type": "array",
        "maxItems": 20,
        "items": {
         "$ref": "#/components/schemas/BreakdownRow"
        },
        "description": "Top event names by count; value counts events and visitors counts distinct nonempty visitor identifiers."
       }
      },
      "required": [
       "totals",
       "previous",
       "top"
      ],
      "additionalProperties": false
     },
     "tech": {
      "type": "object",
      "properties": {
       "devices": {
        "type": "array",
        "maxItems": 20,
        "items": {
         "$ref": "#/components/schemas/BreakdownRow"
        },
        "description": "Top devices; value counts pageviews and visitors counts like the pageview breakdowns. Blank names are omitted."
       },
       "browsers": {
        "type": "array",
        "maxItems": 20,
        "items": {
         "$ref": "#/components/schemas/BreakdownRow"
        },
        "description": "Top browsers, normalized like the browsers breakdown. Blank names are omitted."
       },
       "os": {
        "type": "array",
        "maxItems": 20,
        "items": {
         "$ref": "#/components/schemas/BreakdownRow"
        },
        "description": "Top operating systems, normalized like the os breakdown. Blank names are omitted."
       }
      },
      "required": [
       "devices",
       "browsers",
       "os"
      ],
      "additionalProperties": false
     },
     "live": {
      "type": "object",
      "properties": {
       "pages": {
        "type": "array",
        "maxItems": 20,
        "items": {
         "type": "object",
         "properties": {
          "name": {
           "type": "string",
           "description": "Page path; an empty path becomes /."
          },
          "visitors": {
           "type": "integer",
           "minimum": 0,
           "description": "Distinct nonempty visitor identifiers on human pageviews of this path in the last 30 minutes."
          }
         },
         "required": [
          "name",
          "visitors"
         ],
         "additionalProperties": false
        },
        "description": "Top paths over the last 30 minutes, independent of from and to."
       }
      },
      "required": [
       "pages"
      ],
      "additionalProperties": false
     }
    },
    "required": [
     "events",
     "tech",
     "live"
    ],
    "additionalProperties": false
   },
   "BreakdownRow": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Group label. Empty page values become /; empty acquisition sources become Direct / none."
     },
     "value": {
      "type": "integer",
      "minimum": 0,
      "description": "Matching non-bot row count: pageviews for all dimensions except events, which counts event rows."
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct nonempty daily visitor hashes in the group. In pageview dimensions, pageviews without a hash (mainly imports and older data) add their unique-entry flag; in the events dimension, events without a hash add nothing."
     },
     "last_seen": {
      "type": "string",
      "format": "date-time",
      "description": "Events dimension only: time of the newest matching event in the range, as an ISO 8601 UTC string cut to whole seconds, such as 2026-10-07T08:51:21.000Z."
     }
    },
    "required": [
     "name",
     "value",
     "visitors"
    ],
    "additionalProperties": false
   },
   "BreakdownResponse": {
    "type": "object",
    "properties": {
     "rows": {
      "type": "array",
      "maxItems": 100,
      "items": {
       "$ref": "#/components/schemas/BreakdownRow"
      },
      "description": "Count-descending groups, with no pagination cursor or total-row count."
     }
    },
    "required": [
     "rows"
    ],
    "additionalProperties": false
   },
   "SummaryDay": {
    "type": "object",
    "properties": {
     "day": {
      "type": "string",
      "description": "Calendar day in the requested timezone.",
      "format": "date"
     },
     "pageviews": {
      "type": "integer",
      "minimum": 0,
      "description": "Canonical non-bot pageviews grouped by exact timestamps in the requested timezone."
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct daily visitor hashes on non-bot pageviews for this calendar day; pageviews without a hash count their unique-entry flag, as in the overview."
     }
    },
    "required": [
     "day",
     "pageviews",
     "visitors"
    ],
    "additionalProperties": false
   },
   "SummaryApiDay": {
    "type": "object",
    "properties": {
     "day": {
      "type": "string",
      "description": "Calendar day in the requested timezone.",
      "format": "date"
     },
     "requests": {
      "type": "integer",
      "minimum": 0,
      "description": "API requests on this day."
     },
     "server_errors": {
      "type": "integer",
      "minimum": 0,
      "description": "API requests on this day that returned a 5xx status."
     }
    },
    "required": [
     "day",
     "requests",
     "server_errors"
    ],
    "additionalProperties": false
   },
   "SummarySite": {
    "type": "object",
    "properties": {
     "hostname": {
      "type": "string",
      "description": "Owned registered hostname."
     },
     "pinned_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "ISO 8601 time the owner pinned the site; null when it is not pinned."
     },
     "live": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct nonempty visitor hashes active in the last five minutes on human pageviews/events or append updates linked to human parents; independent of the selected range. Blank imported identities and unlinked/bot-parent updates are excluded. Website traffic only: always 0 for API sites that are not verified."
     },
     "days": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SummaryDay"
      },
      "description": "Sparse ascending calendar days, not zero-filled. Empty for sites without pageviews, including API sites that are not verified."
     },
     "api_days": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SummaryApiDay"
      },
      "description": "Only on API sites (kind api): sparse ascending calendar days with API requests, not zero-filled, counted from the site's registration. Absent on websites."
     }
    },
    "required": [
     "hostname",
     "pinned_at",
     "live",
     "days"
    ],
    "additionalProperties": false
   },
   "SummaryResponse": {
    "type": "object",
    "properties": {
     "sites": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SummarySite"
      },
      "description": "Verified sites plus every API site, verified or not, pinned sites first (most recently pinned first), then by creation time; empty if there are none."
     }
    },
    "required": [
     "sites"
    ],
    "additionalProperties": false
   },
   "CollectorPayload": {
    "type": "object",
    "required": [
     "hostname"
    ],
    "properties": {
     "hostname": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Required after scalar-to-string conversion. Normalized the way site registration does: trimmed and lowercased, with an http:// or https:// scheme, path and trailing dot dropped and internationalized names in xn-- form. A value registration would reject, such as one with a port or an IP address, can't match a site: the hit is accepted and nothing is stored. The result must match a registered hostname exactly; www and apex are different hostnames.",
      "examples": [
       "example.com"
      ]
     },
     "type": {
      "type": "string",
      "enum": [
       "pageview",
       "event",
       "append",
       "error"
      ],
      "default": "pageview",
      "description": "Both POST paths share this default. Omitted or empty values become pageview. Truncated to 16 characters before validation; case-sensitive."
     },
     "event": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Required for type event; ignored otherwise. Truncated to 256 characters, then runs outside ASCII letters/digits become underscores and edge underscores are removed. Case is preserved. Empty after cleaning returns 400.",
      "examples": [
       "signup"
      ]
     },
     "path": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Truncated to 2048 characters. Missing or empty defaults to / for pageview and empty for other types.",
      "examples": [
       "/pricing"
      ]
     },
     "query": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Page query string without the leading ?. Truncated to 2048 characters and parsed for UTM values.",
      "examples": [
       "utm_source=newsletter&utm_medium=email"
      ]
     },
     "referrer": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Full URL or hostname/path, truncated to 2048 characters. Parsed hostname is lowercased, excluding port, query and fragment.",
      "examples": [
       "news.example/article"
      ]
     },
     "metadata": {
      "type": [
       "object",
       "array",
       "string",
       "null"
      ],
      "description": "Objects and arrays are JSON-serialized. Strings are stored as supplied; null stores nothing. Truncated to 4096 characters even if this breaks JSON. Not retrievable, filterable or groupable through the stats API; the live view shows the first 512 characters."
     },
     "id": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Stable logical row ID, truncated to 64 characters. Optional, defaults empty. Bounded per-isolate replay suppression uses site, type and nonempty ID; reports reconcile repeated IDs. Not durable exactly-once delivery."
     },
     "page_id": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Page correlation ID, truncated to 64 characters. Optional, defaults empty; not a replacement for the row replay ID."
     },
     "session_id": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Session correlation ID, truncated to 64 characters. Optional, defaults empty; not used for visitor counting or row replay identity. Stats filters match pageviews and events to each other through it, so rows without one never match a filter on the other type."
     },
     "original_id": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Original pageview ID for an append, truncated to 64 characters. Reports link updates to this parent for its date, bot status and engagement. Missing/unlinked parents do not contribute to engagement."
     },
     "duration": {
      "type": [
       "number",
       "string",
       "boolean",
       "null"
      ],
      "default": null,
      "description": "Incremental seconds. Numeric strings accepted; rounded and capped at 86400. Missing, empty, malformed, non-finite or negative values become null; explicit zero remains zero."
     },
     "scrolled": {
      "type": [
       "number",
       "string",
       "boolean",
       "null"
      ],
      "default": null,
      "description": "Scroll percentage. Numeric strings accepted; rounded and capped at 100. Missing, empty, malformed, non-finite or negative values become null; explicit zero remains zero."
     },
     "viewport_width": {
      "type": [
       "number",
       "string",
       "boolean"
      ],
      "default": 0,
      "description": "Viewport width. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected."
     },
     "viewport_height": {
      "type": [
       "number",
       "string",
       "boolean"
      ],
      "default": 0,
      "description": "Viewport height. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected."
     },
     "screen_width": {
      "type": [
       "number",
       "string",
       "boolean"
      ],
      "default": 0,
      "description": "Screen width. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected."
     },
     "screen_height": {
      "type": [
       "number",
       "string",
       "boolean"
      ],
      "default": 0,
      "description": "Screen height. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected."
     },
     "error": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Error text, truncated to 2048 characters; empty by default. Error rows have no dedicated breakdown."
     },
     "ua": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Supplied user-agent string is truncated to 512 characters. Empty or missing falls back to the request User-Agent header, truncated the same way. Drives browser, OS, device, bot and visitor classification."
     },
     "timezone": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Browser IANA timezone, truncated to 64 characters. Used for country inference, not row timestamps. UTC, fixed offsets and unknown/missing zones have no country; no IP fallback."
     },
     "language": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Language label, truncated to 16 characters."
     },
     "os_name": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Truncated to 64 characters. Nonempty input overrides parsed OS name, then shared normalization applies (Mac OS / Mac OS X become macOS)."
     },
     "os_version": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Truncated to 64 characters. Non-empty input overrides the parsed OS version."
     },
     "brands": {
      "type": [
       "string",
       "number",
       "boolean",
       "array"
      ],
      "description": "Client-hint browser brands as an array or serialized string. Stored up to 1024 characters; supported brands contribute to normalized browser classification."
     },
     "version": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Script version label, truncated to 32 characters."
     },
     "hostname_original": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Original hostname before an override, lowercased and truncated to 253 characters."
     },
     "unique": {
      "type": [
       "boolean",
       "number",
       "string"
      ],
      "default": false,
      "description": "SA-style entry flag, stored as is_unique. True for true, 1, \"true\" or \"1\"; other scalar values are false."
     },
     "mobile": {
      "type": [
       "boolean",
       "number",
       "string"
      ],
      "default": false,
      "description": "Mobile hint. Parsed device type also contributes to the stored mobile flag, and the hint sets device type mobile when the user agent shows none. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false."
     },
     "bot": {
      "type": [
       "boolean",
       "number",
       "string"
      ],
      "default": false,
      "description": "Explicit bot flag. False does not override a bot user-agent match. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false."
     },
     "brave": {
      "type": [
       "boolean",
       "number",
       "string"
      ],
      "default": false,
      "description": "Brave hint: the browser is stored as Brave. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false."
     },
     "duck": {
      "type": [
       "boolean",
       "number",
       "string"
      ],
      "default": false,
      "description": "DuckDuckGo hint: the browser is stored as DuckDuckGo unless brave is also true. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false."
     },
     "https": {
      "type": [
       "boolean",
       "number",
       "string"
      ],
      "default": true,
      "description": "Whether the tracked page used HTTPS. Missing input defaults true. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false."
     },
     "collect-dnt": {
      "type": [
       "boolean",
       "number",
       "string"
      ],
      "default": false,
      "description": "Only true or \"true\" bypasses collection skipping for DNT: 1 or X-Do-Not-Track: 1. Numeric/string 1 is not an override. Skipped requests return success before payload-field validation."
     },
     "utm_source": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns."
     },
     "utm_medium": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns."
     },
     "utm_campaign": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns."
     },
     "utm_term": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns."
     },
     "utm_content": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns."
     },
     "source": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Alias for utm_source, used when no nonempty utm_source is in the query field or the payload. Truncated to 256 characters; stored only as utm_source."
     },
     "ref": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Second alias for utm_source, used when neither utm_source nor source is set. Truncated to 256 characters; stored only as utm_source."
     },
     "medium": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Alias for utm_medium, used when no nonempty utm_medium is in the query field or the payload. Truncated to 256 characters; stored only as utm_medium."
     },
     "campaign": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Alias for utm_campaign, used when no nonempty utm_campaign is in the query field or the payload. Truncated to 256 characters; stored only as utm_campaign."
     },
     "term": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Alias for utm_term, used when no nonempty utm_term is in the query field or the payload. Truncated to 256 characters; stored only as utm_term."
     },
     "content": {
      "type": [
       "string",
       "number",
       "boolean"
      ],
      "description": "Alias for utm_content, used when no nonempty utm_content is in the query field or the payload. Truncated to 256 characters; stored only as utm_content."
     }
    },
    "additionalProperties": true,
    "allOf": [
     {
      "if": {
       "properties": {
        "type": {
         "const": "event"
        }
       },
       "required": [
        "type"
       ]
      },
      "then": {
       "properties": {
        "event": {
         "description": "Required when type is event. Must contain an ASCII letter or digit within its first 256 characters."
        }
       },
       "required": [
        "event"
       ],
       "additionalProperties": true
      }
     }
    ],
    "description": "One payload, not a batch. Both /events and /append default to pageview; set type explicitly. Text limits truncate. Stable nonempty row IDs enable bounded replay suppression and reporting reconciliation, not guaranteed delivery. Timestamps are server-assigned, country is derived from browser timezone, and visitor hashes are server-derived. Payload ts, timestamp, country, ip, visitor_id, time and sri do not override them. Bot and DNT rules apply."
   },
   "ImportConfigField": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "description": "Configuration property name."
     },
     "label": {
      "type": "string",
      "description": "Human-readable field label."
     },
     "type": {
      "type": "string",
      "description": "Input kind; password fields are excluded from returned job config.",
      "enum": [
       "text",
       "password",
       "date"
      ]
     },
     "required": {
      "type": "boolean",
      "description": "Whether creation requires a nonempty string after trimming."
     },
     "placeholder": {
      "type": "string",
      "description": "Optional input placeholder."
     },
     "help": {
      "type": "string",
      "description": "Optional field instructions."
     }
    },
    "required": [
     "key",
     "label",
     "type",
     "required"
    ],
    "additionalProperties": false
   },
   "ImportSource": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Source identifier; currently simpleanalytics."
     },
     "label": {
      "type": "string",
      "description": "Source display label."
     },
     "description": {
      "type": "string",
      "description": "Description of imported data."
     },
     "configFields": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ImportConfigField"
      }
     }
    },
    "required": [
     "id",
     "label",
     "description",
     "configFields"
    ],
    "additionalProperties": false
   },
   "ImportSourcesResponse": {
    "type": "object",
    "properties": {
     "sources": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ImportSource"
      }
     }
    },
    "required": [
     "sources"
    ],
    "additionalProperties": false
   },
   "SimpleAnalyticsImportConfig": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "description": "SimpleAnalytics User ID from Account > API. Trimmed and truncated to 512 characters.",
      "minLength": 1
     },
     "api_key": {
      "type": "string",
      "description": "SimpleAnalytics API key, not a Clicktag bearer token. Trimmed and truncated to 512 characters. Never echoed in job config; removed from stored config on completion; failed and canceled jobs keep it for 7 days for retry, then it is deleted.",
      "minLength": 1,
      "writeOnly": true
     },
     "source_hostname": {
      "type": "string",
      "description": "Source hostname registered in SimpleAnalytics; may differ from the destination site. Trimmed, truncated to 512 characters and lowercased. Must match ^(?!-)[a-z0-9-]{1,63}(?<!-)(\\.[a-z0-9-]{1,63})+$. No scheme, port or path.",
      "minLength": 1
     }
    },
    "required": [
     "user_id",
     "api_key",
     "source_hostname"
    ],
    "additionalProperties": true,
    "description": "All three strings are required. Unknown keys are ignored. Source credentials are tested using a recent pageview export before creating the job."
   },
   "CreateImport": {
    "type": "object",
    "properties": {
     "source": {
      "type": "string",
      "description": "Import source identifier.",
      "const": "simpleanalytics"
     },
     "config": {
      "$ref": "#/components/schemas/SimpleAnalyticsImportConfig"
     },
     "start": {
      "type": "string",
      "description": "Inclusive requested UTC date, at least 2010-01-01 and not after end. The first chunk starts on this date, also mid-month.",
      "format": "date",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
     },
     "end": {
      "type": "string",
      "description": "Inclusive final UTC date. end minus start must not exceed 1826 days. No future-end cutoff. Two chunks per calendar month: pageviews followed by events.",
      "format": "date",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
     }
    },
    "required": [
     "source",
     "config",
     "start",
     "end"
    ],
    "additionalProperties": true,
    "description": "Creates a new asynchronous import. 201 means queued, not imported. No request idempotency key. Stable SA source IDs suppress repeated source records across imports, not overlap with independently collected TT traffic. After ambiguous failures, read existing jobs before sending another POST."
   },
   "ImportPublicConfig": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "description": "Stored non-password source user ID."
     },
     "source_hostname": {
      "type": "string",
      "description": "Normalized source hostname."
     }
    },
    "required": [],
    "additionalProperties": false,
    "description": "Only declared non-password source fields are returned. api_key is never included. Completed jobs retain the visible fields; failed and canceled jobs keep their stored secret config for 7 days for retry, then it is deleted."
   },
   "ImportJob": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Job identifier generated at creation.",
      "format": "uuid"
     },
     "site": {
      "type": "string",
      "description": "Destination Clicktag hostname."
     },
     "source": {
      "type": "string",
      "description": "Source identifier; currently simpleanalytics."
     },
     "source_label": {
      "type": "string",
      "description": "Source display label; currently SimpleAnalytics. Falls back to the ID for an unknown source."
     },
     "status": {
      "type": "string",
      "description": "Job state. Verify progress and actual reports before considering the migration complete.",
      "enum": [
       "queued",
       "running",
       "completed",
       "failed",
       "canceled"
      ]
     },
     "range_start": {
      "type": "string",
      "description": "Requested inclusive UTC start date in YYYY-MM-DD format.",
      "format": "date"
     },
     "range_end": {
      "type": "string",
      "description": "Requested inclusive UTC end date in YYYY-MM-DD format.",
      "format": "date"
     },
     "chunks_total": {
      "type": "integer",
      "minimum": 0,
      "description": "Planned pageview and event chunks."
     },
     "chunks_done": {
      "type": "integer",
      "minimum": 0,
      "description": "Fully checkpointed chunks. Retry uses this cursor."
     },
     "rows_imported": {
      "type": "number",
      "minimum": 0,
      "description": "Records of checkpointed chunks handed to storage, including pageviews, events and derived duration/scroll append rows. Records storage skips because their stable source ID is already stored still count. Not unique pageviews, unique visitors, or a guarantee of exact/durable stored totals. Partial writes may be absent from counters."
     },
     "rows_skipped": {
      "type": "number",
      "minimum": 0,
      "description": "Checkpointed source records skipped for a mismatched hostname or an event with no name."
     },
     "error": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500,
      "description": "Latest worker error, cleared when a chunk succeeds. Can be non-null while queued/running during retries: each chunk is tried up to 4 times, about a minute apart, before the job fails with the last error. Can include upstream response snippets; no general secret redaction is performed."
     },
     "config": {
      "$ref": "#/components/schemas/ImportPublicConfig"
     },
     "created_at": {
      "type": "string",
      "description": "Creation timestamp.",
      "format": "date-time"
     },
     "finished_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Completion, failure or cancellation time; normally null while active. Immediate cancel/retry responses use the previous row snapshot and can return a stale value; poll GET for persisted state."
     }
    },
    "required": [
     "id",
     "site",
     "source",
     "source_label",
     "status",
     "range_start",
     "range_end",
     "chunks_total",
     "chunks_done",
     "rows_imported",
     "rows_skipped",
     "error",
     "config",
     "created_at",
     "finished_at"
    ],
    "additionalProperties": false,
    "description": "Direct response from create/get/cancel/retry, and the shape of list entries. owner_uid and updated_at are not exposed. Progress is checkpointed only after a whole chunk. Password config fields are never echoed. Inspect progress and actual reports, not status alone."
   },
   "ImportsResponse": {
    "type": "object",
    "properties": {
     "imports": {
      "type": "array",
      "maxItems": 50,
      "items": {
       "$ref": "#/components/schemas/ImportJob"
      },
      "description": "Latest 50 jobs ordered by created_at descending. No pagination, cursor or offset."
     }
    },
    "required": [
     "imports"
    ],
    "additionalProperties": false
   },
   "ReportSections": {
    "type": "object",
    "description": "Which sections the email contains. overview is always on.",
    "properties": {
     "overview": {
      "type": "boolean",
      "default": true,
      "description": "Always rendered; accepted and returned for completeness."
     },
     "pages": {
      "type": "boolean",
      "default": true
     },
     "referrers": {
      "type": "boolean",
      "default": true
     },
     "countries": {
      "type": "boolean",
      "default": false
     },
     "events": {
      "type": "boolean",
      "default": false
     },
     "alerts": {
      "type": "boolean",
      "default": true
     },
     "api": {
      "type": "boolean",
      "default": true,
      "description": "API requests, error rates, p95 latency, top and failing endpoints. Rendered only when the site had API requests in the period or the previous one."
     }
    },
    "additionalProperties": false
   },
   "ReportSubscription": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "email": {
      "type": "string",
      "format": "email"
     },
     "frequency": {
      "type": "string",
      "enum": [
       "daily",
       "weekly",
       "monthly"
      ]
     },
     "timezone": {
      "type": "string",
      "description": "IANA timezone; send_hour is wall-clock here."
     },
     "send_hour": {
      "type": "integer",
      "minimum": 0,
      "maximum": 23
     },
     "enabled": {
      "type": "boolean"
     },
     "sections": {
      "$ref": "#/components/schemas/ReportSections"
     },
     "last_sent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "fail_count": {
      "type": "integer",
      "minimum": 0
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "email",
     "frequency",
     "timezone",
     "send_hour",
     "enabled",
     "sections",
     "last_sent_at",
     "fail_count",
     "created_at"
    ],
    "additionalProperties": false
   },
   "ReportsResponse": {
    "type": "object",
    "properties": {
     "mode": {
      "$ref": "#/components/schemas/ReportsMode"
     },
     "subscriptions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ReportSubscription"
      }
     },
     "effective": {
      "$ref": "#/components/schemas/ReportsEffective"
     }
    },
    "required": [
     "mode",
     "subscriptions",
     "effective"
    ],
    "additionalProperties": false
   },
   "CreateReportSubscription": {
    "type": "object",
    "properties": {
     "email": {
      "type": "string",
      "format": "email"
     },
     "frequency": {
      "type": "string",
      "enum": [
       "daily",
       "weekly",
       "monthly"
      ]
     },
     "timezone": {
      "type": "string",
      "default": "UTC",
      "description": "IANA timezone, stored in canonical form (europe/amsterdam becomes Europe/Amsterdam). Fixed offsets such as +02:00 are refused."
     },
     "send_hour": {
      "type": "integer",
      "minimum": 0,
      "maximum": 23,
      "default": 9
     },
     "sections": {
      "$ref": "#/components/schemas/ReportSections"
     }
    },
    "required": [
     "email",
     "frequency"
    ],
    "additionalProperties": false
   },
   "UpdateReportSubscription": {
    "type": "object",
    "description": "Any subset; omitted fields are unchanged.",
    "properties": {
     "frequency": {
      "type": "string",
      "enum": [
       "daily",
       "weekly",
       "monthly"
      ]
     },
     "timezone": {
      "type": "string",
      "description": "IANA timezone, stored in canonical form (europe/amsterdam becomes Europe/Amsterdam). Fixed offsets such as +02:00 are refused."
     },
     "send_hour": {
      "type": "integer",
      "minimum": 0,
      "maximum": 23
     },
     "enabled": {
      "type": "boolean"
     },
     "sections": {
      "$ref": "#/components/schemas/ReportSections"
     }
    },
    "additionalProperties": false
   },
   "GscTotals": {
    "type": "object",
    "properties": {
     "clicks": {
      "type": "number"
     },
     "impressions": {
      "type": "number"
     },
     "ctr": {
      "type": "number"
     },
     "position": {
      "type": "number"
     }
    }
   },
   "GscStatus": {
    "type": "object",
    "properties": {
     "linked": {
      "type": "boolean"
     },
     "property": {
      "type": "string",
      "example": "sc-domain:example.com"
     },
     "sync_state": {
      "type": "string",
      "enum": [
       "backfill",
       "live",
       "error"
      ]
     },
     "backfill_cursor": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "First day of the month the backfill syncs next; null once it's live."
     },
     "synced_until": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "Newest Search Console day synced with data."
     },
     "last_sync_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "last_error": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the link was created."
     }
    },
    "required": [
     "linked"
    ],
    "description": "{ linked: false } for a site without a link; a linked site has every field."
   },
   "GscOverview": {
    "type": "object",
    "properties": {
     "totals": {
      "$ref": "#/components/schemas/GscTotals"
     },
     "previous": {
      "$ref": "#/components/schemas/GscTotals"
     },
     "through": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "Last day totals and previous count, when Google hasn't finalized the end of the range yet; null when they cover the whole range."
     },
     "series": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "date": {
         "type": "string",
         "format": "date",
         "description": "Search Console day (Pacific Time)."
        },
        "t": {
         "type": "number",
         "description": "Unix seconds of that date's midnight in tz."
        },
        "clicks": {
         "type": "number"
        },
        "impressions": {
         "type": "number"
        },
        "ctr": {
         "type": "number"
        },
        "position": {
         "type": "number"
        }
       }
      },
      "description": "One point per day from the start of the range to the last day Google has data for; days without impressions are zero."
     },
     "previous_series": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "date": {
         "type": "string",
         "format": "date",
         "description": "Search Console day (Pacific Time)."
        },
        "t": {
         "type": "number",
         "description": "Unix seconds of that date's midnight in tz."
        },
        "clicks": {
         "type": "number"
        },
        "impressions": {
         "type": "number"
        },
        "ctr": {
         "type": "number"
        },
        "position": {
         "type": "number"
        }
       }
      },
      "description": "The previous period day by day, zero-filled to its end; index i is the same day of the period as series[i]."
     },
     "overlay": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "t": {
         "type": "number"
        },
        "gsc_clicks": {
         "type": "number"
        },
        "tt_pageviews": {
         "type": "number"
        }
       }
      },
      "description": "One point per series day: Google's clicks next to the pageviews the tracker counted from the google.com source that Pacific day, bots and retries excluded. Empty with f."
     },
     "final_through": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "Last day Google has finalized; later days are fresh data that can still change."
     }
    }
   },
   "GscBreakdownResponse": {
    "type": "object",
    "properties": {
     "dim": {
      "type": "string"
     },
     "total": {
      "type": "number",
      "description": "Rows matching q across all pages."
     },
     "through": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "Last day the rows and previous count, when Google hasn't finalized the end of the range yet; null when they cover the whole range."
     },
     "rows": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string",
         "description": "Query, ISO 3166-1 alpha-3 country, device, or page path (prefixed by its host when that isn't the site's own)."
        },
        "url": {
         "type": "string",
         "format": "uri",
         "description": "Pages only: full URL of the page's most-shown variant."
        },
        "clicks": {
         "type": "number"
        },
        "impressions": {
         "type": "number"
        },
        "ctr": {
         "type": "number"
        },
        "position": {
         "type": "number"
        },
        "previous": {
         "oneOf": [
          {
           "$ref": "#/components/schemas/GscTotals"
          },
          {
           "type": "null"
          }
         ],
         "description": "Same row over the previous period of equal length; null when it had no impressions then."
        }
       }
      }
     },
     "anonymized": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/GscTotals"
       },
       {
        "type": "null"
       }
      ],
      "description": "dim=queries only: clicks and impressions Google counted but listed under no query, with their CTR and average position. null when a day in the range is missing for either side, comes from different syncs within the refresh window, or the remainder doesn't add up."
     }
    }
   },
   "CrawlerPurpose": {
    "type": "string",
    "enum": [
     "ai_search",
     "ai_assistant",
     "ai_training",
     "search"
    ],
    "description": "ai_search: fetches that feed AI search answers. ai_assistant: pages a user asked an AI assistant to open. ai_training: crawling for model training. search: search engine crawling."
   },
   "CloudflareConnection": {
    "oneOf": [
     {
      "type": "object",
      "properties": {
       "connected": {
        "type": "boolean",
        "const": false
       }
      },
      "required": [
       "connected"
      ],
      "additionalProperties": false
     },
     {
      "type": "object",
      "properties": {
       "connected": {
        "type": "boolean",
        "const": true
       },
       "token_hint": {
        "type": "string",
        "description": "Last 4 characters of the saved token.",
        "example": "x9Qa"
       },
       "zones": {
        "type": "integer",
        "minimum": 0,
        "description": "Zones the token could list when it was saved."
       },
       "created_at": {
        "type": "string",
        "format": "date-time"
       },
       "updated_at": {
        "type": "string",
        "format": "date-time"
       }
      },
      "required": [
       "connected",
       "token_hint",
       "zones",
       "created_at",
       "updated_at"
      ],
      "additionalProperties": false
     }
    ]
   },
   "CrawlerZone": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string",
      "example": "example.com"
     }
    },
    "required": [
     "id",
     "name"
    ],
    "additionalProperties": false
   },
   "CrawlerStatus": {
    "type": "object",
    "properties": {
     "connected": {
      "type": "boolean",
      "description": "The caller has a saved Cloudflare token."
     },
     "linked": {
      "type": "boolean"
     },
     "zone": {
      "description": "The linked zone. Before linking, the zone the token would use for this hostname: the longest zone name equal to the hostname or a parent domain of it. Null when there is none or no token is saved.",
      "oneOf": [
       {
        "$ref": "#/components/schemas/CrawlerZone"
       },
       {
        "type": "null"
       }
      ]
     },
     "sync_state": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "backfill",
       "live",
       "error",
       null
      ]
     },
     "backfill_from": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Oldest hour the backfill imports."
     },
     "backfill_cursor": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "The backfill has imported everything from here until it went live."
     },
     "synced_until": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Hits are complete up to this time."
     },
     "last_sync_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "last_error": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "connected",
     "linked",
     "zone",
     "sync_state",
     "backfill_from",
     "backfill_cursor",
     "synced_until",
     "last_sync_at",
     "last_error"
    ],
    "additionalProperties": false
   },
   "CrawlerTotals": {
    "type": "object",
    "properties": {
     "ai_hits": {
      "type": "integer",
      "minimum": 0,
      "description": "Verified hits from AI bots (ai_search, ai_assistant and ai_training)."
     },
     "search_hits": {
      "type": "integer",
      "minimum": 0,
      "description": "Verified hits from search engine crawlers."
     },
     "pages": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct paths with verified hits."
     },
     "unverified": {
      "type": "integer",
      "minimum": 0,
      "description": "Hits that claim a known bot's user agent from outside its vendor's published IP ranges."
     }
    },
    "required": [
     "ai_hits",
     "search_hits",
     "pages",
     "unverified"
    ],
    "additionalProperties": false
   },
   "CrawlerSeriesPoint": {
    "type": "object",
    "description": "Verified hits per purpose in one bucket.",
    "properties": {
     "t": {
      "type": "integer",
      "description": "Bucket start, Unix seconds.",
      "example": 1789603200
     },
     "ai_search": {
      "type": "integer",
      "minimum": 0
     },
     "ai_assistant": {
      "type": "integer",
      "minimum": 0
     },
     "ai_training": {
      "type": "integer",
      "minimum": 0
     },
     "search": {
      "type": "integer",
      "minimum": 0
     }
    },
    "required": [
     "t",
     "ai_search",
     "ai_assistant",
     "ai_training",
     "search"
    ],
    "additionalProperties": false
   },
   "CrawlerBotRow": {
    "type": "object",
    "properties": {
     "bot": {
      "type": "string",
      "example": "OAI-SearchBot"
     },
     "vendor": {
      "type": "string",
      "example": "OpenAI"
     },
     "purpose": {
      "$ref": "#/components/schemas/CrawlerPurpose"
     },
     "hits": {
      "type": "integer",
      "minimum": 0,
      "description": "Verified hits."
     },
     "pages": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct paths with verified hits."
     },
     "unverified": {
      "type": "integer",
      "minimum": 0,
      "description": "Hits claiming this bot from outside its vendor's IP ranges."
     },
     "blocked": {
      "type": "integer",
      "minimum": 0,
      "description": "Verified hits answered with 401, 403 or 429."
     },
     "last_seen": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Unix seconds of the latest hour with a verified hit."
     }
    },
    "required": [
     "bot",
     "vendor",
     "purpose",
     "hits",
     "pages",
     "unverified",
     "blocked",
     "last_seen"
    ],
    "additionalProperties": false
   },
   "CrawlerPageRow": {
    "type": "object",
    "properties": {
     "path": {
      "type": "string",
      "example": "/pricing"
     },
     "hits": {
      "type": "integer",
      "minimum": 0,
      "description": "Verified hits."
     },
     "bots": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct bots with verified hits."
     },
     "last_seen": {
      "type": "integer",
      "description": "Unix seconds of the latest hour with a verified hit."
     }
    },
    "required": [
     "path",
     "hits",
     "bots",
     "last_seen"
    ],
    "additionalProperties": false
   },
   "CrawlerOverview": {
    "type": "object",
    "properties": {
     "granularity": {
      "type": "string",
      "enum": [
       "hour",
       "day"
      ],
      "description": "Hourly up to 4 days, daily beyond."
     },
     "totals": {
      "$ref": "#/components/schemas/CrawlerTotals"
     },
     "previous": {
      "$ref": "#/components/schemas/CrawlerTotals",
      "description": "Totals for the same clock times, moved back by as many calendar days as the range covers in tz."
     },
     "series": {
      "type": "array",
      "description": "Zero-filled buckets in tz.",
      "items": {
       "$ref": "#/components/schemas/CrawlerSeriesPoint"
      }
     },
     "bots": {
      "type": "array",
      "maxItems": 100,
      "description": "Ordered by verified hits, then unverified hits.",
      "items": {
       "$ref": "#/components/schemas/CrawlerBotRow"
      }
     },
     "pages": {
      "type": "array",
      "maxItems": 100,
      "description": "Top paths by verified hits.",
      "items": {
       "$ref": "#/components/schemas/CrawlerPageRow"
      }
     },
     "pages_capped": {
      "type": "boolean",
      "description": "More paths had verified hits than pages lists."
     },
     "statuses": {
      "type": "array",
      "description": "Verified hits per response status.",
      "items": {
       "type": "object",
       "properties": {
        "status": {
         "type": "integer",
         "example": 200
        },
        "hits": {
         "type": "integer",
         "minimum": 0
        }
       },
       "required": [
        "status",
        "hits"
       ],
       "additionalProperties": false
      }
     },
     "synced_until": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Hits are complete up to this time."
     }
    },
    "required": [
     "granularity",
     "totals",
     "previous",
     "series",
     "bots",
     "pages",
     "pages_capped",
     "statuses",
     "synced_until"
    ],
    "additionalProperties": false
   },
   "RetentionTotals": {
    "type": "object",
    "required": [
     "visitors",
     "new",
     "returning"
    ],
    "properties": {
     "visitors": {
      "type": "integer",
      "minimum": 0
     },
     "new": {
      "type": "integer",
      "minimum": 0,
      "description": "Browsers whose first visit is in the period."
     },
     "returning": {
      "type": "integer",
      "minimum": 0,
      "description": "Browsers first seen before the period."
     }
    }
   },
   "RetentionResponse": {
    "type": "object",
    "required": [
     "totals",
     "previous",
     "series",
     "cohorts",
     "week",
     "curve",
     "window"
    ],
    "properties": {
     "totals": {
      "$ref": "#/components/schemas/RetentionTotals"
     },
     "previous": {
      "$ref": "#/components/schemas/RetentionTotals",
      "description": "The days right before window.from, as many as window.from through window.to cover."
     },
     "series": {
      "type": "array",
      "items": {
       "type": "object",
       "required": [
        "day",
        "new",
        "returning"
       ],
       "properties": {
        "day": {
         "type": "string",
         "format": "date"
        },
        "new": {
         "type": "integer",
         "minimum": 0
        },
        "returning": {
         "type": "integer",
         "minimum": 0
        }
       }
      },
      "description": "One row per day from the first day in the range with a check-in through window.to; later days without check-ins are zero, and a range without check-ins returns an empty array."
     },
     "cohorts": {
      "type": "array",
      "items": {
       "type": "object",
       "required": [
        "week",
        "size",
        "visitors"
       ],
       "properties": {
        "week": {
         "type": "string",
         "format": "date",
         "description": "Monday of the cohort week."
        },
        "size": {
         "type": "integer",
         "minimum": 0,
         "description": "New browsers in the cohort week; equals visitors[0]."
        },
        "visitors": {
         "type": "array",
         "items": {
          "type": "integer",
          "minimum": 0
         },
         "description": "Index 0 is the cohort size; index n counts cohort browsers active in week n."
        }
       }
      }
     },
     "week": {
      "type": "string",
      "format": "date",
      "description": "Monday of the current UTC week."
     },
     "curve": {
      "type": "array",
      "maxItems": 12,
      "description": "One row per follow-up week that at least one cohort has completed. A cohort counts for week n only once that week has ended.",
      "items": {
       "type": "object",
       "required": [
        "week",
        "retained_share",
        "best_share",
        "worst_share",
        "cohorts",
        "size"
       ],
       "properties": {
        "week": {
         "type": "integer",
         "minimum": 1,
         "maximum": 12
        },
        "retained_share": {
         "type": "number",
         "minimum": 0,
         "description": "Returning browsers divided by the summed size of the cohorts that completed this week."
        },
        "best_share": {
         "type": "number",
         "minimum": 0,
         "description": "Highest single-cohort share."
        },
        "worst_share": {
         "type": "number",
         "minimum": 0,
         "description": "Lowest single-cohort share."
        },
        "cohorts": {
         "type": "integer",
         "minimum": 1,
         "description": "Cohorts that completed this week."
        },
        "size": {
         "type": "integer",
         "minimum": 1,
         "description": "Summed size of those cohorts."
        }
       }
      }
     },
     "window": {
      "type": "object",
      "description": "The resolved days of the request.",
      "required": [
       "from",
       "to",
       "previous_from",
       "previous_to",
       "first_day"
      ],
      "properties": {
       "from": {
        "type": "string",
        "format": "date"
       },
       "to": {
        "type": "string",
        "format": "date",
        "description": "Never after the current UTC date."
       },
       "previous_from": {
        "type": "string",
        "format": "date"
       },
       "previous_to": {
        "type": "string",
        "format": "date"
       },
       "first_day": {
        "type": [
         "string",
         "null"
        ],
        "format": "date",
        "description": "First day with retention data for the site; null before the first check-in."
       }
      }
     }
    }
   },
   "GoalFilter": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "enum": [
       "page",
       "referrer",
       "country",
       "device",
       "browser",
       "os",
       "utm_source",
       "utm_medium",
       "utm_campaign",
       "event"
      ]
     },
     "op": {
      "type": "string",
      "enum": [
       "eq",
       "prefix",
       "contains"
      ],
      "description": "prefix and contains only work on page. page eq also matches one trailing slash; prefix matches whole path segments, so /docs matches /docs/setup but not /docsomething, and / matches every path."
     },
     "value": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256,
      "description": "page values start with / and can't contain ? or #. Event names use letters, digits and _. Other values must equal the breakdown row name."
     }
    },
    "required": [
     "key",
     "op",
     "value"
    ],
    "additionalProperties": false
   },
   "GoalStep": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 40,
      "description": "Optional label, trimmed; an empty name is dropped. Stats name unnamed steps `Step n`."
     },
     "match": {
      "type": "string",
      "enum": [
       "pageview",
       "event"
      ],
      "description": "event steps need exactly one event filter; event filters need match event."
     },
     "filters": {
      "type": "array",
      "maxItems": 6,
      "items": {
       "$ref": "#/components/schemas/GoalFilter"
      },
      "description": "Repeated keys match any of their values; different keys must all match. A funnel pageview step without filters matches any pageview."
     }
    },
    "required": [
     "match",
     "filters"
    ],
    "additionalProperties": false
   },
   "GoalInput": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
     },
     "kind": {
      "type": "string",
      "enum": [
       "goal",
       "funnel"
      ]
     },
     "steps": {
      "type": "array",
      "minItems": 1,
      "maxItems": 6,
      "items": {
       "$ref": "#/components/schemas/GoalStep"
      },
      "description": "A goal has one step, a funnel 2 to 6. Consecutive steps must differ."
     }
    },
    "required": [
     "name",
     "kind",
     "steps"
    ],
    "additionalProperties": false
   },
   "Goal": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "kind": {
      "type": "string",
      "enum": [
       "goal",
       "funnel"
      ]
     },
     "steps": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/GoalStep"
      }
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "invalid": {
      "type": "boolean",
      "const": true,
      "description": "Present when the stored goal no longer passes validation."
     }
    },
    "required": [
     "id",
     "name",
     "kind",
     "steps",
     "created_at",
     "updated_at"
    ],
    "additionalProperties": false
   },
   "GoalsResponse": {
    "type": "object",
    "properties": {
     "goals": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Goal"
      }
     }
    },
    "required": [
     "goals"
    ],
    "additionalProperties": false
   },
   "DeletedGoal": {
    "type": "object",
    "properties": {
     "deleted": {
      "type": "string"
     }
    },
    "required": [
     "deleted"
    ],
    "additionalProperties": false
   },
   "GoalStatsResponse": {
    "type": "object",
    "properties": {
     "granularity": {
      "type": "string",
      "enum": [
       "hour",
       "day"
      ]
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Identified visitor-days in the range."
     },
     "previous_visitors": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 0,
      "description": "Identified visitor-days in the previous window; null when under 99% of its pageviews are identified, or none are."
     },
     "coverage": {
      "type": "object",
      "properties": {
       "pageviews": {
        "type": "integer",
        "minimum": 0
       },
       "identified": {
        "type": "integer",
        "minimum": 0
       },
       "previous": {
        "type": "object",
        "properties": {
         "pageviews": {
          "type": "integer",
          "minimum": 0
         },
         "identified": {
          "type": "integer",
          "minimum": 0
         }
        },
        "required": [
         "pageviews",
         "identified"
        ],
        "additionalProperties": false
       }
      },
      "required": [
       "pageviews",
       "identified",
       "previous"
      ],
      "additionalProperties": false
     },
     "goals": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "kind": {
         "type": "string",
         "const": "goal"
        },
        "conversions": {
         "type": "integer",
         "minimum": 0,
         "description": "Matching pageviews or events."
        },
        "converting_visitors": {
         "type": "integer",
         "minimum": 0
        },
        "rate": {
         "type": [
          "number",
          "null"
         ],
         "minimum": 0,
         "maximum": 1,
         "description": "converting_visitors / visitors, null without visitors."
        },
        "previous": {
         "type": [
          "object",
          "null"
         ],
         "properties": {
          "conversions": {
           "type": "integer",
           "minimum": 0
          },
          "converting_visitors": {
           "type": "integer",
           "minimum": 0
          },
          "rate": {
           "type": [
            "number",
            "null"
           ],
           "minimum": 0,
           "maximum": 1
          }
         },
         "required": [
          "conversions",
          "converting_visitors",
          "rate"
         ],
         "additionalProperties": false
        },
        "series": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "t": {
            "type": "integer",
            "description": "Bucket start, Unix seconds."
           },
           "conversions": {
            "type": "integer",
            "minimum": 0
           }
          },
          "required": [
           "t",
           "conversions"
          ],
          "additionalProperties": false
         }
        }
       },
       "required": [
        "id",
        "name",
        "kind",
        "conversions",
        "converting_visitors",
        "rate",
        "previous",
        "series"
       ],
       "additionalProperties": false
      }
     },
     "funnels": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "kind": {
         "type": "string",
         "const": "funnel"
        },
        "steps": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "name": {
            "type": "string"
           },
           "visitors": {
            "type": "integer",
            "minimum": 0
           }
          },
          "required": [
           "name",
           "visitors"
          ],
          "additionalProperties": false
         }
        },
        "previous": {
         "type": [
          "array",
          "null"
         ],
         "items": {
          "type": "integer",
          "minimum": 0
         },
         "description": "Visitors per step in the previous window."
        }
       },
       "required": [
        "id",
        "name",
        "kind",
        "steps",
        "previous"
       ],
       "additionalProperties": false
      }
     },
     "invalid": {
      "type": "array",
      "description": "Stored goals skipped because they no longer pass validation.",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string"
        },
        "name": {
         "type": "string"
        }
       },
       "required": [
        "id",
        "name"
       ],
       "additionalProperties": false
      }
     }
    },
    "required": [
     "granularity",
     "visitors",
     "previous_visitors",
     "coverage",
     "goals",
     "funnels",
     "invalid"
    ],
    "additionalProperties": false
   },
   "GoalDetailResponse": {
    "type": "object",
    "properties": {
     "dim": {
      "type": "string"
     },
     "breakdown": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string",
         "description": "Entry value; no value reads Direct / none for referrers and / for pages."
        },
        "visitors": {
         "type": "integer",
         "minimum": 0,
         "description": "Visitor-days whose first pageview had this value."
        },
        "steps": {
         "type": "array",
         "items": {
          "type": "integer",
          "minimum": 0
         },
         "description": "Visitor-days that reached each step; one entry for a goal."
        }
       },
       "required": [
        "name",
        "visitors",
        "steps"
       ],
       "additionalProperties": false
      }
     },
     "timing": {
      "type": [
       "array",
       "null"
      ],
      "description": "One entry per step transition; null for goals.",
      "items": {
       "type": "object",
       "properties": {
        "p50_s": {
         "type": [
          "number",
          "null"
         ],
         "minimum": 0,
         "description": "Median seconds; null when no visitor-day made this transition."
        },
        "p90_s": {
         "type": [
          "number",
          "null"
         ],
         "minimum": 0,
         "description": "90th percentile seconds; null when no visitor-day made this transition."
        },
        "n": {
         "type": "integer",
         "minimum": 0,
         "description": "Visitor-days that made this transition."
        }
       },
       "required": [
        "p50_s",
        "p90_s",
        "n"
       ],
       "additionalProperties": false
      }
     }
    },
    "required": [
     "dim",
     "breakdown",
     "timing"
    ],
    "additionalProperties": false
   },
   "ApiAlertRule": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "metric": {
      "type": "string",
      "enum": [
       "error_rate",
       "p95_ms",
       "silence"
      ],
      "description": "error_rate: 5xx share in percent. p95_ms: p95 latency. silence: no requests while the same window yesterday had at least min_requests."
     },
     "method": {
      "type": [
       "string",
       "null"
      ],
      "description": "Endpoint method; null with route for the whole API."
     },
     "route": {
      "type": [
       "string",
       "null"
      ],
      "description": "Route template as shown in the API view, such as /v1/orders/:id."
     },
     "threshold": {
      "type": [
       "number",
       "null"
      ],
      "description": "Percent for error_rate, milliseconds for p95_ms, null for silence. Fires when the value is above it."
     },
     "window_minutes": {
      "type": "integer",
      "enum": [
       5,
       15,
       60
      ]
     },
     "min_requests": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000000,
      "description": "Windows with fewer requests never fire, and a firing rule resolves in them. For silence, the traffic yesterday's window needs."
     },
     "recipients": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "email"
      },
      "minItems": 1,
      "maxItems": 10
     },
     "enabled": {
      "type": "boolean"
     },
     "state": {
      "type": "string",
      "enum": [
       "ok",
       "firing"
      ]
     },
     "state_changed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "last_value": {
      "type": [
       "number",
       "null"
      ],
      "description": "Value that caused the latest state change."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "metric",
     "method",
     "route",
     "threshold",
     "window_minutes",
     "min_requests",
     "recipients",
     "enabled",
     "state",
     "state_changed_at",
     "last_value",
     "created_at"
    ],
    "additionalProperties": false
   },
   "ApiAlertRulesResponse": {
    "type": "object",
    "properties": {
     "rules": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ApiAlertRule"
      }
     }
    },
    "required": [
     "rules"
    ],
    "additionalProperties": false
   },
   "CreateApiAlertRule": {
    "type": "object",
    "properties": {
     "metric": {
      "type": "string",
      "enum": [
       "error_rate",
       "p95_ms",
       "silence"
      ]
     },
     "threshold": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "Required for error_rate (percent, below 100) and p95_ms (ms, below 600000); omit or null for silence."
     },
     "window_minutes": {
      "type": "integer",
      "enum": [
       5,
       15,
       60
      ]
     },
     "method": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^[A-Z]{1,16}$",
      "description": "Set together with route to scope the rule to one endpoint."
     },
     "route": {
      "type": [
       "string",
       "null"
      ],
      "minLength": 1,
      "maxLength": 256
     },
     "min_requests": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000000,
      "default": 20
     },
     "recipients": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "email"
      },
      "minItems": 1,
      "maxItems": 10,
      "description": "Trimmed, lowercased and de-duplicated. Defaults to the signed-in owner's email; a management key has none, so send recipients with it."
     },
     "enabled": {
      "type": "boolean",
      "default": true
     }
    },
    "required": [
     "metric",
     "window_minutes"
    ],
    "additionalProperties": false
   },
   "UpdateApiAlertRule": {
    "type": "object",
    "description": "Only enabled can change; pausing resets the rule to ok. Delete and re-create to change anything else.",
    "properties": {
     "enabled": {
      "type": "boolean"
     }
    },
    "required": [
     "enabled"
    ],
    "additionalProperties": false
   },
   "IngestMetricRow": {
    "type": "object",
    "required": [
     "minute",
     "method",
     "route",
     "status",
     "count",
     "duration_ms_sum"
    ],
    "properties": {
     "minute": {
      "type": "integer",
      "description": "Unix seconds, floored to the minute. At most 7 days old or 5 minutes ahead."
     },
     "method": {
      "type": "string",
      "description": "HTTP method. Uppercased, letters only, at most 16; OTHER when nothing is left."
     },
     "route": {
      "type": "string",
      "description": "Route template such as /users/:id. Raw paths are templated (ids, UUIDs, dates, emails), query strings dropped, at most 12 segments and 256 characters. A 404 whose templated route has no :, *, {, < or [ is stored as /*."
     },
     "status": {
      "type": "integer",
      "minimum": 100,
      "maximum": 599
     },
     "count": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000000000,
      "description": "Requests in this row."
     },
     "duration_ms_sum": {
      "type": "number",
      "minimum": 0,
      "description": "Sum of their durations in milliseconds."
     },
     "histogram": {
      "type": "object",
      "description": "Latency buckets as bucket index (0-250) to request count. Bucket 0 holds durations of 1 ms or less; otherwise min(ceil(ln(ms) / ln(1.08)), 250). Invalid entries are ignored. Rows without it count toward requests and the average duration, but not toward percentiles.",
      "additionalProperties": {
       "type": "integer",
       "minimum": 1
      }
     },
     "user_agent": {
      "type": "string",
      "description": "Caller User-Agent, at most 512 characters; classified into a client name and version."
     },
     "consumer": {
      "type": "string",
      "description": "Opaque caller id, at most 128 characters."
     }
    }
   },
   "IngestErrorRow": {
    "type": "object",
    "required": [
     "ts",
     "method",
     "status"
    ],
    "properties": {
     "ts": {
      "type": "integer",
      "description": "Unix milliseconds of the request. At most 7 days old or 5 minutes ahead."
     },
     "method": {
      "type": "string",
      "description": "As in metric rows."
     },
     "route": {
      "type": "string",
      "description": "Route template; falls back to path. One of route or path is required."
     },
     "path": {
      "type": "string",
      "description": "Raw path, at most 512 characters; query string and fragment are removed."
     },
     "status": {
      "type": "integer",
      "minimum": 400,
      "maximum": 599
     },
     "duration_ms": {
      "type": "number",
      "minimum": 0,
      "description": "Duration in milliseconds, capped at 24 hours. Missing or negative values are stored as 0."
     },
     "user_agent": {
      "type": "string",
      "description": "As in metric rows."
     },
     "consumer": {
      "type": "string",
      "description": "As in metric rows."
     },
     "message": {
      "type": "string",
      "description": "Error message, at most 1000 characters."
     }
    }
   },
   "IngestBatch": {
    "type": "object",
    "required": [
     "v",
     "batch_id"
    ],
    "properties": {
     "v": {
      "type": "integer",
      "const": 1,
      "description": "Wire version."
     },
     "batch_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{8,64}$",
      "description": "Idempotency key. New for every batch of new data; resend the identical body with the same batch_id when retrying. A reused batch_id with different data is dropped as a duplicate."
     },
     "sdk": {
      "type": "string",
      "description": "Informational sender name and version."
     },
     "metrics": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IngestMetricRow"
      },
      "description": "Aggregated rows. Only the first 5000 are read; the rest count as rejected."
     },
     "errors": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IngestErrorRow"
      },
      "description": "Individual 4xx and 5xx requests. Only the first 200 are read; the rest count as rejected. They are samples only: they add nothing to request counts and create no error groups, so count failed requests in metrics too."
     }
    }
   },
   "IngestAccepted": {
    "type": "object",
    "required": [
     "accepted",
     "rejected"
    ],
    "properties": {
     "accepted": {
      "type": "object",
      "required": [
       "metrics",
       "errors"
      ],
      "properties": {
       "metrics": {
        "type": "integer",
        "minimum": 0,
        "description": "Valid metric rows, counted before rows with the same key are merged."
       },
       "errors": {
        "type": "integer",
        "minimum": 0,
        "description": "Valid error rows."
       }
      }
     },
     "rejected": {
      "type": "integer",
      "minimum": 0,
      "description": "Rows that failed validation or exceeded the per-batch caps."
     }
    }
   },
   "ApiKey": {
    "type": "object",
    "required": [
     "id",
     "label",
     "prefix",
     "created_at",
     "last_used_at",
     "created_with"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "label": {
      "type": "string",
      "maxLength": 64
     },
     "prefix": {
      "type": "string",
      "description": "First 11 characters of the key, for recognizing it."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_used_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "null until a batch sent with the key is stored; updated at most once a minute."
     },
     "created_with": {
      "type": [
       "object",
       "null"
      ],
      "required": [
       "id",
       "label"
      ],
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 64
       }
      },
      "description": "The management key that created this key, or null when it was created while signed in. Stays set after that management key is revoked."
     }
    }
   },
   "ApiKeysResponse": {
    "type": "object",
    "required": [
     "keys"
    ],
    "properties": {
     "keys": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ApiKey"
      },
      "description": "Active keys, newest first."
     }
    }
   },
   "CreateApiKey": {
    "type": "object",
    "properties": {
     "label": {
      "type": "string",
      "description": "Trimmed and cut to 64 characters. Defaults to empty."
     }
    }
   },
   "CreatedApiKey": {
    "type": "object",
    "required": [
     "key",
     "secret"
    ],
    "properties": {
     "key": {
      "$ref": "#/components/schemas/ApiKey"
     },
     "secret": {
      "type": "string",
      "pattern": "^tt_[0-9a-f]{48}$",
      "description": "The full key. Only returned here."
     }
    }
   },
   "RevokedApiKey": {
    "type": "object",
    "required": [
     "revoked"
    ],
    "properties": {
     "revoked": {
      "type": "string",
      "description": "The revoked key id."
     }
    }
   },
   "ApiTotals": {
    "type": "object",
    "required": [
     "requests",
     "client_errors",
     "server_errors",
     "avg_ms",
     "p50_ms",
     "p95_ms",
     "p99_ms"
    ],
    "properties": {
     "requests": {
      "type": "integer",
      "minimum": 0,
      "description": "Requests in the range."
     },
     "client_errors": {
      "type": "integer",
      "minimum": 0,
      "description": "4xx responses."
     },
     "server_errors": {
      "type": "integer",
      "minimum": 0,
      "description": "5xx responses."
     },
     "avg_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "Average duration; null without requests."
     },
     "p50_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "Median duration from latency buckets; null without requests that carry latency buckets."
     },
     "p95_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "95th percentile duration; null without requests that carry latency buckets."
     },
     "p99_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "99th percentile duration; null without requests that carry latency buckets."
     }
    },
    "description": "Latency values are milliseconds, whole numbers from 100 ms and one decimal below."
   },
   "ApiSeriesPoint": {
    "type": "object",
    "required": [
     "t",
     "s2xx",
     "s3xx",
     "s4xx",
     "s5xx",
     "p50_ms",
     "p95_ms"
    ],
    "properties": {
     "t": {
      "type": "integer",
      "description": "Bucket start, Unix seconds."
     },
     "s2xx": {
      "type": "integer",
      "minimum": 0,
      "description": "1xx and 2xx responses."
     },
     "s3xx": {
      "type": "integer",
      "minimum": 0,
      "description": "3xx responses."
     },
     "s4xx": {
      "type": "integer",
      "minimum": 0,
      "description": "4xx responses."
     },
     "s5xx": {
      "type": "integer",
      "minimum": 0,
      "description": "5xx responses."
     },
     "p50_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "null when no request in the bucket carries latency buckets."
     },
     "p95_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "null when no request in the bucket carries latency buckets."
     }
    }
   },
   "ApiOverviewResponse": {
    "type": "object",
    "required": [
     "totals",
     "previous",
     "series",
     "granularity",
     "has_data"
    ],
    "properties": {
     "totals": {
      "$ref": "#/components/schemas/ApiTotals"
     },
     "previous": {
      "$ref": "#/components/schemas/ApiTotals",
      "description": "Same totals for the same clock times, moved back by as many calendar days as the range covers in tz."
     },
     "series": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ApiSeriesPoint"
      },
      "description": "Every bucket in the range; empty buckets have zero counts and null latency."
     },
     "granularity": {
      "type": "string",
      "enum": [
       "hour",
       "day"
      ],
      "description": "hour for ranges up to 4 days, otherwise day, in tz."
     },
     "has_data": {
      "type": "boolean",
      "description": "Whether the site has received any API requests since registration, regardless of range and filters."
     }
    }
   },
   "ApiBreakdownRow": {
    "type": "object",
    "required": [
     "name",
     "requests",
     "client_errors",
     "server_errors",
     "p50_ms",
     "p95_ms",
     "p99_ms"
    ],
    "properties": {
     "name": {
      "type": "string",
      "description": "Row value; usable as the filter value for this dimension."
     },
     "method": {
      "type": "string",
      "description": "Endpoints only."
     },
     "route": {
      "type": "string",
      "description": "Endpoints only."
     },
     "requests": {
      "type": "integer",
      "minimum": 0,
      "description": "Requests."
     },
     "client_errors": {
      "type": "integer",
      "minimum": 0,
      "description": "4xx responses."
     },
     "server_errors": {
      "type": "integer",
      "minimum": 0,
      "description": "5xx responses."
     },
     "p50_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "Median duration. null without requests that carry latency buckets."
     },
     "p95_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "95th percentile duration. null without requests that carry latency buckets."
     },
     "p99_ms": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "description": "99th percentile duration. null without requests that carry latency buckets."
     }
    }
   },
   "ApiBreakdownResponse": {
    "type": "object",
    "required": [
     "rows"
    ],
    "properties": {
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ApiBreakdownRow"
      },
      "description": "Sorted by requests, highest first."
     }
    }
   },
   "ApiErrorGroup": {
    "type": "object",
    "required": [
     "status",
     "method",
     "route",
     "requests",
     "endpoint_requests",
     "samples",
     "last_seen"
    ],
    "properties": {
     "status": {
      "type": "integer",
      "minimum": 400,
      "maximum": 599
     },
     "method": {
      "type": "string"
     },
     "route": {
      "type": "string"
     },
     "requests": {
      "type": "integer",
      "minimum": 0,
      "description": "Requests to this endpoint that returned this status."
     },
     "endpoint_requests": {
      "type": "integer",
      "minimum": 0,
      "description": "All requests to this method and route in the range; a status filter does not narrow it."
     },
     "samples": {
      "type": "integer",
      "minimum": 0,
      "description": "Stored samples for this group in the range."
     },
     "last_seen": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Unix seconds of the newest sample; null without samples. Samples are kept for 14 days."
     }
    }
   },
   "ApiErrorsResponse": {
    "type": "object",
    "required": [
     "rows"
    ],
    "properties": {
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ApiErrorGroup"
      },
      "description": "5xx groups first, then by requests."
     }
    }
   },
   "ApiErrorSample": {
    "type": "object",
    "required": [
     "ts",
     "method",
     "route",
     "path",
     "status",
     "duration_ms",
     "client",
     "client_version",
     "consumer",
     "message"
    ],
    "properties": {
     "ts": {
      "type": "integer",
      "description": "Unix milliseconds."
     },
     "method": {
      "type": "string"
     },
     "route": {
      "type": "string"
     },
     "path": {
      "type": "string",
      "description": "Raw path without query string."
     },
     "status": {
      "type": "integer",
      "minimum": 400,
      "maximum": 599
     },
     "duration_ms": {
      "type": "number",
      "minimum": 0,
      "description": "Rounded to 0.1 ms."
     },
     "client": {
      "type": "string"
     },
     "client_version": {
      "type": "string"
     },
     "consumer": {
      "type": "string"
     },
     "message": {
      "type": "string",
      "description": "Empty unless the sender attached one."
     }
    }
   },
   "ApiSamplesResponse": {
    "type": "object",
    "required": [
     "rows"
    ],
    "properties": {
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ApiErrorSample"
      },
      "description": "Newest first, from the last 14 days."
     }
    }
   },
   "VerifyMethodResult": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "detail": {
      "type": "string"
     }
    },
    "required": [
     "ok",
     "detail"
    ],
    "additionalProperties": false
   },
   "SiteVerification": {
    "oneOf": [
     {
      "type": "object",
      "properties": {
       "hostname": {
        "type": "string"
       },
       "verified_at": {
        "type": "string",
        "format": "date-time"
       },
       "already": {
        "type": "boolean",
        "const": true
       }
      },
      "required": [
       "hostname",
       "verified_at",
       "already"
      ],
      "additionalProperties": false
     },
     {
      "type": "object",
      "properties": {
       "hostname": {
        "type": "string"
       },
       "verified_at": {
        "type": "string",
        "format": "date-time"
       },
       "tag": {
        "$ref": "#/components/schemas/VerifyMethodResult"
       },
       "dns": {
        "$ref": "#/components/schemas/VerifyMethodResult"
       },
       "file": {
        "$ref": "#/components/schemas/VerifyMethodResult"
       }
      },
      "required": [
       "hostname",
       "verified_at",
       "tag",
       "dns",
       "file"
      ],
      "additionalProperties": false
     }
    ]
   },
   "SiteVerificationPending": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string",
      "const": "verification not found yet"
     },
     "hostname": {
      "type": "string"
     },
     "tag": {
      "$ref": "#/components/schemas/VerifyMethodResult"
     },
     "dns": {
      "$ref": "#/components/schemas/VerifyMethodResult"
     },
     "file": {
      "$ref": "#/components/schemas/VerifyMethodResult"
     }
    },
    "required": [
     "error",
     "hostname",
     "tag",
     "dns",
     "file"
    ],
    "additionalProperties": false
   },
   "CspCapability": {
    "type": "object",
    "properties": {
     "verdict": {
      "type": "string",
      "enum": [
       "allowed",
       "blocked",
       "unknown"
      ]
     },
     "directive": {
      "type": "string"
     },
     "detail": {
      "type": "string"
     }
    },
    "required": [
     "verdict",
     "directive",
     "detail"
    ],
    "additionalProperties": false
   },
   "CspReport": {
    "type": "object",
    "properties": {
     "present": {
      "type": "boolean"
     },
     "script": {
      "$ref": "#/components/schemas/CspCapability"
     },
     "beacon": {
      "$ref": "#/components/schemas/CspCapability"
     },
     "pixel": {
      "$ref": "#/components/schemas/CspCapability"
     }
    },
    "required": [
     "present",
     "script",
     "beacon",
     "pixel"
    ],
    "additionalProperties": false
   },
   "SetupCheck": {
    "oneOf": [
     {
      "type": "object",
      "properties": {
       "hostname": {
        "type": "string"
       },
       "reachable": {
        "type": "boolean",
        "const": false
       },
       "error": {
        "type": "string"
       }
      },
      "required": [
       "hostname",
       "reachable",
       "error"
      ],
      "additionalProperties": false
     },
     {
      "type": "object",
      "properties": {
       "hostname": {
        "type": "string"
       },
       "reachable": {
        "type": "boolean",
        "const": true
       },
       "url": {
        "type": "string",
        "format": "uri"
       },
       "status": {
        "type": "integer"
       },
       "verified_at": {
        "type": [
         "string",
         "null"
        ],
        "format": "date-time"
       },
       "tag_host": {
        "type": [
         "string",
         "null"
        ],
        "description": "Clicktag host serving a tag that reports to this site, or null when there is none."
       },
       "tag_reports_to": {
        "type": [
         "string",
         "null"
        ],
        "description": "When tags exist but none report to this site, where the first one reports to: its data-hostname, or the page hostname without one. Otherwise null."
       },
       "tag_verifies": {
        "type": [
         "boolean",
         "null"
        ],
        "description": "While the site is unverified, whether a tag that reports to it carries the site's data-verify token, which auto-verification needs. Null once the site is verified or when no tag reports to it."
       },
       "csp": {
        "$ref": "#/components/schemas/CspReport"
       }
      },
      "required": [
       "hostname",
       "reachable",
       "url",
       "status",
       "verified_at",
       "tag_host",
       "tag_reports_to",
       "tag_verifies",
       "csp"
      ],
      "additionalProperties": false
     }
    ]
   },
   "ReportsMode": {
    "type": "string",
    "enum": [
     "inherit",
     "custom",
     "off"
    ],
    "description": "inherit sends the workspace defaults, custom sends this site's own recipients, off sends nothing. Applied by the hourly fan-out. The first switch to custom copies the default recipients, schedule and sections into the site as its own subscriptions."
   },
   "ReportsEffective": {
    "type": "object",
    "description": "What the site sends, computed from the workspace defaults and the site's own rows.",
    "properties": {
     "layer": {
      "type": "string",
      "enum": [
       "default",
       "custom",
       "off"
      ]
     },
     "sends": {
      "type": "boolean"
     },
     "reason": {
      "type": "string",
      "enum": [
       "unverified",
       "no_defaults",
       "defaults_disabled",
       "no_recipients",
       "off"
      ],
      "description": "Only when sends is false."
     },
     "recipients": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "email"
      },
      "description": "Only when sends is true. For custom: enabled recipients that have not failed 5 times in a row."
     },
     "frequency": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "daily",
       "weekly",
       "monthly",
       null
      ],
      "description": "Only when sends is true; null when custom recipients mix frequencies."
     },
     "timezone": {
      "type": [
       "string",
       "null"
      ],
      "description": "Only when sends is true; null when custom recipients mix timezones."
     },
     "send_hour": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 0,
      "maximum": 23,
      "description": "Only when sends is true; null when custom recipients mix hours."
     },
     "next_send_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Only when sends is true. Start of the hour of the next scheduled send."
     }
    },
    "required": [
     "layer",
     "sends"
    ],
    "additionalProperties": false
   },
   "ReportDefaults": {
    "type": "object",
    "properties": {
     "configured": {
      "type": "boolean",
      "description": "False until the first save; the other fields then hold the column defaults."
     },
     "enabled": {
      "type": "boolean"
     },
     "recipients": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "email"
      },
      "maxItems": 10
     },
     "frequency": {
      "type": "string",
      "enum": [
       "daily",
       "weekly",
       "monthly"
      ]
     },
     "timezone": {
      "type": "string",
      "description": "IANA timezone; send_hour is wall-clock here."
     },
     "send_hour": {
      "type": "integer",
      "minimum": 0,
      "maximum": 23
     },
     "sections": {
      "$ref": "#/components/schemas/ReportSections"
     },
     "sites": {
      "type": "object",
      "description": "Every site of the owner except the demo site.",
      "properties": {
       "inherit": {
        "type": "integer",
        "minimum": 0
       },
       "custom": {
        "type": "array",
        "items": {
         "type": "string"
        }
       },
       "off": {
        "type": "array",
        "items": {
         "type": "string"
        }
       }
      },
      "required": [
       "inherit",
       "custom",
       "off"
      ],
      "additionalProperties": false
     },
     "stalled": {
      "type": "array",
      "description": "Default recipients that failed 5 times in a row on an inheriting site. Saving the defaults or the digest retries them.",
      "items": {
       "type": "object",
       "properties": {
        "site": {
         "type": "string"
        },
        "email": {
         "type": "string",
         "format": "email"
        },
        "fail_count": {
         "type": "integer",
         "minimum": 5
        }
       },
       "required": [
        "site",
        "email",
        "fail_count"
       ],
       "additionalProperties": false
      }
     },
     "dropped": {
      "type": "array",
      "description": "Recipients removed by an unsubscribe or a hard bounce since the last save, oldest first.",
      "items": {
       "type": "object",
       "properties": {
        "email": {
         "type": "string",
         "format": "email"
        },
        "reason": {
         "type": "string",
         "enum": [
          "unsubscribed",
          "bounced"
         ]
        },
        "at": {
         "type": "string",
         "format": "date-time"
        }
       },
       "required": [
        "email",
        "reason",
        "at"
       ],
       "additionalProperties": false
      }
     },
     "updated_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "configured",
     "enabled",
     "recipients",
     "frequency",
     "timezone",
     "send_hour",
     "sections",
     "sites",
     "stalled",
     "dropped",
     "updated_at"
    ],
    "additionalProperties": false
   },
   "UpdateReportDefaults": {
    "type": "object",
    "description": "Any subset; omitted fields keep their stored value, or the column default on the first save. Only these fields are read.",
    "properties": {
     "enabled": {
      "type": "boolean"
     },
     "recipients": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "email"
      },
      "description": "Trimmed, lowercased and de-duplicated; at most 10 after that. Replaces the whole list."
     },
     "frequency": {
      "type": "string",
      "enum": [
       "daily",
       "weekly",
       "monthly"
      ]
     },
     "timezone": {
      "type": "string",
      "description": "IANA timezone, stored in canonical form (europe/amsterdam becomes Europe/Amsterdam). Fixed offsets such as +02:00 are refused."
     },
     "send_hour": {
      "type": "integer",
      "minimum": 0,
      "maximum": 23
     },
     "sections": {
      "$ref": "#/components/schemas/ReportSections"
     },
     "updated_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "The updated_at from the last read. When sent, the save is refused with 409 if the defaults changed since, for example after a recipient unsubscribed; null means no defaults saved yet."
     }
    },
    "additionalProperties": true
   },
   "WorkspaceSite": {
    "type": "object",
    "properties": {
     "hostname": {
      "type": "string",
      "description": "Owned verified website."
     },
     "display_name": {
      "type": "string",
      "description": "Display name, or the hostname when none is set."
     },
     "pinned": {
      "type": "boolean"
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct daily visitor hashes on canonical non-bot pageviews, as on each site's overview."
     },
     "pageviews": {
      "type": "integer",
      "minimum": 0,
      "description": "Canonical non-bot pageviews."
     },
     "previous": {
      "type": "object",
      "properties": {
       "visitors": {
        "type": "integer",
        "minimum": 0,
        "description": "Distinct daily visitor hashes on canonical non-bot pageviews, as on each site's overview."
       },
       "pageviews": {
        "type": "integer",
        "minimum": 0,
        "description": "Canonical non-bot pageviews."
       }
      },
      "required": [
       "visitors",
       "pageviews"
      ],
      "additionalProperties": false,
      "description": "The same clock times, moved back by as many calendar days as the range covers in tz."
     },
     "live": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct visitors in the last five minutes, independent of the range."
     }
    },
    "required": [
     "hostname",
     "display_name",
     "pinned",
     "visitors",
     "pageviews",
     "previous",
     "live"
    ],
    "additionalProperties": false
   },
   "WorkspaceSeries": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "description": "all, a hostname, or other for the sites beyond the five busiest."
     },
     "label": {
      "type": "string"
     },
     "visitors": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 0
      },
      "description": "Visitors per bucket."
     },
     "pageviews": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 0
      },
      "description": "Pageviews per bucket."
     }
    },
    "required": [
     "key",
     "label",
     "visitors",
     "pageviews"
    ],
    "additionalProperties": false
   },
   "WorkspaceApiSite": {
    "type": "object",
    "properties": {
     "hostname": {
      "type": "string"
     },
     "display_name": {
      "type": "string",
      "description": "Display name, or the hostname when none is set."
     },
     "requests": {
      "type": "integer",
      "minimum": 0,
      "description": "API requests."
     },
     "server_errors": {
      "type": "integer",
      "minimum": 0,
      "description": "API requests answered with a 5xx status."
     },
     "previous": {
      "type": "object",
      "properties": {
       "requests": {
        "type": "integer",
        "minimum": 0,
        "description": "API requests."
       },
       "server_errors": {
        "type": "integer",
        "minimum": 0,
        "description": "API requests answered with a 5xx status."
       }
      },
      "required": [
       "requests",
       "server_errors"
      ],
      "additionalProperties": false,
      "description": "The same clock times, moved back by as many calendar days as the range covers in tz."
     }
    },
    "required": [
     "hostname",
     "display_name",
     "requests",
     "server_errors",
     "previous"
    ],
    "additionalProperties": false
   },
   "WorkspaceOverviewResponse": {
    "type": "object",
    "properties": {
     "range": {
      "type": "object",
      "properties": {
       "from": {
        "type": "integer",
        "description": "Inclusive Unix seconds."
       },
       "to": {
        "type": "integer",
        "description": "Exclusive Unix seconds."
       },
       "granularity": {
        "type": "string",
        "enum": [
         "hour",
         "day"
        ],
        "description": "hour for ranges up to four days, otherwise day."
       }
      },
      "required": [
       "from",
       "to",
       "granularity"
      ],
      "additionalProperties": false
     },
     "counts": {
      "type": "object",
      "properties": {
       "web": {
        "type": "integer",
        "minimum": 0,
        "description": "Verified websites; every traffic number adds these up."
       },
       "api": {
        "type": "integer",
        "minimum": 0,
        "description": "API sites (kind api), verified or not."
       },
       "unverified": {
        "type": "integer",
        "minimum": 0,
        "description": "Websites waiting for verification; they are left out of every number."
       }
      },
      "required": [
       "web",
       "api",
       "unverified"
      ],
      "additionalProperties": false
     },
     "totals": {
      "type": "object",
      "properties": {
       "visitors": {
        "type": "integer",
        "minimum": 0,
        "description": "Distinct daily visitor hashes on canonical non-bot pageviews, as on each site's overview."
       },
       "pageviews": {
        "type": "integer",
        "minimum": 0,
        "description": "Canonical non-bot pageviews."
       },
       "live": {
        "type": "integer",
        "minimum": 0,
        "description": "Distinct visitors in the last five minutes, summed over sites, independent of the range."
       },
       "active_sites": {
        "type": "integer",
        "minimum": 0,
        "description": "Verified websites with at least one pageview in the range."
       }
      },
      "required": [
       "visitors",
       "pageviews",
       "live",
       "active_sites"
      ],
      "additionalProperties": false
     },
     "previous": {
      "type": "object",
      "properties": {
       "visitors": {
        "type": "integer",
        "minimum": 0,
        "description": "Distinct daily visitor hashes on canonical non-bot pageviews, as on each site's overview."
       },
       "pageviews": {
        "type": "integer",
        "minimum": 0,
        "description": "Canonical non-bot pageviews."
       },
       "active_sites": {
        "type": "integer",
        "minimum": 0,
        "description": "Verified websites with at least one pageview in the previous span."
       }
      },
      "required": [
       "visitors",
       "pageviews",
       "active_sites"
      ],
      "additionalProperties": false,
      "description": "The same clock times, moved back by as many calendar days as the range covers in tz."
     },
     "buckets": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 0
      },
      "description": "Bucket start times in Unix seconds, ascending and zero-filled across the range in tz. Every spark and series array has this length; empty without verified websites."
     },
     "spark": {
      "type": "object",
      "properties": {
       "visitors": {
        "type": "array",
        "items": {
         "type": "integer",
         "minimum": 0
        },
        "description": "Visitors per bucket over all sites."
       },
       "pageviews": {
        "type": "array",
        "items": {
         "type": "integer",
         "minimum": 0
        },
        "description": "Pageviews per bucket over all sites."
       },
       "active_sites": {
        "type": "array",
        "items": {
         "type": "integer",
         "minimum": 0
        },
        "description": "Sites with at least one pageview per bucket."
       }
      },
      "required": [
       "visitors",
       "pageviews",
       "active_sites"
      ],
      "additionalProperties": false
     },
     "series": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WorkspaceSeries"
      },
      "description": "all first, then one line per site by visitors descending; with more than six sites the five busiest plus other."
     },
     "sites": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WorkspaceSite"
      },
      "description": "Verified websites, pinned first, then by visitors descending, then hostname."
     },
     "api": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WorkspaceApiSite"
      },
      "description": "Sites with API requests in either span, by requests descending. Counted in whole UTC hours from each site's registration."
     }
    },
    "required": [
     "range",
     "counts",
     "totals",
     "previous",
     "buckets",
     "spark",
     "series",
     "sites",
     "api"
    ],
    "additionalProperties": false
   },
   "WorkspacePageRow": {
    "type": "object",
    "properties": {
     "site": {
      "type": "string"
     },
     "name": {
      "type": "string",
      "description": "Page path (empty becomes /) or event name."
     },
     "value": {
      "type": "integer",
      "minimum": 0,
      "description": "Pageviews, or event fires for events."
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct nonempty visitor hashes. For pages, pageviews without a hash (mainly imports and older data) add their unique-entry flag; for events, events without a hash add nothing."
     }
    },
    "required": [
     "site",
     "name",
     "value",
     "visitors"
    ],
    "additionalProperties": false
   },
   "WorkspaceSourceRow": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Source (empty becomes Direct / none) or ISO 3166-1 alpha-2 country code."
     },
     "value": {
      "type": "integer",
      "minimum": 0,
      "description": "Canonical non-bot pageviews."
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Distinct daily visitor hashes on canonical non-bot pageviews, as on each site's overview."
     },
     "sites": {
      "type": "integer",
      "minimum": 0,
      "description": "Sites the row was seen on."
     }
    },
    "required": [
     "name",
     "value",
     "visitors",
     "sites"
    ],
    "additionalProperties": false
   },
   "WorkspaceGoalRow": {
    "type": "object",
    "properties": {
     "site": {
      "type": "string"
     },
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "kind": {
      "type": "string",
      "enum": [
       "goal",
       "funnel"
      ]
     },
     "conversions": {
      "type": "integer",
      "minimum": 0,
      "description": "Matching pageviews or events; for funnels, visitor-days that completed every step."
     },
     "converting_visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Identified visitor-days that converted."
     },
     "visitors": {
      "type": "integer",
      "minimum": 0,
      "description": "Identified visitor-days on the site in the range."
     },
     "rate": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0,
      "maximum": 1,
      "description": "converting_visitors / visitors, null without visitors."
     }
    },
    "required": [
     "site",
     "id",
     "name",
     "kind",
     "conversions",
     "converting_visitors",
     "visitors",
     "rate"
    ],
    "additionalProperties": false
   },
   "WorkspaceBreakdownResponse": {
    "type": "object",
    "properties": {
     "dim": {
      "type": "string",
      "enum": [
       "sites",
       "pages",
       "referrers",
       "countries",
       "events",
       "goals"
      ]
     },
     "rows": {
      "type": "array",
      "items": {
       "oneOf": [
        {
         "$ref": "#/components/schemas/WorkspaceSite"
        },
        {
         "$ref": "#/components/schemas/WorkspacePageRow"
        },
        {
         "$ref": "#/components/schemas/WorkspaceSourceRow"
        },
        {
         "$ref": "#/components/schemas/WorkspaceGoalRow"
        }
       ]
      },
      "description": "sites rows match the overview's sites; pages and events rows carry their site; referrers and countries rows add up every site; goals rows list each site's valid goals."
     }
    },
    "required": [
     "dim",
     "rows"
    ],
    "additionalProperties": false
   },
   "ManagementScope": {
    "type": "string",
    "enum": [
     "read",
     "manage",
     "ingest"
    ],
    "description": "read: GET operations. manage: every other method. ingest: POST /api/ingest with X-Totallytics-Site."
   },
   "ManagementKey": {
    "type": "object",
    "required": [
     "id",
     "label",
     "prefix",
     "scopes",
     "created_at",
     "last_used_at"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "label": {
      "type": "string",
      "maxLength": 64
     },
     "prefix": {
      "type": "string",
      "description": "First 14 characters of the key (tt_mk_ and 8 hex), for recognizing it."
     },
     "scopes": {
      "type": "array",
      "minItems": 1,
      "items": {
       "$ref": "#/components/schemas/ManagementScope"
      }
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_used_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "null until the key authenticates a request; updated at most once a minute."
     }
    }
   },
   "ManagementKeysResponse": {
    "type": "object",
    "required": [
     "keys"
    ],
    "properties": {
     "keys": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ManagementKey"
      },
      "description": "Active management keys, newest first."
     }
    }
   },
   "CreateManagementKey": {
    "type": "object",
    "required": [
     "scopes"
    ],
    "properties": {
     "label": {
      "type": "string",
      "maxLength": 64,
      "description": "Optional; trimmed and cut to 64 characters."
     },
     "scopes": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": {
       "$ref": "#/components/schemas/ManagementScope"
      }
     }
    }
   },
   "CreatedManagementKey": {
    "type": "object",
    "required": [
     "key",
     "secret"
    ],
    "properties": {
     "key": {
      "$ref": "#/components/schemas/ManagementKey"
     },
     "secret": {
      "type": "string",
      "pattern": "^tt_mk_[0-9a-f]{48}$",
      "description": "The full key. Only returned here."
     }
    }
   },
   "Me": {
    "type": "object",
    "required": [
     "principal",
     "sites"
    ],
    "properties": {
     "principal": {
      "oneOf": [
       {
        "type": "object",
        "title": "Signed-in owner",
        "required": [
         "uid",
         "via",
         "email"
        ],
        "properties": {
         "uid": {
          "type": "string"
         },
         "via": {
          "const": "firebase"
         },
         "email": {
          "type": "string"
         }
        }
       },
       {
        "type": "object",
        "title": "Management key",
        "required": [
         "uid",
         "via",
         "key_id",
         "label",
         "scopes"
        ],
        "properties": {
         "uid": {
          "type": "string"
         },
         "via": {
          "const": "management_key"
         },
         "key_id": {
          "type": "string",
          "format": "uuid"
         },
         "label": {
          "type": "string"
         },
         "scopes": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/ManagementScope"
          }
         }
        }
       }
      ]
     },
     "sites": {
      "type": "integer",
      "minimum": 0,
      "description": "Sites registered to the account."
     }
    }
   },
   "OwnerSiteKey": {
    "allOf": [
     {
      "$ref": "#/components/schemas/ApiKey"
     },
     {
      "type": "object",
      "required": [
       "site"
      ],
      "properties": {
       "site": {
        "type": "string",
        "description": "Hostname the key belongs to."
       }
      }
     }
    ]
   },
   "OwnerSiteKeysResponse": {
    "type": "object",
    "required": [
     "keys"
    ],
    "properties": {
     "keys": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/OwnerSiteKey"
      },
      "description": "Active site API keys, ordered by site, newest first within a site."
     }
    }
   },
   "RotatedApiKey": {
    "type": "object",
    "required": [
     "key",
     "secret",
     "revoked"
    ],
    "properties": {
     "key": {
      "$ref": "#/components/schemas/ApiKey"
     },
     "secret": {
      "type": "string",
      "pattern": "^tt_[0-9a-f]{48}$",
      "description": "The new key. Only returned here."
     },
     "revoked": {
      "type": "string",
      "description": "Id of the key it replaces, revoked in the same transaction."
     }
    }
   },
   "WorkspaceDigest": {
    "type": "object",
    "properties": {
     "configured": {
      "type": "boolean",
      "description": "False until the first save of the digest or the report defaults; the other fields then hold the column defaults."
     },
     "enabled": {
      "type": "boolean"
     },
     "recipients": {
      "type": "array",
      "maxItems": 10,
      "items": {
       "type": "object",
       "properties": {
        "email": {
         "type": "string",
         "format": "email"
        },
        "last_sent_at": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        },
        "fail_count": {
         "type": "integer",
         "minimum": 0
        }
       },
       "required": [
        "email",
        "last_sent_at",
        "fail_count"
       ],
       "additionalProperties": false
      }
     },
     "frequency": {
      "type": "string",
      "enum": [
       "daily",
       "weekly",
       "monthly"
      ]
     },
     "timezone": {
      "type": "string",
      "description": "IANA timezone; send_hour is wall-clock here."
     },
     "send_hour": {
      "type": "integer",
      "minimum": 0,
      "maximum": 23
     },
     "sites": {
      "type": "integer",
      "minimum": 0,
      "description": "Verified sites the digest covers, the demo site excluded."
     },
     "next_send_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Null while the digest is off, has no recipients or there is no verified site."
     },
     "dropped": {
      "type": "array",
      "description": "Recipients removed by an unsubscribe or a hard bounce since the last save, oldest first.",
      "items": {
       "type": "object",
       "properties": {
        "email": {
         "type": "string",
         "format": "email"
        },
        "reason": {
         "type": "string",
         "enum": [
          "unsubscribed",
          "bounced"
         ]
        },
        "at": {
         "type": "string",
         "format": "date-time"
        }
       },
       "required": [
        "email",
        "reason",
        "at"
       ],
       "additionalProperties": false
      }
     },
     "updated_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Shared with the report defaults: saving either one changes it."
     }
    },
    "required": [
     "configured",
     "enabled",
     "recipients",
     "frequency",
     "timezone",
     "send_hour",
     "sites",
     "next_send_at",
     "dropped",
     "updated_at"
    ],
    "additionalProperties": false
   },
   "UpdateWorkspaceDigest": {
    "type": "object",
    "description": "Any subset; omitted fields keep their stored value, or the column default on the first save. Only these fields are read.",
    "properties": {
     "enabled": {
      "type": "boolean"
     },
     "recipients": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "email"
      },
      "description": "Trimmed, lowercased and de-duplicated; at most 10 after that. Replaces the whole list."
     },
     "frequency": {
      "type": "string",
      "enum": [
       "daily",
       "weekly",
       "monthly"
      ]
     },
     "timezone": {
      "type": "string",
      "description": "IANA timezone, stored in canonical form (europe/amsterdam becomes Europe/Amsterdam). Fixed offsets such as +02:00 are refused."
     },
     "send_hour": {
      "type": "integer",
      "minimum": 0,
      "maximum": 23
     },
     "updated_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "The updated_at from the last read of the digest or the defaults. When sent, the save is refused with 409 if either changed since; null means nothing saved yet."
     }
    },
    "additionalProperties": true
   },
   "LiveTicketRequest": {
    "type": "object",
    "properties": {
     "site": {
      "type": "string",
      "description": "Hostname of a verified website you own, exactly as registered. A missing, empty or non-string site asks for every verified website you own."
     }
    }
   },
   "LiveTicket": {
    "type": "object",
    "properties": {
     "ticket": {
      "type": "string",
      "description": "Single-use credential for GET /api/live/ws: the account id, a dot and 32 hex characters."
     },
     "expires_in": {
      "type": "integer",
      "const": 60,
      "description": "Seconds until the ticket expires."
     },
     "url": {
      "type": "string",
      "description": "Path of the WebSocket with the ticket filled in; connect to it on wss://clicktag.io."
     }
    },
    "required": [
     "ticket",
     "expires_in",
     "url"
    ],
    "additionalProperties": false
   },
   "LiveHit": {
    "type": "object",
    "properties": {
     "site": {
      "type": "string",
      "description": "Hostname the hit belongs to."
     },
     "ts": {
      "type": "integer",
      "format": "int64",
      "description": "Unix milliseconds: when the hit was published on the stream, or its stored time in recent history."
     },
     "type": {
      "type": "string",
      "enum": [
       "pageview",
       "event",
       "append",
       "error"
      ]
     },
     "id": {
      "type": "string",
      "description": "Hit id sent by the tracker; empty when none was sent."
     },
     "orig": {
      "type": "string",
      "description": "original_id sent by the tracker; an append carries the id of the pageview it updates."
     },
     "path": {
      "type": "string",
      "description": "Page path."
     },
     "query": {
      "type": "string",
      "description": "Query string, cut to 256 characters."
     },
     "ref": {
      "type": "string",
      "description": "Referring host."
     },
     "event": {
      "type": "string",
      "description": "Event name of event hits."
     },
     "meta": {
      "type": "string",
      "description": "Event metadata as stored, cut to 512 characters."
     },
     "error": {
      "type": "string",
      "description": "Error text of error hits, cut to 512 characters."
     },
     "dur": {
      "type": [
       "number",
       "null"
      ],
      "description": "Seconds an append adds to its pageview's time on page; null on other hit types or when absent."
     },
     "scroll": {
      "type": [
       "number",
       "null"
      ],
      "description": "Scroll depth an append reports; null on other hit types or when absent."
     },
     "country": {
      "type": "string",
      "description": "Two-letter country code, or empty."
     },
     "device": {
      "type": "string",
      "description": "As collected; not normalized like the breakdowns."
     },
     "browser": {
      "type": "string",
      "description": "As collected; not normalized like the breakdowns."
     },
     "os": {
      "type": "string",
      "description": "As collected; not normalized like the breakdowns."
     },
     "unique": {
      "type": "integer",
      "enum": [
       0,
       1
      ],
      "description": "1 when the tracker flagged the hit as an entry from outside the site (the dashboard's Entry label)."
     },
     "visitor": {
      "type": "string",
      "description": "First 8 characters of the daily visitor hash; empty without one."
     },
     "me": {
      "type": "boolean",
      "description": "Stream hits: true when the hit came from the same IP address and User-Agent as this WebSocket. Always false in recent history."
     },
     "loc": {
      "type": "object",
      "description": "Stream hits only, when Cloudflare located the visitor. City-level estimate, not GPS.",
      "properties": {
       "lat": {
        "type": "number",
        "description": "Latitude rounded to 2 decimals."
       },
       "lng": {
        "type": "number",
        "description": "Longitude rounded to 2 decimals."
       },
       "city": {
        "type": "string",
        "description": "Cut to 64 characters; may be empty."
       },
       "region": {
        "type": "string",
        "description": "Cut to 64 characters; may be empty."
       }
      },
      "required": [
       "lat",
       "lng",
       "city",
       "region"
      ],
      "additionalProperties": false
     }
    },
    "required": [
     "site",
     "ts",
     "type",
     "id",
     "orig",
     "path",
     "query",
     "ref",
     "event",
     "meta",
     "error",
     "dur",
     "scroll",
     "country",
     "device",
     "browser",
     "os",
     "unique",
     "visitor",
     "me"
    ],
    "additionalProperties": false
   },
   "LiveRecentResponse": {
    "type": "object",
    "properties": {
     "since": {
      "type": "integer",
      "format": "int64",
      "description": "Exclusive lower bound that was used, in Unix milliseconds."
     },
     "until": {
      "type": "integer",
      "format": "int64",
      "description": "Server time of the request and inclusive upper bound, in Unix milliseconds."
     },
     "hits": {
      "type": "array",
      "maxItems": 500,
      "items": {
       "$ref": "#/components/schemas/LiveHit"
      },
      "description": "Newest first."
     }
    },
    "required": [
     "since",
     "until",
     "hits"
    ],
    "additionalProperties": false
   },
   "GscRevoked": {
    "type": "object",
    "description": "The Google connection expired or was revoked; reconnect Google in site settings.",
    "properties": {
     "error": {
      "type": "string"
     },
     "revoked": {
      "type": "boolean",
      "const": true
     }
    },
    "required": [
     "error",
     "revoked"
    ],
    "additionalProperties": false
   }
  },
  "parameters": {
   "Hostname": {
    "name": "hostname",
    "in": "path",
    "required": true,
    "description": "Use the normalized hostname returned at registration. Lookups do not lowercase path values.",
    "schema": {
     "type": "string"
    },
    "example": "your-domain.example"
   },
   "From": {
    "name": "from",
    "in": "query",
    "description": "Inclusive Unix seconds, rounded down. Omitted input defaults to to minus 30 days. Explicit zero is valid. Empty, non-numeric, non-finite, negative, reversed or out-of-range bounds return 400; maximum span 400 days.",
    "schema": {
     "type": "number"
    },
    "example": 1789516800
   },
   "To": {
    "name": "to",
    "in": "query",
    "description": "Exclusive Unix seconds, rounded up. Omitted input defaults to current Unix seconds. Empty, non-numeric, non-finite, negative, reversed or out-of-range bounds return 400. Not milliseconds.",
    "schema": {
     "type": "number"
    },
    "example": 1789603200
   },
   "Timezone": {
    "name": "tz",
    "in": "query",
    "description": "IANA timezone for series and summary grouping, not a range offset. Defaults UTC; invalid or unknown zones fall back to UTC. Daily timestamps represent local calendar-day starts, with DST and fractional offsets respected.",
    "schema": {
     "type": "string",
     "default": "UTC"
    },
    "example": "UTC"
   },
   "GscTimezone": {
    "name": "tz",
    "in": "query",
    "description": "IANA timezone used to turn from and to into Search Console dates. Defaults UTC; invalid or unknown zones fall back to UTC.",
    "schema": {
     "type": "string",
     "default": "UTC"
    },
    "example": "UTC"
   },
   "Dimension": {
    "name": "dim",
    "in": "query",
    "required": true,
    "description": "All dimensions count pageviews except events, which counts event rows. Metadata and UTM term/content are not queryable dimensions.",
    "schema": {
     "type": "string",
     "enum": [
      "pages",
      "referrers",
      "countries",
      "devices",
      "browsers",
      "os",
      "utm_sources",
      "utm_mediums",
      "utm_campaigns",
      "events"
     ]
    },
    "example": "pages"
   },
   "Limit": {
    "name": "limit",
    "in": "query",
    "description": "Default 10; numeric input is clamped to 1-100 and fractions round down. Zero or non-numeric input uses 10. No pagination or offset exists.",
    "schema": {
     "type": "integer",
     "default": 10
    },
    "example": 10
   },
   "Filter": {
    "name": "f",
    "in": "query",
    "description": "Repeatable `<key>:<value>` filter. The value is everything after the first colon and must equal a breakdown row name exactly, including `/` and `Direct / none`. Keys: page, referrer, country, device, browser, os, utm_source, utm_medium, utm_campaign, event. Repeated keys match any of their values; different keys must all match. `event` keeps pageviews from sessions that fired the event, and pageview filters keep events from sessions with a matching pageview. Live counts are filtered too. At most 12 filters, values at most 256 characters.",
    "style": "form",
    "explode": true,
    "schema": {
     "type": "array",
     "maxItems": 12,
     "items": {
      "type": "string",
      "pattern": "^(page|referrer|country|device|browser|os|utm_source|utm_medium|utm_campaign|event):.+"
     }
    },
    "example": [
     "country:NL",
     "page:/pricing"
    ]
   },
   "Dnt": {
    "name": "DNT",
    "in": "header",
    "description": "Value 1 skips collection unless collect-dnt is true. The normal success response is returned.",
    "schema": {
     "type": "string"
    },
    "example": "1"
   },
   "XDoNotTrack": {
    "name": "X-Do-Not-Track",
    "in": "header",
    "description": "Value 1 behaves like DNT: 1. This custom header is not in the collector CORS allowlist.",
    "schema": {
     "type": "string"
    },
    "example": "1"
   },
   "Referer": {
    "name": "Referer",
    "in": "header",
    "description": "For noscript, the URL of the page being tracked. Supplies missing hostname, path, https and query; explicit query parameters win even if empty. Does not supply the acquisition referrer field. Browser policies can omit it or reduce it to an origin.",
    "schema": {
     "type": "string"
    },
    "example": "https://example.com/pricing?utm_source=newsletter"
   },
   "CollectHostname": {
    "name": "hostname",
    "in": "query",
    "description": "Registered hostname. When the parameter is absent it comes from the page URL in Referer; an explicit empty value returns 400. Normalized the way site registration does: trimmed and lowercased, with an http:// or https:// scheme, path and trailing dot dropped and internationalized names in xn-- form. A value registration would reject, such as one with a port or an IP address, can't match a site: the hit is accepted and nothing is stored. The result must match a registered hostname exactly; www and apex are different hostnames.",
    "schema": {
     "type": "string"
    },
    "example": "example.com"
   },
   "CollectType": {
    "name": "type",
    "in": "query",
    "description": "Both POST paths share this default. Omitted or empty values become pageview. Truncated to 16 characters before validation; case-sensitive.",
    "schema": {
     "type": "string",
     "enum": [
      "pageview",
      "event",
      "append",
      "error"
     ],
     "default": "pageview"
    }
   },
   "CollectEvent": {
    "name": "event",
    "in": "query",
    "description": "Required for type event; ignored otherwise. Truncated to 256 characters, then runs outside ASCII letters/digits become underscores and edge underscores are removed. Case is preserved. Empty after cleaning returns 400.",
    "schema": {
     "type": "string"
    }
   },
   "CollectPath": {
    "name": "path",
    "in": "query",
    "description": "Truncated to 2048 characters. Missing or empty defaults to / for pageview and empty for other types.",
    "schema": {
     "type": "string"
    }
   },
   "CollectQuery": {
    "name": "query",
    "in": "query",
    "description": "Page query string without the leading ?. Truncated to 2048 characters and parsed for UTM values.",
    "schema": {
     "type": "string"
    }
   },
   "CollectReferrer": {
    "name": "referrer",
    "in": "query",
    "description": "Full URL or hostname/path, truncated to 2048 characters. Parsed hostname is lowercased, excluding port, query and fragment.",
    "schema": {
     "type": "string"
    }
   },
   "CollectMetadata": {
    "name": "metadata",
    "in": "query",
    "description": "URL-encoded metadata string, typically JSON. Truncated to 4096 characters; not exposed by stats, and the live view shows the first 512 characters. No object-style query serialization is used.",
    "schema": {
     "type": "string"
    }
   },
   "CollectId": {
    "name": "id",
    "in": "query",
    "description": "Stable logical row ID, truncated to 64 characters. Optional, defaults empty. Bounded per-isolate replay suppression uses site, type and nonempty ID; reports reconcile repeated IDs. Not durable exactly-once delivery.",
    "schema": {
     "type": "string"
    }
   },
   "CollectPageId": {
    "name": "page_id",
    "in": "query",
    "description": "Page correlation ID, truncated to 64 characters. Optional, defaults empty; not a replacement for the row replay ID.",
    "schema": {
     "type": "string"
    }
   },
   "CollectSessionId": {
    "name": "session_id",
    "in": "query",
    "description": "Session correlation ID, truncated to 64 characters. Optional, defaults empty; not used for visitor counting or row replay identity. Stats filters match pageviews and events to each other through it, so rows without one never match a filter on the other type.",
    "schema": {
     "type": "string"
    }
   },
   "CollectOriginalId": {
    "name": "original_id",
    "in": "query",
    "description": "Original pageview ID for an append, truncated to 64 characters. Reports link updates to this parent for its date, bot status and engagement. Missing/unlinked parents do not contribute to engagement.",
    "schema": {
     "type": "string"
    }
   },
   "CollectDuration": {
    "name": "duration",
    "in": "query",
    "description": "Incremental seconds. Numeric strings accepted; rounded and capped at 86400. Missing, empty, malformed, non-finite or negative values become null; explicit zero remains zero.",
    "schema": {
     "type": "string"
    }
   },
   "CollectScrolled": {
    "name": "scrolled",
    "in": "query",
    "description": "Scroll percentage. Numeric strings accepted; rounded and capped at 100. Missing, empty, malformed, non-finite or negative values become null; explicit zero remains zero.",
    "schema": {
     "type": "string"
    }
   },
   "CollectViewportWidth": {
    "name": "viewport_width",
    "in": "query",
    "description": "Viewport width. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected.",
    "schema": {
     "type": "string"
    }
   },
   "CollectViewportHeight": {
    "name": "viewport_height",
    "in": "query",
    "description": "Viewport height. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected.",
    "schema": {
     "type": "string"
    }
   },
   "CollectScreenWidth": {
    "name": "screen_width",
    "in": "query",
    "description": "Screen width. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected.",
    "schema": {
     "type": "string"
    }
   },
   "CollectScreenHeight": {
    "name": "screen_height",
    "in": "query",
    "description": "Screen height. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 65535; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected.",
    "schema": {
     "type": "string"
    }
   },
   "CollectError": {
    "name": "error",
    "in": "query",
    "description": "Error text, truncated to 2048 characters; empty by default. Error rows have no dedicated breakdown.",
    "schema": {
     "type": "string"
    }
   },
   "CollectUa": {
    "name": "ua",
    "in": "query",
    "description": "Supplied user-agent string is truncated to 512 characters. Empty or missing falls back to the request User-Agent header, truncated the same way. Drives browser, OS, device, bot and visitor classification.",
    "schema": {
     "type": "string"
    }
   },
   "CollectTimezone": {
    "name": "timezone",
    "in": "query",
    "description": "Browser IANA timezone, truncated to 64 characters. Used for country inference, not row timestamps. UTC, fixed offsets and unknown/missing zones have no country; no IP fallback.",
    "schema": {
     "type": "string"
    }
   },
   "CollectLanguage": {
    "name": "language",
    "in": "query",
    "description": "Language label, truncated to 16 characters.",
    "schema": {
     "type": "string"
    }
   },
   "CollectOsName": {
    "name": "os_name",
    "in": "query",
    "description": "Truncated to 64 characters. Nonempty input overrides parsed OS name, then shared normalization applies (Mac OS / Mac OS X become macOS).",
    "schema": {
     "type": "string"
    }
   },
   "CollectOsVersion": {
    "name": "os_version",
    "in": "query",
    "description": "Truncated to 64 characters. Non-empty input overrides the parsed OS version.",
    "schema": {
     "type": "string"
    }
   },
   "CollectBrands": {
    "name": "brands",
    "in": "query",
    "description": "Client-hint browser brands as an array or serialized string. Stored up to 1024 characters; supported brands contribute to normalized browser classification.",
    "schema": {
     "type": "string"
    }
   },
   "CollectVersion": {
    "name": "version",
    "in": "query",
    "description": "Script version label, truncated to 32 characters.",
    "schema": {
     "type": "string"
    }
   },
   "CollectHostnameOriginal": {
    "name": "hostname_original",
    "in": "query",
    "description": "Original hostname before an override, lowercased and truncated to 253 characters.",
    "schema": {
     "type": "string"
    }
   },
   "CollectUnique": {
    "name": "unique",
    "in": "query",
    "description": "SA-style entry flag, stored as is_unique. True for true, 1, \"true\" or \"1\"; other scalar values are false.",
    "schema": {
     "type": "string"
    }
   },
   "CollectMobile": {
    "name": "mobile",
    "in": "query",
    "description": "Mobile hint. Parsed device type also contributes to the stored mobile flag, and the hint sets device type mobile when the user agent shows none. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false.",
    "schema": {
     "type": "string"
    }
   },
   "CollectBot": {
    "name": "bot",
    "in": "query",
    "description": "Explicit bot flag. False does not override a bot user-agent match. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false.",
    "schema": {
     "type": "string"
    }
   },
   "CollectBrave": {
    "name": "brave",
    "in": "query",
    "description": "Brave hint: the browser is stored as Brave. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false.",
    "schema": {
     "type": "string"
    }
   },
   "CollectDuck": {
    "name": "duck",
    "in": "query",
    "description": "DuckDuckGo hint: the browser is stored as DuckDuckGo unless brave is also true. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false.",
    "schema": {
     "type": "string"
    }
   },
   "CollectHttps": {
    "name": "https",
    "in": "query",
    "description": "Whether the tracked page used HTTPS. Missing input defaults true. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false.",
    "schema": {
     "type": "string"
    }
   },
   "CollectCollectDnt": {
    "name": "collect-dnt",
    "in": "query",
    "description": "Only true or \"true\" bypasses collection skipping for DNT: 1 or X-Do-Not-Track: 1. Numeric/string 1 is not an override. Skipped requests return success before payload-field validation.",
    "schema": {
     "type": "string"
    }
   },
   "CollectUtmSource": {
    "name": "utm_source",
    "in": "query",
    "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns.",
    "schema": {
     "type": "string"
    }
   },
   "CollectUtmMedium": {
    "name": "utm_medium",
    "in": "query",
    "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns.",
    "schema": {
     "type": "string"
    }
   },
   "CollectUtmCampaign": {
    "name": "utm_campaign",
    "in": "query",
    "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns.",
    "schema": {
     "type": "string"
    }
   },
   "CollectUtmTerm": {
    "name": "utm_term",
    "in": "query",
    "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns.",
    "schema": {
     "type": "string"
    }
   },
   "CollectUtmContent": {
    "name": "utm_content",
    "in": "query",
    "description": "Campaign source preference: utm_source, source, ref. Other fields prefer utm_<name> then <name>. Within each key, query value precedes direct payload value. Truncated to 256 characters; only source, medium and campaign have breakdowns.",
    "schema": {
     "type": "string"
    }
   },
   "ImportJobId": {
    "name": "jobId",
    "in": "path",
    "required": true,
    "description": "Job ID returned by creation; the job must belong to the requested site.",
    "schema": {
     "type": "string"
    },
    "example": "746b4998-643d-44c0-848d-4d8fe4c6ebef"
   },
   "ApiKeyId": {
    "name": "keyId",
    "in": "path",
    "required": true,
    "description": "Key id from the key list or creation response.",
    "schema": {
     "type": "string",
     "format": "uuid"
    },
    "example": "0b6f3f7e-5d1c-4f5e-9a41-2f8c6d7e9b10"
   },
   "ApiDimension": {
    "name": "dim",
    "in": "query",
    "required": true,
    "description": "Breakdown dimension. Endpoint rows are named `METHOD route`, status rows by code, client rows by client name and consumer rows by consumer id (empty for requests without one).",
    "schema": {
     "type": "string",
     "enum": [
      "endpoints",
      "statuses",
      "clients",
      "consumers"
     ]
    },
    "example": "endpoints"
   },
   "ApiFilter": {
    "name": "f",
    "in": "query",
    "description": "Repeatable `<key>:<value>` filter for request stats. The value is everything after the first colon. Keys: endpoint (`METHOD route`, as in the endpoint breakdown name), status (a code from 100 to 599), client (client name), consumer (consumer id; an empty value matches requests without one). Repeated keys match any of their values; different keys must all match. At most 12 filters, values at most 256 characters.",
    "style": "form",
    "explode": true,
    "schema": {
     "type": "array",
     "maxItems": 12,
     "items": {
      "type": "string",
      "pattern": "^((endpoint|client):.{1,256}|status:[1-5][0-9]{2}|consumer:.{0,256})$"
     }
    },
    "example": [
     "status:500",
     "endpoint:POST /v1/uploads"
    ]
   },
   "ApiRowsLimit": {
    "name": "limit",
    "in": "query",
    "description": "Maximum rows. Default 100; zero or non-numeric input uses 100, fractions round down, and values are clamped to 1-500.",
    "schema": {
     "type": "integer",
     "default": 100,
     "minimum": 1,
     "maximum": 500
    },
    "example": 100
   },
   "ApiSamplesLimit": {
    "name": "limit",
    "in": "query",
    "description": "Maximum samples. Default 20; zero or non-numeric input uses 20, fractions round down, and values are clamped to 1-100.",
    "schema": {
     "type": "integer",
     "default": 20,
     "minimum": 1,
     "maximum": 100
    },
    "example": 20
   },
   "GscFilter": {
    "name": "f",
    "in": "query",
    "description": "Drill into one query or one page: `query:<query>` (breakdown lists `dim=pages`) or `page:<name>` (breakdown lists `dim=queries`), where `<name>` is a Pages row name. One filter at a time; values over 2048 characters are cut to Google's 2048.",
    "schema": {
     "type": "string",
     "pattern": "^(query|page):.+"
    },
    "example": "query:static site hosting"
   },
   "CrawlerFilter": {
    "name": "f",
    "in": "query",
    "description": "Repeatable `<key>:<value>` filter for crawler stats. The value is everything after the first colon. Keys: bot (bot name), vendor, purpose (ai_search, ai_assistant, ai_training or search) and path. At most one filter per key; different keys must all match. Filters apply to every part of the response.",
    "style": "form",
    "explode": true,
    "schema": {
     "type": "array",
     "maxItems": 4,
     "items": {
      "type": "string",
      "pattern": "^((bot|vendor|path):.{1,2048}|purpose:(ai_search|ai_assistant|ai_training|search))$"
     }
    },
    "example": [
     "purpose:ai_search",
     "path:/pricing"
    ]
   },
   "CollectSource": {
    "name": "source",
    "in": "query",
    "description": "Alias for utm_source, used when no nonempty utm_source is in the query field or the payload. Truncated to 256 characters; stored only as utm_source.",
    "schema": {
     "type": "string"
    }
   },
   "CollectRef": {
    "name": "ref",
    "in": "query",
    "description": "Second alias for utm_source, used when neither utm_source nor source is set. Truncated to 256 characters; stored only as utm_source.",
    "schema": {
     "type": "string"
    }
   },
   "CollectMedium": {
    "name": "medium",
    "in": "query",
    "description": "Alias for utm_medium, used when no nonempty utm_medium is in the query field or the payload. Truncated to 256 characters; stored only as utm_medium.",
    "schema": {
     "type": "string"
    }
   },
   "CollectCampaign": {
    "name": "campaign",
    "in": "query",
    "description": "Alias for utm_campaign, used when no nonempty utm_campaign is in the query field or the payload. Truncated to 256 characters; stored only as utm_campaign.",
    "schema": {
     "type": "string"
    }
   },
   "CollectTerm": {
    "name": "term",
    "in": "query",
    "description": "Alias for utm_term, used when no nonempty utm_term is in the query field or the payload. Truncated to 256 characters; stored only as utm_term.",
    "schema": {
     "type": "string"
    }
   },
   "CollectContent": {
    "name": "content",
    "in": "query",
    "description": "Alias for utm_content, used when no nonempty utm_content is in the query field or the payload. Truncated to 256 characters; stored only as utm_content.",
    "schema": {
     "type": "string"
    }
   }
  },
  "requestBodies": {
   "Collector": {
    "required": true,
    "description": "One JSON object of at most 64 KiB. /events and /append both use this body and default to pageview unless type is explicit. Content-Type is not enforced; application/json is recommended and JSON text sent as text/plain by sendBeacon is accepted. Arrays are not batches.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/CollectorPayload"
      },
      "examples": {
       "event": {
        "value": {
         "hostname": "example.com",
         "type": "event",
         "event": "signup",
         "metadata": {
          "plan": "pro"
         }
        }
       },
       "append": {
        "value": {
         "hostname": "example.com",
         "type": "append",
         "original_id": "pageview-id",
         "duration": 42,
         "scrolled": 75
        }
       },
       "pageview": {
        "value": {
         "hostname": "example.com",
         "type": "pageview",
         "path": "/pricing"
        }
       }
      }
     },
     "text/plain": {
      "schema": {
       "type": "string",
       "description": "JSON-encoded single CollectorPayload object, not arbitrary text."
      },
      "example": "{\"hostname\":\"example.com\",\"type\":\"append\",\"original_id\":\"pageview-id\",\"duration\":42,\"scrolled\":75}"
     }
    }
   }
  },
  "responses": {
   "SignInRequired": {
    "description": "A required Firebase ID token is missing, invalid or expired, or the management key is unknown or revoked.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "examples": {
       "token": {
        "value": {
         "error": "sign in required"
        }
       },
       "managementKey": {
        "value": {
         "error": "invalid or revoked management key"
        }
       }
      }
     }
    }
   },
   "UnknownSite": {
    "description": "The hostname is not registered or is not accessible to this caller. Private and unverified sites are not disclosed to non-owners.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "unknown site"
      }
     }
    }
   },
   "SiteExists": {
    "description": "The hostname is registered to another account. Repeating registration as its owner returns 200.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "This site is already registered to another account"
      }
     }
    }
   },
   "InternalError": {
    "description": "Request could not be completed. Incorrectly typed fields on some endpoints can also cause this error.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "internal error"
      }
     }
    }
   },
   "CollectorOk": {
    "description": "Handler receipt only, not durable storage confirmation. Writes run in the background. Also returned when nothing is stored: DNT skips, unregistered hostnames, unverified hostnames that fail automatic verification, registry or storage failures, and IDs this worker isolate already accepted for the same site and type within 24 hours. Do not automatically retry after an ambiguous failure; duplicate rows are possible.",
    "headers": {
     "Access-Control-Allow-Origin": {
      "$ref": "#/components/headers/AllowOrigin"
     },
     "Access-Control-Allow-Methods": {
      "$ref": "#/components/headers/AllowMethods"
     },
     "Access-Control-Allow-Headers": {
      "$ref": "#/components/headers/AllowHeaders"
     },
     "Access-Control-Max-Age": {
      "$ref": "#/components/headers/PreflightMaxAge"
     },
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "text/plain": {
      "schema": {
       "type": "string",
       "const": "ok"
      },
      "example": "ok"
     }
    }
   },
   "PixelOk": {
    "description": "A 1x1 GIF; receipt does not confirm durable storage. Also returned when nothing is stored: DNT skips, unregistered hostnames, unverified hostnames that fail automatic verification, registry or storage failures, and IDs this worker isolate already accepted for the same site and type within 24 hours.",
    "headers": {
     "Access-Control-Allow-Origin": {
      "$ref": "#/components/headers/AllowOrigin"
     },
     "Access-Control-Allow-Methods": {
      "$ref": "#/components/headers/AllowMethods"
     },
     "Access-Control-Allow-Headers": {
      "$ref": "#/components/headers/AllowHeaders"
     },
     "Access-Control-Max-Age": {
      "$ref": "#/components/headers/PreflightMaxAge"
     },
     "Cache-Control": {
      "schema": {
       "type": "string",
       "const": "no-store, no-cache, must-revalidate"
      }
     },
     "Expires": {
      "schema": {
       "type": "string",
       "const": "0"
      }
     }
    },
    "content": {
     "image/gif": {
      "schema": {
       "type": "string",
       "format": "binary"
      }
     }
    }
   },
   "CollectorBadRequest": {
    "description": "Plain-text payload or method error. POST bodies must parse as a non-null, non-array object; arrays are not a batch format and return invalid JSON. DNT skipping occurs after JSON parsing but before payload-field validation.",
    "headers": {
     "Access-Control-Allow-Origin": {
      "$ref": "#/components/headers/AllowOrigin"
     },
     "Access-Control-Allow-Methods": {
      "$ref": "#/components/headers/AllowMethods"
     },
     "Access-Control-Allow-Headers": {
      "$ref": "#/components/headers/AllowHeaders"
     },
     "Access-Control-Max-Age": {
      "$ref": "#/components/headers/PreflightMaxAge"
     }
    },
    "content": {
     "text/plain": {
      "schema": {
       "type": "string"
      },
      "examples": {
       "hostname": {
        "value": "hostname is required"
       },
       "event": {
        "value": "event name is required"
       },
       "type": {
        "value": "unsupported type: other"
       },
       "json": {
        "value": "invalid JSON"
       },
       "method": {
        "value": "POST required"
       },
       "getMethod": {
        "value": "GET required"
       }
      }
     }
    }
   },
   "CollectorGetRequired": {
    "description": "The route was called with a method other than GET, HEAD or OPTIONS. Plain text.",
    "headers": {
     "Access-Control-Allow-Origin": {
      "$ref": "#/components/headers/AllowOrigin"
     },
     "Access-Control-Allow-Methods": {
      "$ref": "#/components/headers/AllowMethods"
     },
     "Access-Control-Allow-Headers": {
      "$ref": "#/components/headers/AllowHeaders"
     },
     "Access-Control-Max-Age": {
      "$ref": "#/components/headers/PreflightMaxAge"
     }
    },
    "content": {
     "text/plain": {
      "schema": {
       "type": "string",
       "const": "GET required"
      },
      "example": "GET required"
     }
    }
   },
   "CollectorPayloadTooLarge": {
    "description": "The body is over 64 KiB. Send one hit per request. Plain text.",
    "headers": {
     "Access-Control-Allow-Origin": {
      "$ref": "#/components/headers/AllowOrigin"
     },
     "Access-Control-Allow-Methods": {
      "$ref": "#/components/headers/AllowMethods"
     },
     "Access-Control-Allow-Headers": {
      "$ref": "#/components/headers/AllowHeaders"
     },
     "Access-Control-Max-Age": {
      "$ref": "#/components/headers/PreflightMaxAge"
     }
    },
    "content": {
     "text/plain": {
      "schema": {
       "type": "string",
       "const": "payload too large"
      },
      "example": "payload too large"
     }
    }
   },
   "Preflight": {
    "description": "Empty preflight response. The global handler returns this for OPTIONS. It does not add CORS headers to subsequent product API or health responses.",
    "headers": {
     "Access-Control-Allow-Origin": {
      "$ref": "#/components/headers/AllowOrigin"
     },
     "Access-Control-Allow-Methods": {
      "$ref": "#/components/headers/AllowMethods"
     },
     "Access-Control-Allow-Headers": {
      "$ref": "#/components/headers/AllowHeaders"
     },
     "Access-Control-Max-Age": {
      "$ref": "#/components/headers/PreflightMaxAge"
     }
    }
   },
   "UnknownImport": {
    "description": "The site is missing or owned by another account, or the import is missing or belongs to another site. Site ownership is checked before job lookup.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "examples": {
       "site": {
        "value": {
         "error": "unknown site"
        }
       },
       "job": {
        "value": {
         "error": "unknown import"
        }
       }
      }
     }
    }
   },
   "UnknownOwnedSite": {
    "description": "No site with this hostname is registered to the caller's account. Sites owned by other accounts return the same response.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "unknown site"
      }
     }
    }
   },
   "ApiStatsBadRequest": {
    "description": "The range or a filter is invalid.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "examples": {
       "range": {
        "value": {
         "error": "invalid range"
        }
       },
       "filter": {
        "value": {
         "error": "invalid filter"
        }
       }
      }
     }
    }
   },
   "VerificationRequired": {
    "description": "The owner must verify domain ownership before using this operation.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "Install your tracking script to verify this site first"
      }
     }
    }
   },
   "ManagementKeyRejected": {
    "description": "The bearer is a management key (tt_mk_...) that is unknown or revoked. Any other invalid bearer token is ignored here and the request is handled as anonymous.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "invalid or revoked management key"
      }
     }
    }
   },
   "ManagementKeyReadScope": {
    "description": "The management key lacks the read scope.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "scope read required"
      }
     }
    }
   },
   "ManagementKeyManageScope": {
    "description": "The management key lacks the manage scope.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "scope manage required"
      }
     }
    }
   },
   "ManagementKeyNotAllowed": {
    "description": "Needs the owner's Firebase ID token; management keys cannot call this operation.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "management keys cannot call this endpoint"
      }
     }
    }
   },
   "ManagementKeyRateLimited": {
    "description": "Too many non-GET calls with this management key. Each key gets a burst of 60 per server instance, refilled at 1 per second.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     },
     "Retry-After": {
      "description": "Seconds to wait before retrying; currently always 1.",
      "schema": {
       "type": "integer",
       "minimum": 1
      },
      "example": 1
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "rate limited, retry in 1s"
      }
     }
    }
   },
   "SiteReadForbidden": {
    "description": "The caller owns the site but it is not verified yet, or the management key lacks the read scope.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "examples": {
       "verification": {
        "value": {
         "error": "Install your tracking script to verify this site first"
        }
       },
       "scope": {
        "value": {
         "error": "scope read required"
        }
       }
      }
     }
    }
   },
   "SiteManageForbidden": {
    "description": "The site is not verified yet, or the management key lacks the manage scope.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "examples": {
       "verification": {
        "value": {
         "error": "Install your tracking script to verify this site first"
        }
       },
       "scope": {
        "value": {
         "error": "scope manage required"
        }
       }
      }
     }
    }
   },
   "LiveSignInRequired": {
    "description": "No valid Firebase ID token. Management keys are not accepted here and get the same response.",
    "headers": {
     "Cache-Control": {
      "$ref": "#/components/headers/NoStore"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": "sign in required"
      }
     }
    }
   }
  },
  "headers": {
   "NoStore": {
    "description": "HTTP responses are not cached; site lookup caching is separate.",
    "schema": {
     "type": "string",
     "const": "no-store"
    }
   },
   "AllowOrigin": {
    "schema": {
     "type": "string",
     "const": "*"
    }
   },
   "AllowMethods": {
    "schema": {
     "type": "string",
     "const": "GET, POST, OPTIONS"
    }
   },
   "AllowHeaders": {
    "schema": {
     "type": "string",
     "const": "Content-Type"
    }
   },
   "PreflightMaxAge": {
    "schema": {
     "type": "string",
     "const": "86400"
    }
   },
   "PublicStatsCache": {
    "description": "Successful web stats read by anyone other than the owner are cached at the edge for 60 seconds after access checks. Cache hits on clicktag.io and clicktag.app arrive with public, max-age=60; legacy Totallytics domains can still return max-age=14400 and allow four hours of browser reuse. Uncached responses and the owner's reads return no-store.",
    "schema": {
     "type": "string",
     "enum": [
      "no-store",
      "public, max-age=60",
      "public, max-age=14400"
     ]
    }
   }
  }
 }
}
