openapi: 3.1.0
info:
  title: Viewcast Select
  version: 0.2.0
  description: |
    Purchase, poll, and retrieve a reach-and-frequency analysis for a
    campaign scenario — the `select` analysis type on Viewcast, alongside
    its existing display-level measurement (draft/final/direct purchases).
    Served by a new `mw-api-viewcast-select` worker, kept separate from the
    live `mw-api-viewcast` worker for blast-radius isolation — a bug in
    this heavier, campaign-scale analysis path cannot destabilize live
    display-purchase traffic. Creating and managing the campaign scenario
    itself is a separate surface at `/v2/scenarios/campaigns/*`, served by
    `mw-api-scenarios` (see `scenarios.yaml`).

    Purchasing an analysis is asynchronous:
    `POST /v2/viewcast/measurements/purchase/select` returns `202` with
    `status: "queued"`; poll the existing
    `GET /v2/viewcast/measurements/{asset_id}` endpoint (documented in the
    Viewcast contract — this surface does not define a separate poll route)
    for state. Once state is `in_flight`, the report is available at
    `GET /v2/viewcast/select/{asset_id}` and updates weekly for as long as
    the campaign flight runs; once `completed`, the report is final and
    permanently retrievable. A failed run auto-refunds the purchase charge
    in full; the campaign scenario asset stays `draft` and is immediately
    re-purchasable.

    Authenticate with an org-scoped API key (`X-API-Key: mw_…`). Purchasing
    an analysis costs 5000 credits — the largest single-call charge in this
    API. Polling is free (same policy as every other Viewcast measurement
    poll); report retrieval costs 1 credit.
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com
servers:
  - url: https://api2.mworks.com/v2
    description: Production
security:
  - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
  schemas:
    ViewcastSelectStatus:
      type: string
      enum:
        - queued
        - running
        - completed
        - failed
      description: |
        Where the analysis run is: `queued` (accepted, not started),
        `running`, `completed` (the campaign asset's report is retrievable)
        or `failed` (`failure_reason` is populated and the purchase is
        auto-refunded in full).
    ViewcastSelectPurchaseRequest:
      type: object
      required:
        - asset_id
        - measurement_request_id
      properties:
        asset_id:
          type: string
          description: The campaign scenario asset to purchase an analysis for (owned by mw-api-scenarios).
        measurement_request_id:
          type: string
          format: uuid
          description: |
            Idempotency key you mint (UUID). A retry with the same key and
            an identical body replays the original result at no additional
            charge; the same key with a different body is rejected with
            `409`. Carried in the request body, not an `Idempotency-Key`
            header.
      description: POST /measurements/purchase request body — asset_id rides in the body, not the path.
    ViewcastSelectResult:
      type: object
      required:
        - measurement_id
        - asset_id
        - status
        - queued_at
      description: |
        The `202` initial-acceptance body for a purchase — NOT the shape
        of a poll response. There is no dedicated Viewcast Select poll
        endpoint; ongoing status polling reuses the existing
        `GET /v2/viewcast/measurements/{asset_id}` route, which returns
        the Viewcast contract's own measurement-result shape (carrying an
        additional required `kind` field with no campaign-scenario value
        defined yet — reconciling the two shapes is a follow-up, not
        resolved here).
      properties:
        measurement_id:
          type: string
          nullable: true
          description: Null only in the brief poll-before-queue race window.
        asset_id:
          type: string
        status:
          $ref: '#/components/schemas/ViewcastSelectStatus'
        queued_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        refund_credits:
          type: integer
          description: Populated on `failed` — the 5000-credit purchase charge, auto-refunded in full.
        failure_reason:
          type: string
    MetricBasis:
      type: string
      enum:
        - measured
        - modeled
        - below_floor
      description: Per-cell provenance for an estimate.
    DataQualityStatus:
      type: string
      enum:
        - ok
        - structural_zero
        - below_floor
        - suppressed
        - outside_measured_surface
        - null_metric
      description: |
        Distinguishes a real measured zero (`structural_zero`) from a
        suppressed, below-floor, or otherwise-unavailable value. No zeros
        are ever silently substituted.
    MetricCellEstimate:
      type: object
      required:
        - status
        - value
        - ci
        - basis
        - data_quality_status
      properties:
        status:
          type: string
          const: estimated
        value:
          type: number
        ci:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
          description: |
            90% confidence interval as `[lower, upper]` (matches `VisitsCiSchema` /
            ADR-006 — not 95%). A directly measured quantity collapses this to
            `[value, value]`. Typed as a same-typed 2-element array rather than a
            strict `prefixItems` tuple (the repo's Spectral ruleset flags
            tuple-only arrays).
        basis:
          $ref: '#/components/schemas/MetricBasis'
        data_quality_status:
          $ref: '#/components/schemas/DataQualityStatus'
    MetricCellAbstained:
      type: object
      required:
        - status
        - abstained
        - reason
      properties:
        status:
          type: string
          const: abstained
        abstained:
          type: boolean
          const: true
        reason:
          type: string
          minLength: 1
      description: |
        A first-class abstention: the underlying observation was too thin
        to estimate honestly. Render as "not available", never as zero.
    MetricCell:
      oneOf:
        - $ref: '#/components/schemas/MetricCellEstimate'
        - $ref: '#/components/schemas/MetricCellAbstained'
      description: Every estimated quantity in a report is one of these two shapes — never a bare number.
    ImpressionPoint:
      type: object
      required:
        - date
        - measurement_state
        - time_basis
        - ots
        - lts
        - circulation
      properties:
        date:
          type: string
          format: date
        measurement_state:
          type: string
          enum:
            - projected
            - preliminary
            - final
        time_basis:
          type: string
        ots:
          $ref: '#/components/schemas/MetricCell'
        lts:
          $ref: '#/components/schemas/MetricCell'
        circulation:
          $ref: '#/components/schemas/MetricCell'
    ReachThreshold:
      type: object
      required:
        - effective_freq
        - reach
      properties:
        effective_freq:
          type: integer
          minimum: 1
        reach:
          $ref: '#/components/schemas/MetricCell'
    ReachBasis:
      type: object
      required:
        - thresholds
        - unique_exposure
        - total_exposures
        - average_frequency
      properties:
        thresholds:
          type: array
          items:
            $ref: '#/components/schemas/ReachThreshold'
        unique_exposure:
          $ref: '#/components/schemas/MetricCell'
        total_exposures:
          $ref: '#/components/schemas/MetricCell'
        average_frequency:
          $ref: '#/components/schemas/MetricCell'
    MarketRef:
      type: object
      required:
        - kind
      properties:
        kind:
          type: string
          enum:
            - market
            - market_group
            - national
          description: |
            A market slice counts residents of that geography; the
            always-on National slice counts everyone reached nationwide. A
            market slice is always at most the National slice.
        geography_id:
          type: string
        name:
          type: string
    SegmentRef:
      type: object
      required:
        - segment_id
      properties:
        segment_id:
          type: integer
        role:
          type: string
    MetricSlice:
      type: object
      required:
        - market
        - segment
        - impressions
        - reach
      properties:
        market:
          $ref: '#/components/schemas/MarketRef'
        segment:
          $ref: '#/components/schemas/SegmentRef'
        impressions:
          type: object
          required:
            - daily
            - cumulative
          properties:
            daily:
              type: array
              items:
                $ref: '#/components/schemas/ImpressionPoint'
            cumulative:
              type: array
              items:
                $ref: '#/components/schemas/ImpressionPoint'
        reach:
          type: object
          required:
            - ots
            - lts
          properties:
            ots:
              $ref: '#/components/schemas/ReachBasis'
            lts:
              $ref: '#/components/schemas/ReachBasis'
      description: |
        All metrics for one market × segment combination. The market axis
        always includes the requested market group (primary read), one
        slice per constituent market when the group has more than one
        member, and the always-on National slice.
    MarketDetail:
      type: object
      required:
        - geography_id
        - name
        - level
        - population
      properties:
        geography_id:
          type: string
        name:
          type: string
        level:
          type: string
          enum:
            - dma
            - cbsa
            - county
        population:
          type: object
          required:
            - base
          properties:
            base:
              type: integer
            by_segment:
              type: array
              items:
                type: object
                properties:
                  segment_id:
                    type: integer
                  population:
                    type: integer
    CampaignSpotDetail:
      type: object
      required:
        - display_id
      properties:
        display_id:
          type: string
        customer_spot_id:
          type: string
        windows:
          type: array
          items:
            type: object
            properties:
              start_date:
                type: string
                format: date
              end_date:
                type: string
                format: date
          description: This spot's ACTUAL delivered windows (actual end dates, not the nominal campaign end).
        assignment_status:
          type: string
          x-motionworks-status: roadmap
    CampaignFlight:
      type: object
      required:
        - start_date
        - end_date
      properties:
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
    CampaignCohort:
      type: object
      required:
        - geography_ids
        - customer_segment_ids
        - customer_base_segment_id
      properties:
        geography_ids:
          type: array
          items:
            type: string
        customer_segment_ids:
          type: array
          items:
            type: integer
        customer_base_segment_id:
          type: integer
        measures_release:
          type: integer
    CampaignCustomer:
      type: object
      required:
        - customer_id
        - customer_name
        - customer_scenario_id
        - customer_scenario_name
      properties:
        customer_id:
          type: integer
        customer_name:
          type: string
        customer_scenario_id:
          type: string
        customer_scenario_name:
          type: string
        customer_scenario_info:
          type: string
    ViewcastSelectReport:
      type: object
      required:
        - asset_id
        - flight
        - cohort
        - customer
        - markets
        - spot_details
        - metric_slices
      properties:
        asset_id:
          type: string
        flight:
          $ref: '#/components/schemas/CampaignFlight'
        cohort:
          $ref: '#/components/schemas/CampaignCohort'
        customer:
          $ref: '#/components/schemas/CampaignCustomer'
        markets:
          type: array
          items:
            $ref: '#/components/schemas/MarketDetail'
        spot_details:
          type: array
          items:
            $ref: '#/components/schemas/CampaignSpotDetail'
        metric_slices:
          type: array
          items:
            $ref: '#/components/schemas/MetricSlice'
        measurement_state_boundaries:
          type: object
          properties:
            final_through:
              type: string
              format: date
              nullable: true
            preliminary_through:
              type: string
              format: date
              nullable: true
            projected_from:
              type: string
              format: date
              nullable: true
        geopath:
          type: object
          nullable: true
          description: Optional, license-gated Geopath plan-state comparison. Roadmap.
          x-motionworks-status: roadmap
        debug:
          type: object
          additionalProperties: true
          description: Intermediate-measurement context. Shape may change; do not build against it except `debug.messages`.
      description: Retrievable once the campaign asset's `state` is `measured` — permanently.
    Provenance:
      type: object
      properties:
        source:
          type: string
        source_doc:
          type: string
          format: uri
        methodology_version:
          type: string
        data_vintage:
          type: string
          format: date
        data_freshness:
          type: string
          enum:
            - hourly
            - daily
            - weekly
            - monthly
            - annually
            - on-demand
            - static
        data_latency_days:
          type: integer
        data_maturity:
          type: string
          enum:
            - production
            - research-preview
            - synthetic-only
            - roadmap
    Meta:
      type: object
      properties:
        request_id:
          type: string
        credits_used:
          type: integer
        credits_remaining:
          type: integer
        product:
          type: string
        version:
          type: string
        provenance:
          $ref: '#/components/schemas/Provenance'
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            status:
              type: integer
            request_id:
              type: string
            product:
              type: string
            docs_url:
              type: string
paths:
  /viewcast/measurements/purchase/select:
    post:
      operationId: purchaseViewcastSelect
      summary: Purchase a Viewcast Select analysis
      description: |
        Purchases and runs a reach-and-frequency Viewcast Select analysis
        for a campaign scenario asset you own. `asset_id` rides in the
        body, not the path. Costs 5000 credits, charged on acceptance.
        `202` + `status: "queued"`. Automatically refunded in full if the
        run fails. Poll `GET /v2/viewcast/measurements/{asset_id}` — the
        existing, free Viewcast measurement-poll route, not a new
        endpoint on this surface — for completion.
      tags:
        - Measurements
      x-credit-cost: 5000
      x-motionworks-status: production
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ViewcastSelectPurchaseRequest'
      responses:
        '202':
          description: Accepted and queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ViewcastSelectResult'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '400':
          description: Invalid request, or the asset is not in a purchasable state (`draft` or `failed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Unknown asset_id, or an asset that does not belong to your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`measurement_request_id` reused with a different body.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /viewcast/select/{asset_id}:
    get:
      operationId: getViewcastSelectReport
      summary: Retrieve a completed Viewcast Select report
      description: |
        Returns the report for a campaign scenario asset while its flight
        is `in_flight` (interim, updated weekly) or once it is `completed`
        (final, permanently retrievable): resolved markets, per-spot
        delivery detail, and confidence-banded `metric_slices`. 1 credit
        per call.
      tags:
        - Reports
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: asset_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ViewcastSelectReport'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '404':
          description: Unknown asset_id, an asset that does not belong to your organization, or an asset in `draft`, `queued`, or `failed` state (no report to serve yet).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
x-tagGroups:
  - name: Viewcast Select
    tags:
      - Measurements
      - Reports
tags:
  - name: Measurements
    description: |
      Purchase a Viewcast Select reach-and-frequency analysis. Poll for
      completion via the existing, free Viewcast measurement-poll route
      (not documented in this surface — see the Viewcast contract).
  - name: Reports
    description: Retrieve a completed Viewcast Select analysis report.
