openapi: 3.1.0
info:
  title: Viewcast
  version: 1.0.0
  description: |
    The Viewcast API measures what an out-of-home display face can be seen from. Four capabilities share the base path `https://api2.mworks.com/v2/viewcast/*`:

    - **Display Validation** — before you build or buy a face, submit its location, dimensions and orientation plus two choices (may the aim move; which road counts as primary) and get back the viewshed, the search area, the primary road, and every road in view with its sightline and any occlusion. Synchronous; a call usually takes 20–45 seconds. **5 credits** per call; identical inputs are served from a 24-hour cache and still charged.
    - **On-Demand** — the full record for one of your Displays assets: its definition plus the resolved measurement detail. 1 credit.
    - **Measurements** — buy a draft (150 credits) or final (300 credits) measurement for an asset you own, or buy a final measurement directly from a `display_id` found in Display Search (300 credits). Draft and final are asynchronous — 202, then poll. Every purchase call also carries the 1-credit base cost; the readiness poll itself is free (0 credits).
    - **Profiles** — read the viewshed profile of a `final_measured` display your organization owns. 1 credit.

    Assets themselves are created and managed under the Displays API (`/v2/displays/assets`). Authenticate with an org-scoped API key (`X-API-Key: mw_…`) or a signed-in session. Prose guides: [Displays On Demand](https://www.mworks.com/docs/displays-on-demand/) and [Display Validation](https://www.mworks.com/docs/viewcast/viewcast-display-validation/).
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com
servers:
  - url: https://api2.mworks.com/v2
    description: Production
security:
  - apiKey: []
tags:
  - name: Display Validation
    description: |
      Check a display face before you build or buy it. One synchronous call returns the viewshed, the primary road and every road in view with sightlines. 5 credits per call; identical inputs are served from a 24-hour cache and still charged.
  - name: On-Demand
    description: |
      The asset definition plus resolved measurement detail for one asset your organization owns. 1 credit per call. Unknown assets, assets your organization does not own, and assets with no finished measurement yet all return 404 (never 403). Responses are never cached.
  - name: Measurements
    description: |
      Buy a draft or final measurement for an asset you own (202, then poll), or buy a final measurement directly from a `display_id` found in Display Search (synchronous). 1-credit base per purchase call plus the purchase: 150 credits for a draft, 300 for a final or direct measurement. The readiness poll itself is free (0 credits). Responses are never cached.
  - name: Profiles
    description: Read the viewshed profile of a `final_measured` display your organization owns. 1 credit per call.
components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
  schemas:
    DisplayValidationRequest:
      type: object
      additionalProperties: false
      required:
        - lat
        - lon
        - face_height_in
        - face_width_in
        - orientation
        - orientation_lock_type
        - primary_road_rule
      properties:
        lat:
          type: number
          format: double
          minimum: -90
          maximum: 90
          description: Latitude of the face's anchor point (WGS84 degrees).
        lon:
          type: number
          format: double
          minimum: -180
          maximum: 180
          description: Longitude of the face's anchor point (WGS84 degrees).
        face_height_in:
          type: number
          format: double
          exclusiveMinimum: 0
          description: Face height in inches (a 14 × 48 ft bulletin is 168 × 576).
        face_width_in:
          type: number
          format: double
          exclusiveMinimum: 0
          description: Face width in inches.
        orientation:
          type: number
          format: double
          minimum: 0
          maximum: 360
          description: |
            Compass direction the face points toward, in degrees clockwise from north (0 = north, 180 = south). Required in every request. With `full_range` the value you submit does not affect the result — every direction is searched — but it must still be present. The orientation actually used comes back as `chosen_orientation`.
        orientation_lock_type:
          type: string
          enum:
            - no_move
            - limited_move
            - full_range
          description: |
            Whether Motionworks may adjust the orientation: `no_move` keeps yours exactly; `limited_move` allows an adjustment of up to 30° either side of yours to find the best road; `full_range` searches every direction (for a sign that is not built yet).
        primary_road_rule:
          type: string
          enum:
            - veh_metres
            - nearest
            - busiest
          description: |
            Which road counts as primary: `veh_metres` — the road the face is most read from, weighting traffic by visible frontage (recommended); `busiest` — the highest-volume road in view; `nearest` — the road closest to the face.
    DisplayValidationOutcome:
      type: string
      enum:
        - validated
        - no_coverage
        - insufficient_data
        - rejected
      description: |
        The validation outcome. Any outcome other than `validated` — `no_coverage`, `insufficient_data` or `rejected` — is still a successful 200: the validation ran and the outcome is data. `error_message` carries the reason when one is available.
    Segment:
      type: object
      properties:
        segment_id:
          type: string
          description: Road segment identifier.
        highway:
          type: string
          description: OpenStreetMap highway class of the segment (for example `motorway`, `primary`, `residential`).
        osm_way:
          type: string
          description: OpenStreetMap way the segment belongs to.
        way_frontage_m:
          type: number
          description: Length of the way's frontage visible from the face, in metres.
        way_segments:
          type: number
          description: Number of segments the way contributes.
        direction:
          type: string
          description: Direction of travel of the segment, as reported by the validation procedure.
        bearing:
          type: number
          description: Bearing of the segment, in degrees clockwise from north.
      description: |
        A candidate road segment considered for the primary road. Roads are ranked as whole OpenStreetMap ways, so several segments may share one `osm_way`.
    InFrameDisplay:
      type: object
      properties:
        geometry:
          description: GeoJSON geometry of the segment.
        sightline:
          type: string
          description: GeoJSON line from the face to the nearest point of the segment.
        occlusion:
          type: boolean
          description: Whether a building blocks the sightline from the face to this segment.
        display_id:
          type: string
          description: Identifier of the in-frame feature as returned by the validation procedure.
        distance_m:
          type: number
          description: Distance from the face to the nearest point of the segment, in metres.
      description: |
        A road segment the face sees at the chosen orientation, with its geometry and the sightline from the face to its nearest point.
    DisplayValidationData:
      type: object
      description: |
        The validation result. Fields beyond those listed here are forwarded from the validation procedure unmodified; traffic-volume (AADT) fields are not included.
      required:
        - outcome
      properties:
        outcome:
          $ref: '#/components/schemas/DisplayValidationOutcome'
        error_message:
          type: string
          nullable: true
          description: Reason for a non-`validated` outcome, when available.
        requested_latitude:
          type: number
          description: Echo of the submitted anchor latitude.
        requested_longitude:
          type: number
          description: Echo of the submitted anchor longitude.
        face_height_in:
          type: number
          description: Echo of the submitted face height, inches.
        face_width_in:
          type: number
          description: Echo of the submitted face width, inches.
        orientation:
          type: number
          description: Echo of the submitted orientation, degrees clockwise from north.
        orientation_lock_type:
          type: string
          description: Echo of the submitted orientation lock.
        chosen_orientation:
          type: number
          description: |
            The orientation actually used, in degrees clockwise from north. Differs from `orientation` only when the lock allowed the aim to move.
        primary_road_rule:
          type: string
          description: Echo of the submitted primary-road rule.
        primary_road_bearing:
          type: number
          description: Bearing of the primary road at the point the face reads it, degrees clockwise from north.
        served_direction:
          type: string
          description: Which direction of travel on the primary road the face serves.
        primary_segment_id:
          type: string
          description: Segment identifier of the primary road.
        primary_highway:
          type: string
          description: OpenStreetMap highway class of the primary road.
        primary_osm_way:
          type: string
          description: OpenStreetMap way of the primary road.
        primary_way_frontage_m:
          type: number
          description: Metres of the primary road in view.
        primary_way_segments:
          type: number
          description: How many segments the primary road spans in view.
        segments:
          type: array
          items:
            $ref: '#/components/schemas/Segment'
          description: Every candidate road segment considered, ranked by the chosen rule.
        radius_m:
          type: number
          description: Reading distance of the face, in metres (depends on the site's urbanicity).
        search_geography:
          description: |
            GeoJSON polygon of everything that was searched — every aim the orientation lock allowed. The viewshed lies within it.
        viewshed:
          description: GeoJSON polygon of the viewshed at the chosen orientation — what the face delivers.
        urbanicity:
          type: string
          description: Urbanicity class of the site; it sets the reading distance.
        urbanicity_year:
          type: integer
          description: Vintage year of the urbanicity data.
        traffic_year:
          type: integer
          description: Vintage year of the traffic data.
        osm_vintage:
          type: string
          description: OpenStreetMap vintage used (YYYYMM).
        in_frame:
          type: array
          items:
            $ref: '#/components/schemas/InFrameDisplay'
          description: |
            The road segments the face sees at the chosen orientation: the geometry of each, the sightline to it, and whether a building blocks that sightline.
    Provenance:
      type: object
      required:
        - source
        - source_doc
        - methodology_version
        - data_vintage
        - data_freshness
        - data_latency_days
        - data_maturity
      properties:
        source:
          type: string
          description: The Motionworks dataset or model the result came from.
        source_doc:
          type: string
          format: uri
          description: Link to the methodology or dataset documentation for this result.
        methodology_version:
          type: string
          description: Version of the methodology that produced the result.
        data_vintage:
          type: string
          pattern: ^\d{4}-\d{2}-\d{2}$
          description: The date the underlying data represents.
        data_freshness:
          type: string
          enum:
            - hourly
            - daily
            - weekly
            - monthly
            - annually
            - on-demand
            - static
          description: How often the underlying data is refreshed.
        data_latency_days:
          type: integer
          minimum: 0
          description: Typical days between an observation and its availability in the API.
        data_maturity:
          type: string
          enum:
            - production
            - research-preview
            - synthetic-only
            - roadmap
          description: |
            Maturity of the data behind the result: `production` (supported and stable), `research-preview` (available, subject to change), `synthetic-only` or `roadmap` (not yet backed by production data).
    DisplayValidationMeta:
      type: object
      additionalProperties: false
      required:
        - request_id
        - credits_used
        - product
        - provenance
        - cached
      properties:
        request_id:
          type: string
          description: Unique id for this request. Quote it when contacting support.
        credits_used:
          type: integer
          description: 'Credits charged for the call: 5, whether or not the result came from cache.'
        product:
          type: string
          enum:
            - viewcast
          description: The product that served the call (`viewcast`).
        provenance:
          $ref: '#/components/schemas/Provenance'
        cached:
          type: boolean
          description: Whether the response came from the 24-hour cache.
    DisplayValidationResponse:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          $ref: '#/components/schemas/DisplayValidationData'
        meta:
          $ref: '#/components/schemas/DisplayValidationMeta'
    OnDemandMeasurement:
      type: object
      description: 'The measurement detail. Every field is optional: a completed result may carry a subset.'
      properties:
        attributes:
          type: object
          additionalProperties: true
          description: The resolved display descriptor.
        audience:
          type: object
          properties:
            traffic:
              type: integer
            face_circ:
              type: integer
            face_ots:
              type: integer
            face_lts:
              type: integer
            spot_circ:
              type: integer
            spot_ots:
              type: integer
            spot_lts:
              type: integer
            spot_length:
              type: integer
              minimum: 0
            share_of_voice:
              type: number
              minimum: 0
              maximum: 1
            face_share_of_voice:
              type: number
              minimum: 0
              maximum: 1
            dwell:
              type: number
            viewable_distance:
              type: number
          description: |
            The audience block: opportunities to see, likelihood to see, circulation, spot metrics, share of voice and related measures.
        viewshed_id:
          type: string
          description: The system viewshed identifier for the measured viewshed.
        viewshed_geography:
          $ref: '#/components/schemas/GeoJSONGeometry'
          description: The measured viewshed polygon as GeoJSON.
        location:
          $ref: '#/components/schemas/GeoJSONGeometry'
          description: The resolved measurement location as a GeoJSON point.
        market:
          type: object
          additionalProperties: false
          properties:
            co_geoid:
              type: string
            co_name:
              type: string
            st_name:
              type: string
            cbsa_geoid:
              type: string
            cbsa_name:
              type: string
            cbsa_type:
              type: string
            dma_geoid:
              type: string
            dma_name:
              type: string
            postal_code:
              type: string
          required:
            - co_geoid
            - co_name
            - st_name
            - dma_geoid
            - dma_name
          description: The resolved market geography record (county, state, CBSA, DMA geoids and names, CBSA type, postal code). Only the documented keys are served; anything else is stripped at projection time.
        period_start:
          type: number
          description: Measurement period start (YYYYMM).
        period_end:
          type: number
          description: Measurement period end (YYYYMM).
        period_days:
          type: number
          description: Measurement period length in days.
        assignments:
          type: array
          description: The path/segment assignment records for the measured display (returned on this endpoint only). Each record's path is GeoJSON.
          items:
            $ref: '#/components/schemas/ViewcastAssignment'
    GeoJSONGeometry:
      type: object
      description: A GeoJSON geometry (RFC 7946, longitude-first axis order). Point (location), LineString (assignment path), or Polygon/MultiPolygon (viewshed).
      required:
        - type
        - coordinates
      properties:
        type:
          type: string
          enum:
            - Point
            - LineString
            - Polygon
            - MultiPolygon
        coordinates:
          description: GeoJSON coordinate array, shaped by the geometry type.
    ViewcastAssignment:
      type: object
      description: A path/segment assignment record from the measurement run.
      properties:
        id:
          type: string
          description: The assignment identifier.
        type:
          type: string
          description: The assignment kind (for example `path`).
        path:
          $ref: '#/components/schemas/GeoJSONGeometry'
          description: The assignment path as GeoJSON.
      required:
        - path
    ViewcastOnDemandResponse:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          type: object
          description: The asset definition plus the measurement detail block.
          properties:
            asset_id:
              type: string
              minLength: 1
            org_id:
              type: string
            state:
              type: string
              enum:
                - draft
                - draft_measured
                - final_measured
            classification:
              type: string
              enum:
                - Roadside
                - Place-Based
            anchor:
              type: object
              properties:
                lat:
                  type: number
                  minimum: -90
                  maximum: 90
                lon:
                  type: number
                  minimum: -180
                  maximum: 180
            related_parties:
              type: array
              items:
                type: object
            display_id:
              type: string
            display:
              type: object
            user_reference:
              type: string
              nullable: true
            publication:
              type: string
              enum:
                - private
                - public
            subscription_started_at:
              type: string
              nullable: true
            subscription_expires_at:
              type: string
              nullable: true
            row_version:
              type: integer
            created_at:
              type: string
            updated_at:
              type: string
            measurement:
              $ref: '#/components/schemas/OnDemandMeasurement'
        meta:
          $ref: '#/components/schemas/ProvenanceMeta'
    ProvenanceMeta:
      type: object
      required:
        - request_id
        - credits_used
        - product
        - provenance
      properties:
        request_id:
          type: string
          description: Unique id for this request.
        credits_used:
          type: integer
          description: Credits charged for this call.
        product:
          type: string
          enum:
            - viewcast
          description: The product that served the call (`viewcast`).
        provenance:
          $ref: '#/components/schemas/Provenance'
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - status
            - request_id
          properties:
            code:
              type: string
            message:
              type: string
            status:
              type: integer
            request_id:
              type: string
            product:
              type: string
            docs_url:
              type: string
              format: uri
          description: 'The error: a stable `code`, a human-readable `message`, and optional `context` with the specifics.'
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            status:
              type: integer
            request_id:
              type: string
            docs_url:
              type: string
          description: 'The error: a stable `code`, a human-readable `message`, and optional `context` with the specifics.'
    ViewcastAssetAnchor:
      type: object
      required:
        - lat
        - lon
      properties:
        lat:
          type: number
          minimum: -90
          maximum: 90
          description: Latitude of the face (WGS84 degrees).
        lon:
          type: number
          minimum: -180
          maximum: 180
          description: Longitude of the face (WGS84 degrees).
    ViewcastMeasurementOwnedPurchaseRequest:
      type: object
      additionalProperties: false
      required:
        - asset_id
        - measurement_request_id
      description: |
        Purchase body for the `draft` and `final` types: the asset to measure and your idempotency id. The purchase type is in the path.
      properties:
        asset_id:
          type: string
          description: The asset you own that should be measured.
        measurement_request_id:
          type: string
          format: uuid
          description: |
            Your own idempotency identifier for this purchase (UUID). Replaying the same id with the same body returns the original result without a second charge; the same id with a different body returns 409 `idempotency_mismatch`.
    ViewcastMeasurementDirectPurchaseRequest:
      type: object
      additionalProperties: false
      required:
        - display_id
        - measurement_request_id
      description: |
        Purchase body for the `direct` type: just the `display_id`. The asset definition (classification, anchor, display descriptor) is sourced from the Motionworks display catalog at purchase time, not supplied by you. A `display_id` the catalog does not return 404s before any charge. Your organization may hold at most one active direct license per `display_id`: a re-attempt inside the 12-month subscription window 409s with an already-licensed message; purchasing again after the window lapses is allowed. Other organizations may license the same display independently.
      properties:
        display_id:
          type: string
          minLength: 1
          description: The display to purchase, as returned by Display Search. Recorded on the new asset.
        measurement_request_id:
          type: string
          format: uuid
          description: |
            Your own idempotency identifier for this purchase (UUID). Replaying the same id with the same body returns the original 200 at no charge with `X-MW-Idempotent-Replay: 1`; the same id with a different body returns 409 `idempotency_mismatch` (rejected before any charge, so no credit moves).
    ViewcastMeasurementResult:
      type: object
      required:
        - measurement_id
        - asset_id
        - kind
        - status
        - queued_at
      properties:
        measurement_id:
          type: string
          nullable: true
          description: |
            Identifier of this measurement run (UUID). Null only when you poll a resolved asset before its first measurement run has been queued.
        asset_id:
          type: string
          description: The asset the run belongs to.
        kind:
          type: string
          enum:
            - draft
            - final
            - direct
          description: 'Which purchase produced the run: `draft`, `final` or `direct`.'
        status:
          type: string
          enum:
            - queued
            - running
            - completed
            - failed
          description: '`queued`, `running`, `completed` or `failed`.'
        queued_at:
          type: string
          format: date-time
          description: When the run was queued (ISO 8601).
        completed_at:
          type: string
          format: date-time
          description: When the run finished (ISO 8601). Absent while the run is queued or running.
        refund_credits:
          type: integer
          minimum: 1
          description: Credits refunded automatically when a run fails. Always positive when present.
        failure_reason:
          type: string
          description: Why the run failed, when it did.
    ViewcastRelatedParty:
      type: object
      additionalProperties: false
      description: |
        A party related to the display, as returned on read. The Geopath
        `registry` entry carries the frame id as its `party_display_id` (a
        STRING) — this is the house identifier format shared with the search
        response. `source_id` and `observed_at` are server-stamped.
      required:
        - party_name
        - role
        - source_id
        - observed_at
      properties:
        party_name:
          type: string
          description: The organization (e.g. "Geopath").
        division:
          type: string
          nullable: true
          description: Sub-entity.
        role:
          type: string
          enum:
            - owner
            - operator
            - manager
            - reseller
            - agency
            - platform
            - venue
            - registry
            - measurement_provider
          description: The party's role on this display.
        party_display_id:
          type: string
          description: The party's own identifier for this display. On the Geopath `registry` entry this is the frame id.
        other_ids:
          type: object
          additionalProperties:
            type: string
        media_name:
          type: string
          nullable: true
        status_type:
          type: string
          nullable: true
        source_id:
          type: string
          description: Server-stamped source that asserted this entry.
        observed_at:
          type: string
          description: Server-stamped observation timestamp.
    ViewcastProfileDetail:
      type: object
      x-motionworks-status: production
      x-motionworks-source: viewcast-profiles
      x-motionworks-source-doc: https://docs.mworks.com/docs/viewcast-profiles
      description: |
        The full measurement profile of a display your organization owns: the nested face/spot/reach measurement blocks, market, classification, measurement period and the display's single location (`anchor`, shared with every asset endpoint), merged with the identity, lifecycle state, publication and the 12-month subscription window of the owned final_measured asset, plus the day-of-week/demographics distribution (percent_circ), historical metrics (history), assignment records and the industry-reported Geopath comparison block. Frame/spot identifiers are reported under `related_parties[]` (Geopath `registry` `party_display_id`), never as bare top-level integers.
      required:
        - market
        - construction_type
        - classification
        - media_type
        - digital
        - period_start
        - period_end
        - period_days
        - face
        - spot
        - reach
        - display_id
        - state
        - publication
        - anchor
        - subscription_started_at
        - subscription_expires_at
        - created_at
        - updated_at
      properties:
        place_id:
          type: integer
          description: |
            Place identifier, when the face belongs to a place. Omitted for roadside faces (no place) and until the data service reports it.
        related_parties:
          type: array
          description: |
            Party-scoped identifiers for this display, in the SAME shape as the search response. The Geopath frame id is carried as the `registry` party's `party_display_id` (a STRING), not as a bare top-level integer. Frame/spot ids are reported here, never as `spot_id`/`geopath_spot_id`/`geopath_frame_id`.
          items:
            $ref: '#/components/schemas/ViewcastRelatedParty'
        market:
          type: object
          required:
            - co_geoid
            - co_name
            - st_name
            - dma_geoid
            - dma_name
          properties:
            co_geoid:
              type: string
            co_name:
              type: string
            st_name:
              type: string
            cbsa_geoid:
              type: string
            cbsa_name:
              type: string
            cbsa_type:
              type: string
            dma_geoid:
              type: string
            dma_name:
              type: string
            postal_code:
              type: string
        construction_type:
          type: string
          description: Construction type of the display.
        classification:
          type: string
          enum:
            - Roadside
            - Place-Based
          description: |
            The display's classification (`Roadside` or `Place-Based`). The single classification field, matching the asset format (`GET /v2/displays/assets*`); the profile does not serve a separate `classification_type`.
        media_type:
          type: string
          description: Size-based display classification (Display, Panel, Junior Poster, Poster, Bulletin, Murals).
        digital:
          type: boolean
          description: Whether the display is digital.
        face_orientation:
          type: number
          description: Face orientation in degrees (0 = north).
        face_height:
          type: number
          description: Face height in inches.
        face_width:
          type: number
          description: Face width in inches.
        viewable_distance:
          type: number
          description: Viewable distance in feet.
        face_audit_status:
          type: integer
          description: Audit status code for the face measurement.
        period_start:
          type: string
          format: date
          description: Measurement period start (ISO 8601 date).
        period_end:
          type: string
          format: date
          description: Measurement period end (ISO 8601 date).
        period_days:
          type: integer
          description: Number of days in the measurement period.
        face:
          type: object
          description: Face-level measurements (aggregated across all spots on the face).
          required:
            - traffic
            - face_circ
            - face_ots
            - dwell
          properties:
            traffic:
              type: integer
              description: Average weekly traffic (vehicles + pedestrians) in the viewshed.
            face_circ:
              type: integer
              description: Average weekly face circulation.
            face_ots:
              type: integer
              description: Average weekly face Opportunity to See impressions.
            face_lts:
              type: integer
              description: |
                Average weekly face Likelihood to See impressions. Optional: omitted until the data service reports a measured face_lts.
            face_share_of_voice:
              type: number
              description: Face-level share of voice (0–1).
            dwell:
              type: number
              description: Circulation-weighted dwell time in minutes.
        spot:
          type: object
          description: Spot-level measurements (weighted by share_of_voice and spot_length for digital displays).
          required:
            - spot_circ
            - spot_ots
            - share_of_voice
          properties:
            spot_circ:
              type: integer
              description: Average weekly spot circulation.
            spot_ots:
              type: integer
              description: Average weekly spot OTS impressions.
            spot_lts:
              type: integer
              description: |
                Average weekly spot LTS impressions. Optional: the measured value is pending upstream; the industry-reported Geopath value stays in the `geopath` block and is never promoted here.
            share_of_voice:
              type: number
              description: Spot-level share of voice (0–1).
            spot_length:
              type: integer
              description: Duration of spot in seconds (digital displays only; omitted when null).
        reach:
          type: object
          description: Reach metrics (total = US; market = DMA; eff1 = exposed 1+; eff3 = exposed 3+).
          required:
            - total_population
            - market_population
            - total_reach_eff1
            - market_reach_eff1
            - total_reach_eff3
            - market_reach_eff3
          properties:
            total_population:
              type: integer
            market_population:
              type: integer
            total_reach_eff1:
              type: integer
            market_reach_eff1:
              type: integer
            total_reach_eff3:
              type: integer
            market_reach_eff3:
              type: integer
        assignment_status:
          type: string
          enum:
            - Industry Assigned
            - System Assigned
            - Motionworks Assigned
            - Unverified Assignments
          description: Assignment status classification, when reported.
        display_id:
          type: string
          nullable: true
          description: The requested display id (canonical, or the copy's source display).
        state:
          type: string
          enum:
            - draft
            - draft_measured
            - final_measured
          description: Lifecycle state of the asset (`final_measured` for a published display).
        publication:
          type: string
          enum:
            - private
            - public
          description: '`private` or `public`.'
        anchor:
          $ref: '#/components/schemas/ViewcastAssetAnchor'
        subscription_started_at:
          type: string
          format: date-time
          nullable: true
          description: Start of the 12-month measurement subscription (ISO 8601).
        subscription_expires_at:
          type: string
          format: date-time
          nullable: true
          description: |
            Expiry of the 12-month measurement subscription window. At expiry this route fails closed until repurchase.
        created_at:
          type: string
          format: date-time
          description: When the asset was created (ISO 8601).
        updated_at:
          type: string
          format: date-time
          description: When the asset last changed (ISO 8601).
        percent_circ:
          type: object
          description: Day-of-week/hour traffic distribution plus demographic breakdowns.
          properties:
            day:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  description:
                    type: string
                  value:
                    type: number
                  hourly:
                    type: array
                    items:
                      type: number
            segment:
              type: object
              description: Demographic segments (loosely typed; keys vary by segment).
              additionalProperties: true
        history:
          type: object
          description: Historical metrics by year and month.
          properties:
            year:
              type: array
              items:
                $ref: '#/components/schemas/ViewcastHistoryEntry'
            month:
              type: array
              items:
                $ref: '#/components/schemas/ViewcastHistoryEntry'
        assignments:
          type: array
          description: Assigned path/place traffic summaries (identity join keys only).
          items:
            type: object
            properties:
              id:
                type: string
              type:
                type: string
        geopath:
          type: object
          description: |
            Industry-reported Geopath metrics for direct comparison with the Motionworks-measured values above. Not a source for the top-level spot_lts.
          properties:
            face_id:
              type: integer
            spot_id:
              type: integer
            face_traffic:
              type: integer
            face_circ:
              type: integer
            spot_circ:
              type: integer
            spot_ots:
              type: integer
            spot_lts:
              type: integer
    ViewcastHistoryEntry:
      type: object
      description: One historical metrics row (year or month granularity).
      properties:
        id:
          type: string
        period_start:
          type: string
        traffic:
          type: integer
        face_circ:
          type: integer
        spot_circ:
          type: integer
        spot_ots:
          type: number
        dwell:
          type: number
        period_days:
          type: integer
        avg_daily_ped_traffic:
          type: integer
          nullable: true
        avg_daily_veh_traffic:
          type: integer
          nullable: true
    Meta:
      type: object
      properties:
        request_id:
          type: string
          description: Unique id for this request. Quote it when contacting support.
        credits_used:
          type: integer
          description: Credits charged for this call.
        credits_remaining:
          type: integer
          description: Credits remaining on your account after this call.
        product:
          type: string
          description: The Motionworks product that served the call.
        version:
          type: string
          description: API version that served the call.
        provenance:
          $ref: '#/components/schemas/Provenance'
paths:
  /viewcast/display-validation:
    post:
      operationId: viewcast_display_validation
      tags:
        - Display Validation
      summary: Validate a display face
      description: |
        Submit a display face — its anchor point, height and width in inches, its orientation, and two choices: whether Motionworks may adjust the orientation (`orientation_lock_type`) and which road counts as primary (`primary_road_rule`). The response carries the outcome, the orientation actually used, the primary road, every road segment in view with its sightline and whether a building blocks it, the viewshed polygon (what the face delivers) and the search polygon (every aim that was allowed).

        **5 credits per call.** Synchronous; a call usually takes 20–45 seconds. Identical inputs are served from a 24-hour cache; cache hits are still charged. An outcome other than `validated` — `no_coverage`, `insufficient_data` or `rejected` — is still a successful 200: the validation ran, and the outcome is data rather than an HTTP error. `error_message` carries the reason when one is available.
      x-credit-cost: 1
      x-motionworks-status: production
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisplayValidationRequest'
            examples:
              roadside_billboard:
                summary: Roadside billboard validation (Phoenix)
                value:
                  lat: 33.4484
                  lon: -112.074
                  face_height_in: 14
                  face_width_in: 48
                  orientation: 180
                  orientation_lock_type: no_move
                  primary_road_rule: veh_metres
      responses:
        '200':
          description: Validation result (any outcome, including rejected).
          headers:
            X-Request-Id:
              schema:
                type: string
            X-MW-Credits-Used:
              schema:
                type: string
            X-MW-Worker:
              schema:
                type: string
                enum:
                  - viewcast
            X-MW-Cache-Status:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
            Cache-Control:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisplayValidationResponse'
        '400':
          description: Invalid request body or schema validation failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Missing or invalid credentials, or a credential that is not org-scoped.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '402':
          description: Insufficient credits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '413':
          description: |
            The requested face dimensions produce a viewshed too large to return. Reduce the face height or width and retry. Deterministic for these inputs: retrying the same request will not succeed. A failed call is refunded automatically.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: |
            The validation service is temporarily unavailable. Retry later. A failed call is refunded automatically.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /viewcast/on-demand/{asset_id}:
    get:
      operationId: viewcast_on_demand_detail
      tags:
        - On-Demand
      summary: Get asset + measurement detail
      description: |
        Returns the asset definition and the resolved measurement detail — display attributes and the audience block — for one asset your organization owns, by `asset_id`. 1 credit per call. Unknown assets, assets your organization does not own, and assets with no finished measurement yet all return 404 (never 403). Responses are never cached (`Cache-Control: private, no-store`).
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: asset_id
          in: path
          required: true
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Asset + measurement detail.
          headers:
            X-Request-Id:
              schema:
                type: string
            X-MW-Credits-Used:
              schema:
                type: string
            X-MW-Worker:
              schema:
                type: string
                enum:
                  - viewcast
            Cache-Control:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ViewcastOnDemandResponse'
        '401':
          description: Missing or invalid credentials, or a credential that is not org-scoped.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: |
            Unknown `asset_id`, not owned by your organization, or no finished measurement yet — the three are indistinguishable by design.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /viewcast/measurements/purchase/draft:
    post:
      operationId: viewcast_measurements_purchase_draft
      tags:
        - Measurements
      summary: Buy a draft measurement (202, then poll)
      description: |
        Queues a draft measurement for an asset you own in state `draft`. Replaying the same `measurement_request_id` after an interrupted purchase is allowed; any other second draft on a `draft_measured` asset returns 409 — re-measuring is a refresh, not a second purchase. Asynchronous: the response is 202 and you poll `GET /v2/viewcast/measurements/{asset_id}` for the result. A draft measurement completes within one hour; if it cannot, the purchase is refunded automatically, as it is for any measurement that fails.

        **Cost:** 1 credit base plus a 150-credit draft purchase when the asset is in state `draft`.

        **Idempotency:** the body carries a `measurement_request_id` you mint (UUID). Replaying the same id with the same body returns the original queued result without a second charge, provided your account can still cover the charge at replay time — a replay against an empty wallet returns 402 and nothing is charged. The legacy `Idempotency-Key` header is not accepted and returns 400.
      x-credit-cost: 1
      x-motionworks-status: production
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ViewcastMeasurementOwnedPurchaseRequest'
      responses:
        '202':
          description: |
            Measurement queued. The body is the same measurement result the poll endpoint returns — there is no separate acceptance schema.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ViewcastMeasurementResult'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient credits for the purchase.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Unknown `asset_id`, or not owned by your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            A measurement is already running on this asset, or a draft purchase is not allowed from the asset's current state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /viewcast/measurements/purchase/final:
    post:
      operationId: viewcast_measurements_purchase_final
      tags:
        - Measurements
      summary: 'Buy the final measurement (STUB: 501 until wired)'
      description: |
        **Not yet available.** The final measurement is stubbed until the measurement provider's viewshed function is wired; every call returns `501 NOT_YET_AVAILABLE` with no charge and no state change. When wired, it will queue the final measurement for an asset you own (202, then poll `GET /v2/viewcast/measurements/{asset_id}`), completing in the daily completion run, with a failed measurement refunding automatically.

        **Cost:** when wired, 1 credit base plus a 300-credit final purchase starting the asset's 12-month measurement subscription. The stub charges nothing.

        **Idempotency:** the same `measurement_request_id` (UUID) contract as the draft purchase — replaying the same id with the same body returns the original result without a second charge.
      x-credit-cost: 1
      x-motionworks-status: production
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ViewcastMeasurementOwnedPurchaseRequest'
      responses:
        '202':
          description: |
            Measurement queued. The body is the same measurement result the poll endpoint returns — there is no separate acceptance schema.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ViewcastMeasurementResult'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient credits for the purchase.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Unknown `asset_id`, or not owned by your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Not purchasable from the asset's current state, an idempotency conflict on `measurement_request_id`, or a concurrent purchase on the same asset. Not reachable while the final purchase is stubbed; documented here so the contract is complete when the stub lifts. No charge.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '501':
          description: 'Not yet available: the final measurement is stubbed until the measurement provider''s viewshed function is wired. No charge, no state change.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /viewcast/measurements/purchase/direct:
    post:
      operationId: viewcast_measurements_purchase_direct
      tags:
        - Measurements
      summary: Buy a measurement directly from a display_id (synchronous)
      description: |
        Buy a final measurement straight from a `display_id` you found in Display Search, without creating a draft asset first. Synchronous: the response is 200 with the newly created `final_measured` asset, locked down like any asset that finished the normal lifecycle (refine and archive return 409; classification is immutable).

        You supply only the `display_id`. The asset definition (classification, anchor, display descriptor) is sourced from the Motionworks display catalog at purchase time. A `display_id` the catalog does not return 404s before anything is charged. Licensing is per organization per subscription window: your organization may hold at most one active license for a `display_id`; a re-attempt inside the 12-month window returns 409 with an already-licensed message (a concurrent duplicate is refunded automatically), and purchasing again after the window lapses is allowed. Other organizations may license the same display independently.

        **Cost:** 1 credit base plus a 300-credit purchase, which starts the asset's 12-month measurement subscription from the purchase date.

        **Idempotency:** replays ride `measurement_request_id` (UUID). The same id and body return the original 200 at no charge with `X-MW-Idempotent-Replay: 1`; the same id with a different body returns 409 `idempotency_mismatch` (rejected before any charge, so no credit moves).
      x-credit-cost: 1
      x-motionworks-status: production
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ViewcastMeasurementDirectPurchaseRequest'
      responses:
        '200':
          description: |
            Measurement purchased. The new `final_measured` asset is returned in the measurement-result envelope (kind `direct`, status `completed`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ViewcastMeasurementResult'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '400':
          description: |
            Invalid request: missing or empty `display_id`, malformed `measurement_request_id`, a retired caller-supplied field (`classification`, `anchor`, `related_parties`, `display`, `user_reference`), or unknown body keys.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient credits for the purchase.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            The `display_id` is not in the Motionworks display catalog and cannot be licensed. Returned before any charge.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            Your organization already holds an active license for this `display_id` (a concurrent duplicate purchase is refunded automatically); or `idempotency_mismatch` — same `measurement_request_id` with a different body (no charge).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            The purchase could not be completed. No credits were charged if the display catalog was unreachable or returned unusable data. If the failure happened after the charge, the charge was reversed automatically.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /viewcast/measurements/{asset_id}:
    get:
      operationId: viewcast_measurements_latest
      tags:
        - Measurements
      summary: Poll the latest measurement run
      description: |
        Returns the most recent measurement run for the asset — its status and, if it failed, the credits refunded. Poll this after a 202 from a purchase. Readiness polling is free (0 credits; you pay only for the measurement itself, never for the poll). For a final measurement, `completed` means the daily completion run succeeded; the new `display_id` then appears on the asset (`GET /v2/displays/assets/{asset_id}`), not on this result.
      x-credit-cost: 0
      x-motionworks-status: production
      parameters:
        - name: asset_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The most recent measurement run for the asset.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ViewcastMeasurementResult'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '404':
          description: Unknown `asset_id`, or not owned by your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /viewcast/profiles/{display_id}:
    get:
      operationId: viewcast_profiles_get
      tags:
        - Profiles
      summary: Read a viewshed profile
      description: |
        Returns the viewshed profile of a `final_measured` display by its Motionworks `display_id` — the identifier assigned when the asset reached `final_measured` (not a Placecast `place_id`, and not a face id).

        This is a licensed read: your organization must already own a measurement for the display. Purchase one under `POST /v2/viewcast/measurements/purchase/{type}`; this endpoint sells nothing. Unowned callers receive 403 `FEATURE_NOT_LICENSED` whether or not the id exists. Owned callers pay the 1-credit base cost.
      x-credit-cost: 1
      x-motionworks-status: production
      x-motionworks-source-doc: https://docs.mworks.com/docs/viewcast-profiles
      parameters:
        - name: display_id
          in: path
          required: true
          schema:
            type: string
          description: Motionworks display identifier (`dsp…`), assigned when the asset reached `final_measured`.
      responses:
        '200':
          description: The profile view.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ViewcastProfileDetail'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '403':
          description: |
            Your organization owns no measurement for this display (`FEATURE_NOT_LICENSED`). Returned for known and unknown ids alike, so the answer reveals nothing about whether the id exists. No charge. Purchase a measurement under `POST /v2/viewcast/measurements/purchase/{type}`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Your organization owns the display, but its profile data is not yet available. No charge.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: The profile data service is temporarily unavailable. No charge.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
x-tagGroups:
  - name: Display validation
    tags:
      - Display Validation
  - name: Measurements
    tags:
      - Measurements
      - On-Demand
  - name: Profiles
    tags:
      - Profiles
x-customer-voice: true
