openapi: 3.1.0
info:
  title: Lodd API
  version: "2026-05-08"
  description: |
    Headless web analytics for AI agents.

    All success responses are wrapped in `{ "data": ..., "request_id": "uuid" }`.
    All error responses use `{ "error": { "type": "api_error", "code": "...", "message": "..." }, "request_id": "uuid" }`.
  contact:
    name: Lodd
    url: https://lodd.dev
  license:
    name: Proprietary

servers:
  - url: https://api.lodd.dev/v1

security:
  - ApiKeyHeader: []
  - BearerAuth: []

components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key passed via X-API-Key header.
    BearerAuth:
      type: http
      scheme: bearer
      description: API key passed as a Bearer token.

  parameters:
    SiteId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Site UUID.
    StartDate:
      name: start_date
      in: query
      schema:
        type: string
        format: date
      description: Start of date range (ISO 8601). Default 30 days ago.
    EndDate:
      name: end_date
      in: query
      schema:
        type: string
        format: date
      description: End of date range (ISO 8601). Default now.
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 10
        maximum: 1000
      description: Maximum number of results.
    Timezone:
      name: timezone
      in: query
      schema:
        type: string
        default: UTC
      description: IANA timezone for date bucketing.
    Interval:
      name: interval
      in: query
      schema:
        type: string
        enum: [day, hour]
        default: day
      description: Bucket interval.
    EventName:
      name: event_name
      in: query
      required: true
      schema:
        type: string
      description: Event name to filter by.
    EventNameOptional:
      name: event_name
      in: query
      schema:
        type: string
      description: Optional event name filter.
    FilterCountry:
      name: filter_country
      in: query
      schema:
        type: string
      description: "2-letter country code (e.g. US)."
    FilterBrowser:
      name: filter_browser
      in: query
      schema:
        type: string
      description: "Browser name (e.g. Chrome)."
    FilterOs:
      name: filter_os
      in: query
      schema:
        type: string
      description: "Operating system (e.g. macOS)."
    FilterDeviceType:
      name: filter_device_type
      in: query
      schema:
        type: string
        enum: [desktop, mobile, tablet]
      description: Device type.
    FilterUtmSource:
      name: filter_utm_source
      in: query
      schema:
        type: string
      description: UTM source exact match.
    FilterReferrerContains:
      name: filter_referrer_contains
      in: query
      schema:
        type: string
      description: Substring match on referrer.

  schemas:
    Site:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        domain:
          type: string
        tracking_secret:
          type: string
        created_at:
          type: string
          format: date-time
      required: [id, name, domain, created_at]

    SiteResolved:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        domain:
          type: string
        created_at:
          type: string
          format: date-time
      required: [id, name, domain, created_at]

    Analytics:
      type: object
      properties:
        total_page_views:
          type: integer
        unique_visitors:
          type: integer
        unique_countries:
          type: integer
        average_duration:
          type: number
        pages_per_visit:
          type: number
        bounce_rate:
          type: number

    Snapshot:
      type: object
      properties:
        today_visitors:
          type: integer
        yesterday_visitors:
          type: integer
        visitor_change_percent:
          type: number
        top_referrer:
          type: string
        top_country:
          type: string
        average_duration_today:
          type: number
        average_duration_yesterday:
          type: number
        duration_change_percent:
          type: number

    TimeseriesBucket:
      type: object
      properties:
        date_label:
          type: string
        page_views:
          type: integer
        unique_visitors:
          type: integer

    PageBreakdown:
      type: object
      properties:
        url:
          type: string
        page_views:
          type: integer
        bounce_rate:
          type: number
        average_duration:
          type: number

    SourceBreakdown:
      type: object
      properties:
        source_name:
          type: string
        visitors:
          type: integer
        page_views:
          type: integer
        bounce_rate:
          type: number
        average_duration:
          type: number

    CountryBreakdown:
      type: object
      properties:
        country:
          type: string
        visitors:
          type: integer
        page_views:
          type: integer
        bounce_rate:
          type: number
        average_duration:
          type: number

    BrowserBreakdown:
      type: object
      properties:
        browser:
          type: string
        visitors:
          type: integer
        page_views:
          type: integer

    OsBreakdown:
      type: object
      properties:
        os:
          type: string
        visitors:
          type: integer
        page_views:
          type: integer

    DeviceBreakdown:
      type: object
      properties:
        device_type:
          type: string
        visitors:
          type: integer
        page_views:
          type: integer

    EntryExitPage:
      type: object
      properties:
        url:
          type: string
        entries:
          type: integer
        exits:
          type: integer

    BotReport:
      type: object
      properties:
        user_agent:
          type: string
        hits:
          type: integer

    Event:
      type: object
      properties:
        event_name:
          type: string
        url:
          type: string
        properties:
          type: object
        timestamp:
          type: string
          format: date-time

    EventCount:
      type: object
      properties:
        event_name:
          type: string
        count:
          type: integer
        unique_sessions:
          type: integer

    EventTimeseriesBucket:
      type: object
      properties:
        date_label:
          type: string
        count:
          type: integer

    FunnelStep:
      type: object
      properties:
        step:
          type: integer
        type:
          type: string
        match:
          type: string
        sessions:
          type: integer
        conversion_rate:
          type: number

    FunnelStepInput:
      type: object
      properties:
        type:
          type: string
          enum: [pageview, event]
        match:
          type: string
      required: [type, match]

    ConversionPage:
      type: object
      properties:
        url:
          type: string
        conversions:
          type: integer
        conversion_rate:
          type: number

    SourceConversion:
      type: object
      properties:
        source_name:
          type: string
        conversions:
          type: integer
        conversion_rate:
          type: number

    Actor:
      type: object
      properties:
        actor:
          type: string
        event_count:
          type: integer
        first_seen:
          type: string
          format: date-time
        last_seen:
          type: string
          format: date-time

    ActorActivity:
      type: object
      properties:
        event_name:
          type: string
        url:
          type: string
        properties:
          type: object
        timestamp:
          type: string
          format: date-time

    RetentionCohort:
      type: object
      properties:
        cohort_week:
          type: string
        total_actors:
          type: integer
        retained:
          type: object
          additionalProperties:
            type: integer

    Annotation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        content:
          type: string
        timestamp:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    TrackableLink:
      type: object
      properties:
        id:
          type: string
          format: uuid
        short_code:
          type: string
        destination_url:
          type: string
          format: uri
        source_type:
          type: string
        source_label:
          type: string
        click_count:
          type: integer
        status:
          type: string
          enum: [active, archived]
        created_at:
          type: string
          format: date-time
        last_clicked_at:
          type: string
          format: date-time
          nullable: true

    LinkClicksResponse:
      type: object
      properties:
        link:
          type: object
          properties:
            id:
              type: string
              format: uuid
            short_code:
              type: string
            destination_url:
              type: string
            source_type:
              type: string
            source_label:
              type: string
            click_count:
              type: integer
        clicks:
          type: array
          items:
            type: object
            properties:
              clicked_at:
                type: string
                format: date-time
              referrer:
                type: string
                nullable: true
              country:
                type: string
                nullable: true
        total_clicks_in_period:
          type: integer

    Usage:
      type: object
      properties:
        plan:
          type: string
          enum: [free, paid]
        used:
          type: integer
        limit:
          type: integer
        month:
          type: string
        percent:
          type: integer

    ApiKey:
      type: object
      properties:
        id:
          type: string
          format: uuid
        prefix:
          type: string
        name:
          type: string
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        is_current:
          type: boolean
        status:
          type: string
          enum: [active, revoked]

    ApiKeyCreated:
      type: object
      properties:
        api_key:
          type: string
          description: The raw key. Only returned once.
        id:
          type: string
          format: uuid
        prefix:
          type: string
        name:
          type: string
        created_at:
          type: string
          format: date-time
        warning:
          type: string

    PerformanceMetric:
      type: object
      properties:
        group:
          type: string
        avg_load_time:
          type: number
        median_load_time:
          type: number
        p95_load_time:
          type: number
        sample_count:
          type: integer

    IpExclusionResult:
      type: object
      properties:
        status:
          type: string
          enum: [excluded, already_excluded]
        ip_hash_prefix:
          type: string
        message:
          type: string

    SuccessResponse:
      type: object
      properties:
        data: {}
        request_id:
          type: string
          format: uuid
      required: [data, request_id]

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              const: api_error
            code:
              type: string
              enum:
                - auth_required
                - invalid_api_key
                - access_denied
                - invalid_request
                - usage_limit_reached
                - route_not_found
                - upstream_unavailable
            message:
              type: string
          required: [type, code, message]
        request_id:
          type: string
          format: uuid
      required: [error, request_id]

  responses:
    Unauthorized:
      description: Authentication required or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Access denied to the requested resource.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    BadRequest:
      description: Missing or invalid parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    PaymentRequired:
      description: Monthly event limit reached.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    TooManyRequests:
      description: Rate limit exceeded. Check Retry-After header.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Unix timestamp when the rate limit window resets.
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    BadGateway:
      description: Analytics service temporarily unavailable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

  # Webhook schema is referenced inline below
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
            enum: [usage.threshold, usage.limit_reached]
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        last_triggered_at:
          type: string
          format: date-time
          nullable: true
        failure_count:
          type: integer

paths:
  # --- Sites ---

  /sites:
    get:
      operationId: listSites
      summary: List all sites
      tags: [Sites]
      responses:
        "200":
          description: List of sites.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Site"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createSite
      summary: Create a site
      tags: [Sites]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                domain:
                  type: string
              required: [name, domain]
      responses:
        "200":
          description: Created site.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Site"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /sites/resolve:
    get:
      operationId: resolveSite
      summary: Resolve domain to site UUID
      tags: [Sites]
      parameters:
        - name: domain
          in: query
          required: true
          schema:
            type: string
          description: Domain to resolve (normalised automatically).
      responses:
        "200":
          description: Resolved site.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/SiteResolved"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # --- Analytics ---

  /sites/{id}/analytics:
    get:
      operationId: getAnalytics
      summary: Aggregate analytics
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Aggregate stats.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Analytics"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/snapshot:
    get:
      operationId: getSnapshot
      summary: Today vs yesterday snapshot
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/Timezone"
      responses:
        "200":
          description: Snapshot comparison.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Snapshot"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/timeseries:
    get:
      operationId: getTimeseries
      summary: Visitor and page view counts over time
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Interval"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Time series buckets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/TimeseriesBucket"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/realtime:
    get:
      operationId: getRealtime
      summary: Active visitors in last 5 minutes
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/SiteId"
      responses:
        "200":
          description: Number of active visitors.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: integer
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/performance:
    get:
      operationId: getPerformance
      summary: Page load time metrics
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/Limit"
        - name: group_by
          in: query
          schema:
            type: string
            enum: [page, device, country, browser]
            default: page
          description: Dimension to group metrics by.
      responses:
        "200":
          description: Performance metrics.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/PerformanceMetric"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # --- Breakdowns ---

  /sites/{id}/pages:
    get:
      operationId: getPages
      summary: Top pages with engagement metrics
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - name: url_pattern
          in: query
          schema:
            type: string
          description: Filter to pages matching this substring.
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Page breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/PageBreakdown"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/sources:
    get:
      operationId: getSources
      summary: Traffic sources with engagement metrics
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Source breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SourceBreakdown"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/countries:
    get:
      operationId: getCountries
      summary: Visitors by country
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Country breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CountryBreakdown"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/browsers:
    get:
      operationId: getBrowsers
      summary: Browser breakdown
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Browser breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/BrowserBreakdown"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/operating-systems:
    get:
      operationId: getOperatingSystems
      summary: Operating system breakdown
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: OS breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/OsBreakdown"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/devices:
    get:
      operationId: getDevices
      summary: Device type breakdown
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Device breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/DeviceBreakdown"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/entry-exit-pages:
    get:
      operationId: getEntryExitPages
      summary: Session entry and exit pages
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Entry and exit page data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/EntryExitPage"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/bots:
    get:
      operationId: getBots
      summary: Bot and crawler traffic report
      tags: [Breakdowns]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
      responses:
        "200":
          description: Bot report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/BotReport"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # --- Events ---

  /sites/{id}/events:
    get:
      operationId: getEvents
      summary: Raw event records
      tags: [Events]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/EventNameOptional"
      responses:
        "200":
          description: Event records.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Event"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/event-counts:
    get:
      operationId: getEventCounts
      summary: Event name totals
      tags: [Events]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Event counts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/EventCount"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/event-timeseries:
    get:
      operationId: getEventTimeseries
      summary: Single event plotted over time
      tags: [Events]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/EventName"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Interval"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Event time series.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/EventTimeseriesBucket"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # --- Conversions & Funnels ---

  /sites/{id}/funnel:
    get:
      operationId: getFunnelGet
      summary: Conversion funnel (via query param)
      tags: [Conversions]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - name: steps
          in: query
          required: true
          schema:
            type: string
          description: JSON-encoded array of funnel steps.
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      responses:
        "200":
          description: Funnel results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/FunnelStep"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: getFunnelPost
      summary: Conversion funnel (via POST body)
      tags: [Conversions]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/FilterCountry"
        - $ref: "#/components/parameters/FilterBrowser"
        - $ref: "#/components/parameters/FilterOs"
        - $ref: "#/components/parameters/FilterDeviceType"
        - $ref: "#/components/parameters/FilterUtmSource"
        - $ref: "#/components/parameters/FilterReferrerContains"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                steps:
                  type: array
                  items:
                    $ref: "#/components/schemas/FunnelStepInput"
              required: [steps]
      responses:
        "200":
          description: Funnel results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/FunnelStep"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/conversion-pages:
    get:
      operationId: getConversionPages
      summary: Pages that lead to conversions
      tags: [Conversions]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/EventName"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Conversion page data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ConversionPage"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/source-conversions:
    get:
      operationId: getSourceConversions
      summary: Sources ranked by conversion rate
      tags: [Conversions]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/EventName"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Source conversion data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SourceConversion"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # --- Actors ---

  /sites/{id}/actors:
    get:
      operationId: getActors
      summary: Active actors with event counts
      tags: [Actors]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Actor list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Actor"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/actors/{actor}/activity:
    get:
      operationId: getActorActivity
      summary: Event timeline for one actor
      tags: [Actors]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - name: actor
          in: path
          required: true
          schema:
            type: string
          description: Actor identifier (opaque hash).
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Actor activity timeline.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ActorActivity"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /sites/{id}/actors/retention:
    get:
      operationId: getActorRetention
      summary: Weekly cohort retention
      tags: [Actors]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Timezone"
      responses:
        "200":
          description: Retention cohort data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/RetentionCohort"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # --- Annotations ---

  /sites/{id}/annotations:
    get:
      operationId: listAnnotations
      summary: List annotations in a date range
      tags: [Annotations]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Annotations.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Annotation"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createAnnotation
      summary: Create an annotation
      tags: [Annotations]
      parameters:
        - $ref: "#/components/parameters/SiteId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  maxLength: 500
                  description: Annotation text.
                timestamp:
                  type: string
                  format: date-time
                  description: When the event happened. Defaults to now.
              required: [content]
      responses:
        "200":
          description: Created annotation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Annotation"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # --- Trackable Links ---

  /sites/{id}/links:
    get:
      operationId: listLinks
      summary: List trackable links with click counts
      tags: [Trackable Links]
      parameters:
        - $ref: "#/components/parameters/SiteId"
        - name: status
          in: query
          schema:
            type: string
            enum: [active, archived, all]
            default: active
          description: Filter by link status.
      responses:
        "200":
          description: Trackable links.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/TrackableLink"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createLink
      summary: Create a trackable link
      tags: [Trackable Links]
      parameters:
        - $ref: "#/components/parameters/SiteId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                destination_url:
                  type: string
                  format: uri
                  description: Must start with http:// or https://.
                source_type:
                  type: string
                  description: "Channel (e.g. linkedin, twitter, email)."
                source_label:
                  type: string
                  description: Optional human-readable label.
              required: [destination_url, source_type]
      responses:
        "200":
          description: Created link.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TrackableLink"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /links/{code}/clicks:
    get:
      operationId: getLinkClicks
      summary: Click data for a trackable link
      tags: [Trackable Links]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Short code of the trackable link.
        - $ref: "#/components/parameters/StartDate"
        - $ref: "#/components/parameters/EndDate"
      responses:
        "200":
          description: Link click data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/LinkClicksResponse"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # --- IP Exclusion ---

  /sites/{id}/exclude-ip:
    post:
      operationId: excludeIp
      summary: Exclude an IP from tracking
      tags: [IP Exclusion]
      parameters:
        - $ref: "#/components/parameters/SiteId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                ip_address:
                  type: string
                  description: IP to exclude. Auto-detected if omitted.
      responses:
        "201":
          description: IP excluded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/IpExclusionResult"
                  request_id:
                    type: string
                    format: uuid
        "200":
          description: IP already excluded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/IpExclusionResult"
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # --- Account ---

  /usage:
    get:
      operationId: getUsage
      summary: Current month event usage
      tags: [Account]
      responses:
        "200":
          description: Usage data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Usage"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"

  /keys:
    get:
      operationId: listKeys
      summary: List API keys
      tags: [Account]
      responses:
        "200":
          description: API keys.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createKey
      summary: Create a new API key
      tags: [Account]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 100
                  description: Optional name for the key.
      responses:
        "201":
          description: Created key. The raw key is returned only once.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ApiKeyCreated"
                  request_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"

  /keys/{id}/revoke:
    post:
      operationId: revokeKey
      summary: Revoke an API key
      tags: [Account]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the key to revoke.
      responses:
        "200":
          description: Key revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      key_prefix:
                        type: string
                      name:
                        type: string
                      revoked_at:
                        type: string
                        format: date-time
                      status:
                        type: string
                        const: revoked
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # --- Webhooks ---
  /webhooks:
    get:
      operationId: listWebhooks
      summary: List registered webhooks
      tags: [Webhooks]
      responses:
        "200":
          description: Array of webhooks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        url: { type: string, format: uri }
                        events: { type: array, items: { type: string } }
                        active: { type: boolean }
                        created_at: { type: string, format: date-time }
                        last_triggered_at: { type: string, format: date-time, nullable: true }
                        failure_count: { type: integer }
                  request_id: { type: string, format: uuid }
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createWebhook
      summary: Register a webhook
      description: Returns a signing secret (HMAC-SHA256) — save it, it cannot be retrieved again.
      tags: [Webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS URL to receive webhook deliveries.
                events:
                  type: array
                  items:
                    type: string
                    enum: [usage.threshold, usage.limit_reached]
                  description: Events to subscribe to. Defaults to all.
      responses:
        "201":
          description: Webhook registered with signing secret.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      url: { type: string, format: uri }
                      events: { type: array, items: { type: string } }
                      active: { type: boolean }
                      created_at: { type: string, format: date-time }
                      secret: { type: string, description: "HMAC-SHA256 signing secret. Save this — it cannot be retrieved again." }
                  request_id: { type: string, format: uuid }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /webhooks/{id}/delete:
    post:
      operationId: deleteWebhook
      summary: Delete a webhook
      tags: [Webhooks]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Webhook deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

tags:
  - name: Sites
    description: Manage tracked sites.
  - name: Analytics
    description: Aggregate analytics and time series data.
  - name: Breakdowns
    description: Traffic breakdowns by page, source, country, browser, OS, and device.
  - name: Events
    description: Custom event tracking and queries.
  - name: Conversions
    description: Conversion funnels and attribution.
  - name: Actors
    description: Privacy-safe visitor segments.
  - name: Annotations
    description: Mark notable events on your timeline.
  - name: Trackable Links
    description: Campaign attribution with per-click tracking.
  - name: Webhooks
    description: Event-driven notifications with HMAC-SHA256 signed payloads.
  - name: IP Exclusion
    description: Exclude IPs from analytics.
  - name: Account
    description: Usage, billing, and API key management.
