openapi: 3.1.0
info:
  title: Motionworks API - Popcast Reporting
  version: 0.1.0
  description: |
    Popcast Reporting will show what local news is saying about a US Census block group: a rolling window of headlines from local newspapers, TV stations, community publications and reference-site attention, with the topics and sentiment they carry.

    **Coming soon.** This capability is on the Motionworks roadmap and is not yet available to call. The request and response shapes are published so you can plan an integration; they may change before launch.
  x-internal-notes: |
    Reporting layer of The Conscience Stack (TF-99). Planned feeds: local newspaper RSS, TV station RSS, Patch, Substack local publications, Wikipedia pageview surges, GNews (credential-gated). Data maturity starts at synthetic-only and promotes to production after 7 days of ingest against the feed registry. Envelope + provenance per ADR-003.
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com

servers:
  - url: https://api2.mworks.com
    description: Production

tags:
  - name: reporting
    description: "Local news headlines, topics and sentiment for a block group over a rolling window. Coming soon."
    x-displayName: Reporting

paths:
  /v2/popcast/reporting/{fips}:
    get:
      operationId: getReportingSignal
      summary: Read the current reporting signal
      description: |
        Will return the headlines about one block group over a rolling 7-, 14- or 30-day window, with topics, a sentiment split and a count by source. Planned cost: 1 credit per call. Coming soon — not yet available to call.
      tags: [reporting]
      x-motionworks-status: roadmap
      x-motionworks-roadmap-issue: "TF-99"
      x-motionworks-data-maturity: synthetic-only
      x-credit-cost: 1
      parameters:
        - $ref: '#/components/parameters/FipsParam'
        - name: window
          in: query
          schema: { type: integer, enum: [7, 14, 30], default: 7 }
          description: "Rolling window in days (7, 14 or 30)."
      responses:
        '200':
          description: Reporting signal
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportingSignalResponse' }
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          $ref: '#/components/responses/ValidationError'

  /v2/popcast/reporting/{fips}/history:
    get:
      operationId: getReportingHistory
      summary: Read reporting history
      description: |
        Will return up to 26 weekly snapshots of headline volume, topics and sentiment for one block group between `from` and `to`, with cursor pagination. Planned cost: 3 credits per call. Coming soon — not yet available to call.
      tags: [reporting]
      x-motionworks-status: roadmap
      x-motionworks-roadmap-issue: "TF-99"
      x-motionworks-data-maturity: synthetic-only
      x-credit-cost: 3
      parameters:
        - $ref: '#/components/parameters/FipsParam'
        - { name: from, in: query, schema: { type: string, format: date }, description: "Earliest week to include (`YYYY-MM-DD`)." }
        - { name: to, in: query, schema: { type: string, format: date }, description: "Latest week to include (`YYYY-MM-DD`)." }
        - { name: cursor, in: query, schema: { type: string }, description: "Opaque cursor from the previous page." }
        - { name: limit, in: query, schema: { type: integer, maximum: 26 }, description: "Results per page." }
      responses:
        '200':
          description: History snapshots
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportingHistoryResponse' }

  /v2/popcast/reporting/batch:
    post:
      operationId: batchReportingSignals
      summary: Read reporting signals for many block groups
      description: |
        Will return the current reporting signal for up to 100 block groups in one call. Planned cost: 100 credits per call (1 per block group). Coming soon — not yet available to call.
      tags: [reporting]
      x-motionworks-status: roadmap
      x-motionworks-roadmap-issue: "TF-99"
      x-motionworks-data-maturity: synthetic-only
      x-credit-cost: 100
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportingBatchRequest' }
      responses:
        '200':
          description: Batch results
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportingBatchResponse' }

components:
  parameters:
    FipsParam:
      name: fips
      in: path
      required: true
      schema: { type: string, pattern: '^\d{12}$' }
      description: 12-digit US Census block group GEOID

  schemas:
    Headline:
      type: object
      required: [id, title, source_type, source_name, published_at]
      properties:
        id: { type: string, description: "Unique id of the record." }
        title: { type: string, description: "The headline text." }
        source_type:
          type: string
          enum: [news-api, patch, tv-rss, substack, local-gov, wikipedia, newspaper-rss]
          description: Kind of outlet the headline came from.
        source_name: { type: string, description: "Name of the outlet." }
        url: { type: string, format: uri, description: "Link to the story." }
        published_at: { type: string, format: date-time, description: "When the story was published (ISO 8601)." }
        sentiment: { type: string, enum: [positive, neutral, negative], description: "Sentiment of the headline: `positive`, `neutral` or `negative`." }
        topics: { type: array, items: { type: string }, description: "Topics mentioned, as short labels." }
      description: One headline.
    ReportingPayload:
      type: object
      properties:
        headlines: { type: array, items: { $ref: '#/components/schemas/Headline' }, description: "Headlines in the window, newest first." }
        topics: { type: array, items: { type: string }, description: "Topics mentioned, as short labels." }
        sentiment:
          type: object
          properties:
            positive_pct: { type: number, description: "Share of items with positive sentiment, as a percentage." }
            neutral_pct: { type: number, description: "Share of items with neutral sentiment, as a percentage." }
            negative_pct: { type: number, description: "Share of items with negative sentiment, as a percentage." }
          description: Sentiment split across the headlines.
        sources:
          type: object
          additionalProperties: { type: integer }
          description: Headline counts by outlet.
      description: The reporting signal contents.
    ReportingSignal:
      type: object
      required: [fips, signal_layer, period_end, window_days, refreshed_at, data_quality, source_breakdown, payload]
      properties:
        fips: { type: string, description: "12-digit US Census block group FIPS code." }
        signal_layer: { type: string, enum: [reporting], description: "Which signal layer this record belongs to." }
        period_end: { type: string, description: "Last day of the period this record covers (`YYYY-MM-DD`)." }
        window_days: { type: integer, enum: [7, 14, 30], description: "Length of the rolling window, in days." }
        refreshed_at: { type: string, format: date-time, description: "When the signal was last refreshed (ISO 8601)." }
        data_quality: { type: string, enum: [high, medium, low], description: "Confidence in the signal: `high`, `medium` or `low`." }
        source_breakdown:
          type: object
          additionalProperties: { type: integer }
          description: "How many records each contributing source supplied, keyed by source."
        payload: { $ref: '#/components/schemas/ReportingPayload' }
      description: The current reporting signal for one block group.
    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' }
      description: Request metadata.
    Provenance:
      type: object
      # Invariant F2 — is_focused <=> measurement_method (ADR-033 §2.3).
      # JSON Schema 2020-12 conditional (OpenAPI 3.1 is a 2020-12 dialect).
      # Both fields stay OPTIONAL and independently omittable: the `if`
      # only fires when BOTH are present, so an envelope carrying only
      # `is_focused`, only `measurement_method`, or neither still validates
      # — which keeps every non-Placecast product's existing envelope valid.
      # Only the disagreeing combination is unrepresentable. Mirrors the
      # .refine() guard on ProvenanceSchema in
      # packages/types/src/schemas/provenance.ts.
      if:
        required: [is_focused, measurement_method]
      then:
        oneOf:
          - properties:
              is_focused:
                const: true
              measurement_method:
                const: direct
          - properties:
              is_focused:
                const: false
              measurement_method:
                const: estimated
      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, description: "The date the underlying data represents." }
        data_freshness: { type: string, description: "How often the underlying data is refreshed (for example `daily`, `monthly`, `annually`)." }
        data_latency_days: { type: integer, description: "Typical days between an observation and its availability in the API." }
        data_maturity: { type: string, description: "Maturity of the data behind the result: `production` (supported and stable), `research-preview` (available, subject to change), or a pre-release value (`roadmap`, or generated placeholder data not yet backed by production measurement)." }
        is_focused:
          type: boolean
          description: |
            Whether the place is directly measured (`true`) rather than estimated from its peer group (`false`). When present alongside `measurement_method`, the two agree.
          x-internal-notes: |
            Mirrors ProvenanceSchema.is_focused in packages/types/src/schemas/provenance.ts; paired with measurement_method by invariant F2 (ADR-033 §2.3).
        measurement_method:
          type: string
          enum: [direct, estimated]
          description: |
            `direct` when the place is measured directly; `estimated` when its statistics come from peer-group inference. When present alongside `is_focused`, the two agree (`true` ↔ `direct`).
      description: Where the data came from and how mature it is.
    ReportingSignalResponse:
      type: object
      properties:
        data: { $ref: '#/components/schemas/ReportingSignal' }
        meta: { $ref: '#/components/schemas/Meta' }
      description: One reporting signal with request metadata.
    ReportingHistorySnapshot:
      type: object
      properties:
        fips: { type: string, description: "12-digit US Census block group FIPS code." }
        period_start: { type: string, description: "First day of the period this record covers (`YYYY-MM-DD`)." }
        period_end: { type: string, description: "Last day of the period this record covers (`YYYY-MM-DD`)." }
        window_days: { type: integer, description: "Length of the rolling window, in days." }
        headline_count: { type: integer, description: "Headlines published in the period." }
        topics: { type: array, items: { type: string }, description: "Topics mentioned, as short labels." }
        sentiment:
          type: object
          properties:
            positive_pct: { type: number, description: "Share of items with positive sentiment, as a percentage." }
            neutral_pct: { type: number, description: "Share of items with neutral sentiment, as a percentage." }
            negative_pct: { type: number, description: "Share of items with negative sentiment, as a percentage." }
          description: Sentiment split across those headlines.
        data_quality: { type: string, enum: [high, medium, low], description: "Confidence in the signal: `high`, `medium` or `low`." }
      description: Headline activity for one week.
    ReportingHistoryResponse:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/ReportingHistorySnapshot' }
          description: The list of results.
        pagination:
          type: object
          properties:
            cursor: { type: string, nullable: true, description: "Opaque cursor for the next page. Pass it back as `cursor` to continue; absent on the last page." }
            has_more: { type: boolean, description: "Whether another page follows." }
            total: { type: integer, description: "Total number of items in the window." }
          description: Paging information for the list.
        meta: { $ref: '#/components/schemas/Meta' }
      description: A page of weekly snapshots with request metadata.
    ReportingBatchRequest:
      type: object
      required: [fips_codes]
      properties:
        fips_codes:
          type: array
          maxItems: 100
          items: { type: string, pattern: '^\d{12}$' }
          description: Block group FIPS codes to look up (12 digits each).
        window: { type: integer, enum: [7, 14, 30], description: "Rolling window in days (7, 14 or 30)." }
      description: Block groups to look up and the window to use.
    ReportingBatchResponse:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/ReportingSignal' }
          description: The list of results.
        meta: { $ref: '#/components/schemas/Meta' }
      description: "One reporting signal per requested block group, with request metadata."
    ApiError:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, description: "Stable machine-readable error code." }
            message: { type: string, description: "Human-readable explanation of the error." }
            status: { type: integer, description: "HTTP status code, repeated in the body." }
            request_id: { type: string, description: "Unique id for this request. Quote it when contacting support." }
          description: "The error: a stable `code`, a human-readable `message`, and the HTTP `status`."
      description: The standard error envelope.
  responses:
    NotFound:
      description: No data for block group
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
x-tagGroups:
  - name: Popcast Reporting
    tags:
      - reporting
x-customer-voice: true
