{
  "openapi": "3.1.0",
  "info": {
    "title": "Totallytics API",
    "version": "2026-09-18",
    "description": "Current unversioned API; the document version is a reference revision, not a URL prefix. Product management uses Firebase ID bearer tokens. Public site metadata and stats can be read anonymously; private reads require the owner. 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. Successful collection is receipt, not durable storage confirmation. Write examples use placeholder hostnames; demo.bitgate.dev is for read-only examples. Site lookups are cached per server instance for five minutes (existing) or one minute (missing). 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. No product API-key or SDK is defined here."
  },
  "servers": [
    {
      "url": "https://totallytics.com"
    }
  ],
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://analytics.bitgate.dev/docs"
  },
  "tags": [
    {
      "name": "Sites",
      "description": "Site registration, ownership and visibility."
    },
    {
      "name": "Stats",
      "description": "Overview, dimension breakdowns and account summaries."
    },
    {
      "name": "Imports",
      "description": "Owner-only historical import jobs. Source discovery requires sign-in. 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."
    }
  ],
  "security": [],
  "paths": {
    "/api/sites": {
      "get": {
        "tags": [
          "Sites"
        ],
        "operationId": "listSites",
        "summary": "List your sites",
        "description": "Returns all caller-owned sites ordered by creation time. Requires a Firebase ID token; 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"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "FirebaseIdToken": []
          }
        ]
      },
      "post": {
        "tags": [
          "Sites"
        ],
        "operationId": "createSite",
        "summary": "Register a hostname",
        "description": "Registers a globally unique hostname to the signed-in account. New sites are private with an empty display name. Maximum 50 sites per account. No hostname verification workflow is performed by this route.",
        "responses": {
          "201": {
            "description": "Site registered.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedSite"
                },
                "example": {
                  "hostname": "your-domain.example"
                }
              }
            }
          },
          "400": {
            "description": "Hostname 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": "invalid hostname"
                    }
                  },
                  "limit": {
                    "value": {
                      "error": "site limit reached"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignInRequired"
          },
          "409": {
            "$ref": "#/components/responses/SiteExists"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "FirebaseIdToken": []
          }
        ],
        "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. All owned sites are included in creation order; inactive sites have days: [] and live: 0. 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",
                      "live": 0,
                      "days": [
                        {
                          "day": "2026-09-16",
                          "pageviews": 3,
                          "visitors": 2
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignInRequired"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "FirebaseIdToken": []
          }
        ],
        "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": "Anonymous access is allowed only for a public site. Private sites require their owner token. Unknown site returns 404, unauthenticated private reads 401, and another account 403. Public reads do not validate the token. Site lookups cache existing entries for five minutes and misses for one minute per server instance.",
        "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"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignInRequired"
          },
          "403": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownSite"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {},
          {
            "FirebaseIdToken": []
          }
        ],
        "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 and visibility",
        "description": "Owner only, even if the site is public. Changes display_name and is_public; no rename or ownership transfer. The handling instance invalidates its lookup cache, but other instances can retain an existing entry for up to five minutes.",
        "responses": {
          "200": {
            "description": "Current name and visibility.",
            "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
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignInRequired"
          },
          "403": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownSite"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "FirebaseIdToken": []
          }
        ],
        "requestBody": {
          "required": false,
          "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. Associated import-job records are deleted by database cascade; previously imported analytics remain.",
        "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/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownSite"
          },
          "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 site or owner. Access is checked before query validation. Returns totals, previous equal-length period, sparse series, live and granularity. Pageviews/visitors filter hourly aggregates by bucket start: non-hour-aligned bounds differ from exact-timestamp breakdowns. Append averages filter individual append timestamps and are per append row, not pageview. Visitor identifiers rotate daily in UTC; overview distinct counts are approximate. Live counts all non-bot row types over the last five minutes, independent of the selected range.",
        "responses": {
          "200": {
            "description": "Traffic overview.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OverviewResponse"
                },
                "example": {
                  "totals": {
                    "pageviews": 3,
                    "visitors": 2,
                    "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
                    },
                    {
                      "t": 1789520400,
                      "pageviews": 2,
                      "visitors": 2
                    }
                  ],
                  "live": 0,
                  "granularity": "hour"
                }
              }
            }
          },
          "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/SignInRequired"
          },
          "403": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownSite"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {},
          {
            "FirebaseIdToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "$ref": "#/components/parameters/Timezone"
          }
        ]
      },
      "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}/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 site or owner. Counts raw non-bot rows within exact timestamps; events counts event rows and other dimensions count pageviews. Group visitors are exact distinct daily-rotating identifiers. Empty labels are omitted except pages/referrers, where they become Direct / none. Sorted by value descending. tz is ignored. No metadata query, arbitrary filters, pagination cursor, offset or total-row count. Access is checked before dimension and range validation.",
        "responses": {
          "200": {
            "description": "Ranked groups.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BreakdownResponse"
                },
                "example": {
                  "rows": [
                    {
                      "name": "/pricing",
                      "value": 3,
                      "visitors": 2
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing/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/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownSite"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {},
          {
            "FirebaseIdToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Dimension"
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ]
      },
      "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": "Serves the tracker script, which sends subsequent pageview and event requests. Loading the file alone does not collect a pageview. Configure it using script-tag attributes; see the installation guide.",
        "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=86400, stale-while-revalidate=604800"
                }
              }
            },
            "content": {
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "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. 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. Normally only registered hostnames are stored; a registry lookup failure can bypass that check. 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. Lowercased and truncated to 253 characters, not trimmed. Use the exact registered hostname without scheme, port or path.",
            "schema": {
              "type": "string"
            },
            "example": "your-domain.example",
            "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/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. 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/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": "No authentication. Shares the exact same handler and body as the other POST collector route. Both default to pageview, so set type explicitly. For append rows use type append and an optional original_id referring to the original pageview; it is not validated and does not deduplicate. JSON parsing is required before DNT skipping. Writes run in the background; 200 is not durable confirmation. Unregistered hostnames normally drop, but a lookup failure can bypass the check. Bots can be stored but are excluded from reports. No client timestamp, country or IP override and no automatic retry/idempotency guarantee.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/CollectorOk"
          },
          "400": {
            "$ref": "#/components/responses/CollectorBadRequest"
          }
        },
        "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": "No authentication. Shares the exact same handler and body as the other POST collector route. Both default to pageview, so set type explicitly. For append rows use type append and an optional original_id referring to the original pageview; it is not validated and does not deduplicate. JSON parsing is required before DNT skipping. Writes run in the background; 200 is not durable confirmation. Unregistered hostnames normally drop, but a lookup failure can bypass the check. Bots can be stored but are excluded from reports. No client timestamp, country or IP override and no automatic retry/idempotency guarantee.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/CollectorOk"
          },
          "400": {
            "$ref": "#/components/responses/CollectorBadRequest"
          }
        },
        "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.",
        "responses": {
          "200": {
            "description": "The HTTP handler responded.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "const": "ok"
                },
                "example": "ok"
              }
            }
          }
        },
        "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 any signed-in Totallytics 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"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "FirebaseIdToken": []
          }
        ]
      },
      "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 Firebase ID bearer token 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": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownSite"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "FirebaseIdToken": []
          }
        ]
      },
      "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. Validates source credentials with a recent SimpleAnalytics export before creating the job; the request can take time. 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 rounds start down to its month boundary and creates pageview and event chunks through the inclusive end. New jobs do not deduplicate overlapping data, and no idempotency key is supported. Verify progress and actual reports before considering the migration complete. Do not automatically repeat POST after an ambiguous failure.",
        "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. Provider validation can return upstream error text.",
            "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": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownSite"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "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"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "Queue submission failed after job creation. The handler cancels the new job; 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 Firebase ID bearer token 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": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownImport"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "FirebaseIdToken": []
          }
        ]
      },
      "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. Stops later queued work, not an in-flight export or write, and does not roll back rows already written. Stored source configuration is retained for retry. The response uses the prior job snapshot with status set to canceled, so finished_at can be stale; poll GET for persisted state.",
        "responses": {
          "200": {
            "description": "Cancellation recorded; in-flight work may still write rows.",
            "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": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownImport"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "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"
                    }
                  }
                }
              }
            }
          }
        },
        "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, but partial-chunk writes and ambiguous insert retries may duplicate rows. Not idempotent or exactly-once. The creation-rate cap is not reapplied. Queue submission failure can return 500 after status was changed to queued. Another active job on the site can also produce 500 from the unique constraint. The response uses the prior snapshot with status queued and error null; finished_at can be stale. Poll GET before further actions.",
        "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": {
            "$ref": "#/components/responses/NotYourSite"
          },
          "404": {
            "$ref": "#/components/responses/UnknownImport"
          },
          "500": {
            "description": "Queue submission can fail after the job is changed to queued; an existing active job can also reject retry. Inspect persisted state before another action.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Job is neither failed nor canceled.",
            "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"
                    }
                  }
                }
              }
            }
          }
        },
        "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": []
      }
    }
  },
  "components": {
    "securitySchemes": {
      "FirebaseIdToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Current Firebase ID token for an Totallytics 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."
      }
    },
    "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 site metadata and stats reads."
          },
          "created_at": {
            "type": "string",
            "description": "ISO 8601 creation timestamp.",
            "format": "date-time"
          }
        },
        "required": [
          "hostname",
          "display_name",
          "is_public",
          "created_at"
        ],
        "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 site metadata and stats reads."
          },
          "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."
          }
        },
        "required": [
          "hostname",
          "display_name",
          "is_public",
          "created_at",
          "owner_uid"
        ],
        "additionalProperties": false
      },
      "SitesResponse": {
        "type": "object",
        "properties": {
          "sites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OwnedSite"
            },
            "description": "All owned sites, ordered by creation time. No pagination."
          }
        },
        "required": [
          "sites"
        ],
        "additionalProperties": false
      },
      "CreateSite": {
        "type": "object",
        "required": [
          "hostname"
        ],
        "properties": {
          "hostname": {
            "type": "string",
            "description": "Trimmed and lowercased before validation. Normalized value is at most 253 characters and matches ^(?!-)[a-z0-9-]{1,63}(?<!-)(\\.[a-z0-9-]{1,63})+$. Use a hostname without scheme, port or path.",
            "examples": [
              "your-domain.example"
            ]
          }
        },
        "additionalProperties": true,
        "description": "Only hostname is 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."
          }
        },
        "required": [
          "hostname"
        ],
        "additionalProperties": false
      },
      "UpdateSite": {
        "type": "object",
        "properties": {
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Truncated to 128 characters, not rejected for excess length. Empty string clears; omitted or null retains the current name."
          },
          "is_public": {
            "type": "boolean",
            "description": "True enables anonymous metadata and stats reads. Omitted or non-boolean input is ignored by the implementation."
          }
        },
        "additionalProperties": true,
        "description": "Only these fields are read. Empty objects and unreadable JSON leave current values unchanged. Other incorrectly typed fields may cause a 500, not a validation error."
      },
      "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."
          }
        },
        "required": [
          "hostname",
          "display_name",
          "is_public"
        ],
        "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": "Non-bot pageviews from hourly aggregates."
          },
          "visitors": {
            "type": "integer",
            "minimum": 0,
            "description": "Approximate distinct pageview visitor identifiers, not deduplicated people across UTC days."
          },
          "avg_duration_s": {
            "type": "integer",
            "minimum": 0,
            "description": "Rounded mean duration in seconds over non-bot append rows; zero when none exist."
          },
          "avg_scroll": {
            "type": "integer",
            "minimum": 0,
            "description": "Rounded mean scroll percentage over non-bot append rows; zero when none exist.",
            "maximum": 100
          }
        },
        "required": [
          "pageviews",
          "visitors",
          "avg_duration_s",
          "avg_scroll"
        ],
        "additionalProperties": false
      },
      "SeriesPoint": {
        "type": "object",
        "properties": {
          "t": {
            "type": "integer",
            "format": "int64",
            "description": "Unix seconds. Daily bucket conversion does not guarantee midnight in the requested timezone."
          },
          "pageviews": {
            "type": "integer",
            "minimum": 0,
            "description": "Non-bot pageviews in this bucket."
          },
          "visitors": {
            "type": "integer",
            "minimum": 0,
            "description": "Approximate distinct pageview visitor identifiers in this bucket."
          }
        },
        "required": [
          "t",
          "pageviews",
          "visitors"
        ],
        "additionalProperties": false
      },
      "OverviewResponse": {
        "type": "object",
        "properties": {
          "totals": {
            "$ref": "#/components/schemas/OverviewMetrics"
          },
          "previous": {
            "$ref": "#/components/schemas/OverviewMetrics",
            "description": "Immediately preceding interval of equal duration."
          },
          "series": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SeriesPoint"
            },
            "description": "Ascending sparse buckets. Quiet buckets are not zero-filled; append activity can create a zero-pageview bucket."
          },
          "live": {
            "type": "integer",
            "minimum": 0,
            "description": "Exact distinct identifiers across all non-bot row types in the last five minutes, independent of the requested range."
          },
          "granularity": {
            "type": "string",
            "enum": [
              "hour",
              "day"
            ],
            "description": "Hour for a rounded span up to four days; day otherwise."
          }
        },
        "required": [
          "totals",
          "previous",
          "series",
          "live",
          "granularity"
        ],
        "additionalProperties": false
      },
      "BreakdownRow": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Group label. Empty page/referrer values 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": "Exact distinct visitor identifiers within this group."
          }
        },
        "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": "Non-bot pageviews from hourly aggregates."
          },
          "visitors": {
            "type": "integer",
            "minimum": 0,
            "description": "Approximate distinct pageview visitor identifiers within this day."
          }
        },
        "required": [
          "day",
          "pageviews",
          "visitors"
        ],
        "additionalProperties": false
      },
      "SummarySite": {
        "type": "object",
        "properties": {
          "hostname": {
            "type": "string",
            "description": "Owned registered hostname."
          },
          "live": {
            "type": "integer",
            "minimum": 0,
            "description": "Exact distinct non-bot visitor identifiers across all row types in the last five minutes."
          },
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SummaryDay"
            },
            "description": "Sparse ascending days, not zero-filled. Empty for sites without rolled-up activity."
          }
        },
        "required": [
          "hostname",
          "live",
          "days"
        ],
        "additionalProperties": false
      },
      "SummaryResponse": {
        "type": "object",
        "properties": {
          "sites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SummarySite"
            },
            "description": "Every owned site, ordered by creation time; empty if no sites are owned."
          }
        },
        "required": [
          "sites"
        ],
        "additionalProperties": false
      },
      "CollectorPayload": {
        "type": "object",
        "required": [
          "hostname"
        ],
        "properties": {
          "hostname": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Required after scalar-to-string conversion. Lowercased and truncated to 253 characters, not trimmed. Use the exact registered hostname without scheme, port or path.",
            "examples": [
              "your-domain.example"
            ]
          },
          "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": "Truncated to 2048 characters. Use hostname/path without a URL scheme; the first slash-separated segment becomes the lowercased referrer hostname.",
            "examples": [
              "news.example/article"
            ]
          },
          "metadata": {
            "type": [
              "object",
              "array",
              "string",
              "null"
            ],
            "description": "Objects and arrays are JSON-serialized. Strings are stored as supplied. Truncated to 4096 characters even if this breaks JSON. Not retrievable, filterable or groupable through the stats API."
          },
          "id": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Row correlation ID. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes."
          },
          "page_id": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Page correlation ID. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes."
          },
          "session_id": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Session correlation ID, not used for visitor counts. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes."
          },
          "original_id": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Original pageview id or page_id for an append; not validated or joined for report averages. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes."
          },
          "duration": {
            "type": [
              "number",
              "string",
              "boolean"
            ],
            "default": 0,
            "description": "Duration in seconds. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 86400; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected."
          },
          "scrolled": {
            "type": [
              "number",
              "string",
              "boolean"
            ],
            "default": 0,
            "description": "Scroll percentage. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 100; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected."
          },
          "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. Drives browser, OS, device, bot and visitor classification."
          },
          "timezone": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Client timezone label, truncated to 64 characters. Does not affect row timestamps."
          },
          "language": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Language label, truncated to 16 characters."
          },
          "os_name": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Truncated to 64 characters. Non-empty input overrides the parsed OS name."
          },
          "os_version": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Truncated to 64 characters. Non-empty input overrides the parsed OS version."
          },
          "brands": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Brand information as a serialized string, truncated to 256 characters."
          },
          "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": "Stored uniqueness hint; does not control stats visitor counts. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false."
          },
          "mobile": {
            "type": [
              "boolean",
              "number",
              "string"
            ],
            "default": false,
            "description": "Mobile hint. Parsed device type also contributes to the stored mobile flag. 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": "Browser-specific flag; no stats breakdown. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false."
          },
          "duck": {
            "type": [
              "boolean",
              "number",
              "string"
            ],
            "default": false,
            "description": "Browser-specific flag; no stats breakdown. 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": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns."
          },
          "utm_medium": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns."
          },
          "utm_campaign": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns."
          },
          "utm_term": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns."
          },
          "utm_content": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns."
          }
        },
        "additionalProperties": true,
        "allOf": [
          {
            "if": {
              "properties": {
                "type": {
                  "const": "event"
                }
              },
              "required": [
                "type"
              ]
            },
            "then": {
              "required": [
                "event"
              ]
            }
          }
        ],
        "description": "One payload, not a batch. Both /events and /append use this schema and default to type pageview. Unknown fields are ignored. Text limits are truncation, not request rejection. Most scalar fields coerce numbers and booleans to strings; object values are ignored outside metadata. IDs do not provide idempotency. Timestamp, country and visitor identifiers are server-derived; payload ts, timestamp, country, ip, visitor_id, time and sri do not override them. Bot matching and DNT skipping also 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 an Totallytics bearer token. Trimmed and truncated to 512 characters. Never echoed in job config; removed from stored config on completion but retained for failed/canceled retries.",
            "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 planner rounds this down to the first day of its month; use a month-boundary start to avoid including earlier dates.",
            "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 idempotency key or cross-job deduplication ledger. 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 retain their stored secret config for retry."
      },
      "ImportJob": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Job identifier generated at creation.",
            "format": "uuid"
          },
          "site": {
            "type": "string",
            "description": "Destination Totallytics 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. It does not reflect the planner rounding to month start.",
            "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": "Checkpointed inserted records, including pageviews, events and derived duration/scroll append rows. Not unique pageviews, unique visitors, or a guarantee of exact/durable stored totals. Partial writes may be absent and retry duplicates are not reconciled."
          },
          "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. Can be non-null while queued/running during retries. 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
      }
    },
    "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. Defaults to to minus 30 days. Missing, empty, zero or non-numeric input uses the default. Rounded range must be finite, positive in length and no longer than 400 days.",
        "schema": {
          "type": "number"
        },
        "example": 1789516800
      },
      "To": {
        "name": "to",
        "in": "query",
        "description": "Exclusive Unix seconds, rounded up. Defaults to current Unix seconds. Missing, empty, zero or non-numeric input uses the default. Not milliseconds.",
        "schema": {
          "type": "number"
        },
        "example": 1789603200
      },
      "Timezone": {
        "name": "tz",
        "in": "query",
        "description": "Series grouping or summary day timezone, not a range offset. Defaults UTC. Syntax accepts one or two slash-separated components of 1-32 letters/digits/_/+/-. Invalid syntax, including multi-slash zones, silently falls back to UTC. Accepted but unknown timezone names can return 500. Daily overview t values are not guaranteed to represent local midnight.",
        "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": "Send an integer. Default 10; numeric input is clamped to 1-100. Zero or non-numeric input uses 10. Fractions are not rounded and can return 500. No pagination or offset exists.",
        "schema": {
          "type": "integer",
          "default": 10
        },
        "example": 10
      },
      "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://your-domain.example/pricing?utm_source=newsletter"
      },
      "CollectHostname": {
        "name": "hostname",
        "in": "query",
        "description": "Required after scalar-to-string conversion. Lowercased and truncated to 253 characters, not trimmed. Use the exact registered hostname without scheme, port or path.",
        "schema": {
          "type": "string"
        },
        "example": "your-domain.example"
      },
      "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": "Truncated to 2048 characters. Use hostname/path without a URL scheme; the first slash-separated segment becomes the lowercased referrer hostname.",
        "schema": {
          "type": "string"
        }
      },
      "CollectMetadata": {
        "name": "metadata",
        "in": "query",
        "description": "URL-encoded metadata string, typically JSON. Truncated to 4096 characters; not exposed by stats. No object-style query serialization is used.",
        "schema": {
          "type": "string"
        }
      },
      "CollectId": {
        "name": "id",
        "in": "query",
        "description": "Row correlation ID. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes.",
        "schema": {
          "type": "string"
        }
      },
      "CollectPageId": {
        "name": "page_id",
        "in": "query",
        "description": "Page correlation ID. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes.",
        "schema": {
          "type": "string"
        }
      },
      "CollectSessionId": {
        "name": "session_id",
        "in": "query",
        "description": "Session correlation ID, not used for visitor counts. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes.",
        "schema": {
          "type": "string"
        }
      },
      "CollectOriginalId": {
        "name": "original_id",
        "in": "query",
        "description": "Original pageview id or page_id for an append; not validated or joined for report averages. Truncated to 64 characters. Optional, defaults empty. IDs do not deduplicate writes.",
        "schema": {
          "type": "string"
        }
      },
      "CollectDuration": {
        "name": "duration",
        "in": "query",
        "description": "Duration in seconds. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 86400; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected.",
        "schema": {
          "type": "string"
        }
      },
      "CollectScrolled": {
        "name": "scrolled",
        "in": "query",
        "description": "Scroll percentage. Numeric scalar strings are accepted. Rounded to the nearest integer and clamped to 100; missing, non-finite or non-positive values become zero. Input above the maximum is not rejected.",
        "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. Drives browser, OS, device, bot and visitor classification.",
        "schema": {
          "type": "string"
        }
      },
      "CollectTimezone": {
        "name": "timezone",
        "in": "query",
        "description": "Client timezone label, truncated to 64 characters. Does not affect row timestamps.",
        "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. Non-empty input overrides the parsed OS name.",
        "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": "Brand information as a serialized string, truncated to 256 characters.",
        "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": "Stored uniqueness hint; does not control stats visitor counts. True for true, 1, \"true\" or \"1\"; other supplied 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. 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": "Browser-specific flag; no stats breakdown. True for true, 1, \"true\" or \"1\"; other supplied scalar values are false.",
        "schema": {
          "type": "string"
        }
      },
      "CollectDuck": {
        "name": "duck",
        "in": "query",
        "description": "Browser-specific flag; no stats breakdown. 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": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns.",
        "schema": {
          "type": "string"
        }
      },
      "CollectUtmMedium": {
        "name": "utm_medium",
        "in": "query",
        "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns.",
        "schema": {
          "type": "string"
        }
      },
      "CollectUtmCampaign": {
        "name": "utm_campaign",
        "in": "query",
        "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns.",
        "schema": {
          "type": "string"
        }
      },
      "CollectUtmTerm": {
        "name": "utm_term",
        "in": "query",
        "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats breakdowns.",
        "schema": {
          "type": "string"
        }
      },
      "CollectUtmContent": {
        "name": "utm_content",
        "in": "query",
        "description": "Truncated to 256 characters. A non-empty value with the same name parsed from query takes precedence. Only source, medium and campaign have stats 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"
      }
    },
    "requestBodies": {
      "Collector": {
        "required": true,
        "description": "One JSON object. /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": "your-domain.example",
                  "type": "event",
                  "event": "signup",
                  "metadata": {
                    "plan": "pro"
                  }
                }
              },
              "append": {
                "value": {
                  "hostname": "your-domain.example",
                  "type": "append",
                  "original_id": "pageview-id",
                  "duration": 42,
                  "scrolled": 75
                }
              },
              "pageview": {
                "value": {
                  "hostname": "your-domain.example",
                  "type": "pageview",
                  "path": "/pricing"
                }
              }
            }
          },
          "text/plain": {
            "schema": {
              "type": "string",
              "description": "JSON-encoded single CollectorPayload object, not arbitrary text."
            },
            "example": "{\"hostname\":\"your-domain.example\",\"type\":\"append\",\"original_id\":\"pageview-id\",\"duration\":42,\"scrolled\":75}"
          }
        }
      }
    },
    "responses": {
      "SignInRequired": {
        "description": "A required Firebase ID token is missing, invalid or expired.",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/NoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "sign in required"
            }
          }
        }
      },
      "NotYourSite": {
        "description": "Authenticated caller is not the owner of the private read or mutation target.",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/NoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not your site"
            }
          }
        }
      },
      "UnknownSite": {
        "description": "The hostname has no registered site.",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/NoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unknown site"
            }
          }
        }
      },
      "RouteNotFound": {
        "description": "No matching product API route.",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/NoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not found"
            }
          }
        }
      },
      "SiteExists": {
        "description": "The hostname is already registered.",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/NoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "site already exists"
            }
          }
        }
      },
      "InternalError": {
        "description": "Request could not be completed. Unknown timezone names, fractional breakdown limits or incorrectly typed body fields 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. DNT-skipped and normally discarded unregistered-hostname requests also succeed. 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. DNT skips and unregistered hostnames can also return this response.",
        "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 object. Arrays are not a batch format and normally fail hostname validation. 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"
              }
            }
          }
        }
      },
      "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 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"
                }
              }
            }
          }
        }
      }
    },
    "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"
        }
      }
    }
  }
}
