openapi: 3.1.0
info:
  title: Qusto External API
  version: "1.1.0"
  description: |
    Core+ public API. Authenticate with `X-Qusto-API-Key` (Settings → API Access).
    Site-scoped routes require `site_id` owned by the key's workspace.
    Aggregate analytics and commerce metrics only — no PII / DSAR via this API.

    **1.1.0** — `/external/stats/*` now return a Qusto-owned envelope with an
    explicit, validated set of parameters instead of forwarding whatever was
    sent; `/external/stats/realtime` returns `{ visitors, ... }` instead of a
    bare number. Added `/external/data-coverage` and `/external/customers/ltv-bands`;
    `/external/customers/ltv` is now deprecated in favor of the band-suppressed version.
  contact:
    name: Qusto Support
    email: hello@qusto.io
    url: https://qusto.io/docs/api/
servers:
  - url: https://app.qusto.io/api/v1
security:
  - ApiKeyAuth: []
paths:
  /external/account:
    get:
      summary: Workspace account
      operationId: getAccount
      responses:
        "200":
          description: Account metadata
  /external/sites:
    get:
      summary: List sites
      operationId: listSites
      responses:
        "200":
          description: Site list
  /external/revenue:
    get:
      summary: Revenue summary
      operationId: getRevenue
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: Revenue aggregates
        "403":
          description: Forbidden (ownership or license)
  /external/stats/aggregate:
    get:
      summary: Aggregate traffic stats
      operationId: getStatsAggregate
      description: Totals for the period. `data` holds one entry per requested metric.
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/period"
        - $ref: "#/components/parameters/date"
        - $ref: "#/components/parameters/metrics"
      responses:
        "200":
          description: Aggregate totals
        "400":
          description: Invalid period, metric, limit or page — the message names the valid values
  /external/stats/timeseries:
    get:
      summary: Timeseries traffic stats
      operationId: getStatsTimeseries
      description: One row per day for the period. `data` is an array.
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/period"
        - $ref: "#/components/parameters/date"
        - $ref: "#/components/parameters/metrics"
      responses:
        "200":
          description: Daily rows
        "400":
          description: Invalid parameter
  /external/stats/breakdown:
    get:
      summary: Breakdown traffic stats by dimension
      operationId: getStatsBreakdown
      description: |
        Metrics grouped by `property`, which is required. `data` is an array
        ordered by the first requested metric, descending.
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/period"
        - $ref: "#/components/parameters/date"
        - $ref: "#/components/parameters/metrics"
        - $ref: "#/components/parameters/property"
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: Rows grouped by the requested property
        "400":
          description: Missing or unknown property, or invalid limit/page
  /external/stats/realtime:
    get:
      summary: Current visitors
      operationId: getStatsRealtime
      description: |
        Visitors active in the last 5 minutes. Returns
        `{ "site_id": ..., "visitors": 0, "generated_at": ... }`.
      parameters:
        - $ref: "#/components/parameters/siteId"
      responses:
        "200":
          description: Live visitor count
  /external/events:
    get:
      summary: Event name breakdown
      operationId: listEvents
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: Event breakdown
  /external/funnel/conversion:
    get:
      summary: Funnel / event conversion signal
      operationId: getFunnelConversion
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: Funnel steps
  /external/cart/abandonment:
    get:
      summary: Cart abandonment (Growth+)
      operationId: getCartAbandonment
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: Abandonment stats
        "403":
          description: Feature not on plan
  /external/customers/ltv:
    get:
      summary: Customer LTV rows (Growth+) — deprecated
      operationId: getCustomerLTV
      deprecated: true
      description: |
        Returns individual customer rows (token + exact spend). Kept only for
        integrations built before the k-anonymity work; not recommended for
        new integrations. Use `/external/customers/ltv-bands` instead.
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: LTV rows
  /external/customers/ltv-bands:
    get:
      summary: Customer LTV band distribution (Growth+)
      operationId: getCustomerLTVBands
      description: |
        k-anonymity-suppressed replacement for `/external/customers/ltv`: a
        spend-band distribution with customer counts, never individual rows.
        This is the endpoint the Qusto MCP server calls.
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: Spend bands with customer counts
        "403":
          description: Feature not on plan
  /external/data-coverage:
    get:
      summary: Browser-tag coverage of real orders (Core+)
      operationId: getDataCoverage
      description: |
        How many of the site's real, paid orders the browser tag never
        recorded. Orders reach Qusto both from the on-site beacon and from
        the store platform's server-to-server order webhook; orders present
        only in the webhook had no beacon session (blocked script, JS off,
        declined consent, abandoned redirect). `untagged_orders` and
        `untagged_revenue` are that measured gap; `tag_coverage_pct` is the
        share the tag did see. Aggregate counts and sums only — no order
        rows, no customer tokens.
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: Order counts, untagged revenue, and tag coverage percentage
        "403":
          description: Forbidden (site ownership or license)
  /external/attribution/summary:
    get:
      summary: Attribution summary (Professional+)
      operationId: getAttributionSummary
      parameters:
        - $ref: "#/components/parameters/siteId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200":
          description: Channel attribution
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Qusto-API-Key
  parameters:
    siteId:
      name: site_id
      in: query
      required: true
      schema:
        type: string
    from:
      name: from
      in: query
      required: true
      schema:
        type: string
        format: date
    to:
      name: to
      in: query
      required: true
      schema:
        type: string
        format: date
    period:
      name: period
      in: query
      required: false
      description: Relative window. Defaults to 30d. `custom` additionally requires `date`.
      schema:
        type: string
        default: 30d
        enum: [day, 7d, 30d, month, 6mo, 12mo, year, custom]
    date:
      name: date
      in: query
      required: false
      description: "Range for period=custom, as YYYY-MM-DD,YYYY-MM-DD."
      schema:
        type: string
    metrics:
      name: metrics
      in: query
      required: false
      description: Comma-separated. Defaults to visitors.
      schema:
        type: string
        default: visitors
        enum: [visitors, pageviews, visits, bounce_rate, visit_duration, events]
    property:
      name: property
      in: query
      required: false
      description: Breakdown dimension. Required for /stats/breakdown.
      schema:
        type: string
        enum:
          - event:name
          - event:page
          - visit:source
          - visit:referrer
          - visit:utm_source
          - visit:utm_medium
          - visit:utm_campaign
          - visit:country
          - visit:region
          - visit:city
          - visit:device
          - visit:browser
          - visit:os
    limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
    page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
