openapi: 3.1.0
info:
  title: Popcast
  version: 2.0.0
  description: |
    The Popcast API is Motionworks' population intelligence family. It answers two questions: where does an audience live, and where do they spend time?

    - **Segments** — define a population segment from demographic filters (age, gender, race and ethnicity, language, household size, children, income, vehicles, PRIZM and consumer segments). Every filter is optional and filters compose as AND. A segment returns a `motionworks_segment_id` you reuse in At Home queries and Set Dynamics cohorts. The Segments endpoints are being enabled and currently return 501 `NOT_YET_AVAILABLE`; when live, 1 credit per call.
    - **Popcast At Home** — count the persons and households in a segment by primary residence, for one geography or across every geography of a type (block group, ZIP Code Tabulation Area, county, metro, DMA, state). Annual vintages. 2 credits per page.
    - **Popcast Anytime Maptiles** — vector tiles of hourly occupancy (how many people are present in a Census block group or ZIP Code Tabulation Area at each hour of the day, by day type) for each published segment. Mint a grant, hand its TileJSON URL to MapLibre or Mapbox GL JS, and render. Grant management is free; tile traffic is metered at 1 credit per 1,000 tiles.

    Authenticate with an org-scoped API key (`X-API-Key: mw_…`) or a signed-in session. The tile catalog is public. Prose guides: [Popcast At Home](https://www.mworks.com/docs/popcast-at-home/), [Popcast Anytime](https://www.mworks.com/docs/popcast-anytime/) and [Popcast Anytime Maptiles](https://www.mworks.com/docs/popcast/popcast-anytime-maptiles/).
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com
servers:
  - url: https://api2.mworks.com/v2
    description: Production
tags:
  - name: Popcast
    description: |
      Define population segments and count where they live. At Home counts cost 2 credits per page. The Segments endpoints are being enabled and currently return 501 `NOT_YET_AVAILABLE`; when live they cost 1 credit per call.
  - name: Tile Discovery
    description: |
      The public tile catalog: every Popcast Anytime tileset, with its segment, coverage window, geography family, zoom range and field schema. No authentication, no charge.
  - name: Tile Grants
    description: |
      Mint, list, read and revoke tile-delivery grants. A grant belongs to your organization and names the web origins allowed to fetch tiles. Free.
  - name: Tile TileJSON
    description: |
      The TileJSON manifest for a grant — the URL you hand to your map library. The manifest embeds a short-lived tile token in its tile URLs, so your map library needs no auth header.
  - name: Tile Data
    description: |
      The tile bytes themselves (Mapbox Vector Tile, binary). Your map library requests these with the token from the TileJSON manifest; you will not normally call this endpoint yourself.
security:
  - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        A signed-in user session (`Authorization: Bearer <jwt>`), as used by the Motionworks console and account flows to manage tile grants for your organization.
    tileTokenAuth:
      type: apiKey
      in: query
      name: token
      description: |
        Opaque per-grant tile-token (24-hour TTL) minted by `GET /v2/popcast/tiles/anytime/grants/{id}/tilejson`. Accepted ONLY as the `?token=` query parameter — header form is rejected. Treat as a short-lived bearer credential.
  schemas:
    PopcastSegmentDefinition:
      type: object
      x-motionworks-status: production
      x-motionworks-source: popcast-cohort-search
      x-motionworks-source-doc: https://docs.mworks.com/docs/ref-segments
      required:
        - customer_segment_id
        - customer_segment_name
      description: |
        A population segment defined from demographic filters. Every filter is optional (omit = no restriction) and filters compose as AND.
      properties:
        customer_segment_id:
          type: string
          description: Your own identifier for the segment, echoed back on every row.
        customer_segment_name:
          type: string
          description: Your own display name for the segment.
        age_min:
          type: integer
          minimum: 0
          maximum: 100
          description: Minimum age in years, inclusive.
        age_max:
          type: integer
          minimum: 0
          maximum: 100
          description: Maximum age in years, inclusive.
        gender:
          type: string
          enum:
            - f
            - m
          description: '`f` or `m`. Omit for all genders.'
        races:
          type: array
          items:
            type: string
            enum:
              - white
              - black
              - native
              - asian
              - islander
              - other
              - multiple
          description: Race codes to include (any match).
        hispanic:
          type: boolean
          description: Restrict to Hispanic or Latino persons (`true`) or to persons who are not (`false`).
        worker:
          type: boolean
          description: Restrict to persons in the workforce (`true`) or not (`false`).
        language:
          type: array
          items:
            type: string
            enum:
              - english
              - spanish
              - other
          description: Language-spoken-at-home codes to include (any match).
        household_size_min:
          type: integer
          minimum: 1
          maximum: 20
          description: Minimum household size, inclusive.
        household_size_max:
          type: integer
          minimum: 1
          maximum: 20
          description: Maximum household size, inclusive.
        household_children:
          type: boolean
          description: Restrict to households with children (`true`) or without (`false`).
        household_income_min:
          type: integer
          description: Minimum household income in USD, inflation-adjusted to the vintage year, inclusive. May be negative.
        household_income_max:
          type: integer
          description: Maximum household income in USD, inflation-adjusted to the vintage year, inclusive.
        household_vehicles_min:
          type: integer
          minimum: 0
          maximum: 5
          description: Minimum vehicles available to the household, inclusive.
        household_vehicles_max:
          type: integer
          minimum: 0
          maximum: 5
          description: Maximum vehicles available to the household, inclusive.
        prizm_segments:
          type: array
          items:
            type: integer
          description: Claritas PRIZM Premier segment codes to include (any match).
        consumer_segments:
          type: array
          items:
            type: string
          description: Consumer segment codes to include (any match).
        year:
          type: integer
          description: Population vintage year the segment is evaluated against.
    PopcastSegment:
      allOf:
        - $ref: '#/components/schemas/PopcastSegmentDefinition'
        - type: object
          properties:
            motionworks_segment_id:
              type: string
              format: uuid
            customer_id:
              type: integer
            customer_name:
              type: string
            vintage:
              type: string
    PopcastAtHome:
      type: object
      x-motionworks-status: production
      x-motionworks-source: popcast-at-home
      x-motionworks-source-doc: https://docs.mworks.com/docs/popcast-at-home
      properties:
        motionworks_segment_id:
          type: string
          description: The Motionworks identifier of the segment counted.
        customer_segment_id:
          type: string
          description: Your own identifier for the segment, as supplied when it was defined.
        customer_segment_name:
          type: string
          description: Your own display name for the segment.
        year:
          type: integer
          description: Population vintage year of the counts.
        geography_id:
          type: string
          description: |
            Geography identifier, for example `US2010XXBG360610031001` (block group), `US2020STCO36061` (county) or `US2020XDMA532` (DMA).
        persons:
          type: integer
          description: Persons in the segment who live in this geography.
        households:
          type:
            - integer
            - 'null'
          description: |
            Households with at least one person matching the segment in this geography. Currently null — a household-level breakdown is planned.
        all_persons:
          type: integer
          description: Total persons in the geography, regardless of segment.
        all_households:
          type:
            - integer
            - 'null'
          description: |
            Total households in the geography, regardless of segment. Currently null — a household-level breakdown is planned.
        hh_persons:
          type:
            - integer
            - 'null'
          description: Segment persons living in households (not group quarters). Currently null.
        hh_households:
          type:
            - integer
            - 'null'
          description: Segment households. Currently null.
        gq_persons:
          type:
            - integer
            - 'null'
          description: |
            Segment persons living in non-institutional group quarters (college dormitories, military barracks and similar). Currently null.
        gq_households:
          type:
            - integer
            - 'null'
          description: |
            Segment households in non-institutional group quarters — each group-quarters resident counts as one household, so this equals `gq_persons`. Currently null.
        vintage:
          type: string
          description: Data vintage label for the counts.
      description: |
        One geography's At Home counts for a segment: how many persons and households in the segment live there, with the geography's totals for context.
    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 matching results, when the endpoint reports one.
    Provenance:
      type: object
      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
          format: date
          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
          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).
        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.
        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`).
    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'
    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
            context:
              type: object
              description: |
                Per-error structured fields. Where several sub-causes share a status, `context.reason` names the specific one (see each operation's error list).
              additionalProperties: true
          description: 'The error: a stable `code`, a human-readable `message`, and optional `context` with the specifics.'
    TileMeta:
      type: object
      x-motionworks-status: production
      required:
        - request_id
        - credits_used
        - product
      properties:
        request_id:
          type: string
          description: Unique id for this request.
        credits_used:
          type: integer
          enum:
            - 0
          description: Always 0 — grant management is free; tile traffic is metered separately at 1 credit per 1,000 tiles.
        product:
          type: string
          enum:
            - popcast
          description: Always `popcast`.
    TilesetCatalogLayer:
      type: object
      x-motionworks-status: production
      description: TileJSON 3.0.0 `vector_layers[]` entry — one per MVT layer inside the tiles.
      required:
        - id
        - fields
      properties:
        id:
          type: string
          description: |
            Layer id inside the tile bytes (the MVT `source-layer`). Always `default` for Popcast Anytime tilesets — not the URL slug.
        description:
          type: string
          description: What the layer contains.
        minzoom:
          type: integer
          minimum: 0
          description: Lowest zoom level the layer serves.
        maxzoom:
          type: integer
          minimum: 0
          description: Highest zoom level the layer serves.
        fields:
          type: object
          description: Map of MVT feature property → TileJSON field type (`String` | `Number` | `Boolean`).
          additionalProperties:
            type: string
            enum:
              - String
              - Number
              - Boolean
    TilesetCatalogEntry:
      type: object
      x-motionworks-status: production
      required:
        - slug
        - title
        - minzoom
        - maxzoom
        - bounds
        - layers
        - grant_create_url
      properties:
        slug:
          type: string
          description: |
            Tileset slug. Matches the `{layer}` URL path segment used in `GET /v2/popcast/tiles/anytime/{layer}/{z}/{x}/{y}.mvt` and the values accepted in `TileGrantCreateRequest.tilesets`. Take slugs from the catalog (`GET /v2/popcast/tiles/anytime/tilesets`) rather than constructing them. They follow `tiles_anytime_<format>_<segment id>_<start yyyymm>_<end yyyymm>_<geography family>_v1`, where the geography family is `bg` (Census block groups) or `zcta` (ZIP Code Tabulation Areas).
        title:
          type: string
          description: Human-readable display title for this tileset.
        minzoom:
          type: integer
          minimum: 0
          description: Lowest zoom level the tileset serves.
        maxzoom:
          type: integer
          minimum: 0
          description: Highest zoom level the tileset serves.
        bounds:
          type: array
          description: TileJSON `bounds` — `[west, south, east, north]`, WGS84.
          items:
            type: number
          minItems: 4
          maxItems: 4
        layers:
          type: array
          items:
            $ref: '#/components/schemas/TilesetCatalogLayer'
          description: The MVT layers inside the tiles, with their field schemas.
        audkey:
          type: string
          description: |
            The segment's 16-character identifier (the `motionworks_segment_id` used in Popcast Anytime data). Also embedded in the slug.
        display_name:
          type: string
          description: Display name of the segment, suitable for a picker.
        yyyymm_start:
          type: string
          description: First month of the coverage window (`YYYYMM`).
        yyyymm_end:
          type: string
          description: Last month of the coverage window (`YYYYMM`).
        format_tag:
          type: string
          description: |
            Reserved; not returned today. Would carry the version tag of the tile field layout (for example `v2_3`), which is currently embedded in the slug.
        grains:
          type: array
          items:
            type: string
          description: |
            Reserved; not returned today. Would list the geography grains present in the tileset, switched by zoom: `ST` (state), `STCO` (county), `TRCT` (census tract) and `BG` (block group) for the block-group family; `ZCTA` replaces `TRCT`/`BG` in the ZIP Code family.
        day_types:
          type: array
          items:
            type: integer
          description: |
            Reserved; not returned today. Would list the day-type slots in each hourly field: `1` Mon–Thu, `2` Fri, `3` Sat, `4` Sun.
        hours:
          type: integer
          description: |
            Reserved; not returned today. Would give the number of hourly occupancy fields (24 = `occ_h00` … `occ_h23`).
        grant_create_url:
          type: string
          format: uri
          description: |
            The endpoint to POST to for a grant that covers this tileset (`POST /v2/popcast/tiles/anytime/grants`). Minting a grant and reading its TileJSON is the only path from this catalog to tiles.
        licensed:
          type: boolean
          description: |
            Whether your organization is licensed for this tileset. This public endpoint has no caller context, so the flag is currently always `false`; licensing is checked when you mint a grant.
    TilesetCatalogResponse:
      type: object
      x-motionworks-status: production
      required:
        - tilesets
        - metadata_stale
      properties:
        tilesets:
          type: array
          items:
            $ref: '#/components/schemas/TilesetCatalogEntry'
          description: |
            Every Popcast Anytime tileset currently published. Empty while the catalog is being refreshed (see `metadata_stale`).
        metadata_stale:
          type: boolean
          description: |
            `false` when the catalog is current. `true` when it could not be refreshed — the list is then empty; retry a few minutes later. Do not assume a fixed number of rows.
    TileBreakpointArray:
      type: array
      minItems: 5
      maxItems: 5
      items:
        type: number
      description: |
        Five strictly ascending breakpoint values for one geography grain (`ST`, `STCO`, `TRCT` or `BG`), ready to use as MapLibre `interpolate` stops.
    TileGrantCreateRequest:
      type: object
      x-motionworks-status: production
      required:
        - name
        - tilesets
        - allowed_origins
        - expires_at
      properties:
        name:
          type: string
          maxLength: 120
          description: Human-readable label for this grant (shown in the developer dashboard).
        tilesets:
          type: array
          minItems: 0
          maxItems: 12
          items:
            type: string
          description: |
            Tileset slugs to include, taken from the catalog (see `TilesetCatalogEntry.slug`). An empty array means every tileset currently published — the grant is stored with the expanded list, so tilesets published later need a new grant. A slug that is not published returns 403 `TILE_LAYER_NOT_LICENSED`.
        allowed_origins:
          type: array
          items:
            type: string
          description: |
            Browser `Origin` allowlist for the TileJSON and tile requests made with this grant. Requests must carry an `Origin` header that matches one entry. To test from curl or a native app, include the literal string `null` in `allowed_origins`. A request without an `Origin` header is otherwise refused (403 `ORIGIN_NOT_ALLOWED`, `error.context.reason: origin_required`). Wildcards are not supported — list each origin literally.
        expires_at:
          type: string
          format: date-time
          description: |
            ISO 8601 timestamp at which this grant expires. The tile-token JWT minted by `/tilejson` independently expires 24h after each mint — re-fetch TileJSON before then.
    TileGrant:
      type: object
      x-motionworks-status: production
      description: |
        A Popcast Anytime tile-delivery grant. The `grant_id` is a Crockford-base32 ULID and is itself a bearer credential — anyone who knows it can fetch the TileJSON capability URL from an allowed origin.
      properties:
        grant_id:
          type: string
          description: Opaque ULID. Treat as a secret.
        org_id:
          type: string
          format: uuid
          description: The organization that owns the grant.
        product:
          type: string
          enum:
            - popcast
          description: Always `popcast`.
        name:
          type: string
          description: The label you gave the grant.
        tilesets:
          type: array
          items:
            type: string
          description: Tileset slugs the grant covers.
        allowed_origins:
          type: array
          items:
            type: string
          description: Browser origins allowed to fetch TileJSON and tiles with this grant.
        expires_at:
          type: string
          format: date-time
          description: When the grant expires (ISO 8601).
        revoked_at:
          type: string
          format: date-time
          nullable: true
          description: When the grant was revoked, or null while it is active.
        created_at:
          type: string
          format: date-time
          description: When the grant was created (ISO 8601).
        created_by:
          type: string
          format: uuid
          nullable: true
          description: |
            The user who created the grant. For a grant minted with an API key, this is the user who created that key.
        usage_30d:
          type: integer
          description: 30-day tile fetch count. Reported as 0 today; usage aggregation is coming.
    TileGrantCreateResponse:
      type: object
      x-motionworks-status: production
      properties:
        grant_id:
          type: string
          description: Opaque ULID. Treat as a secret.
        tile_json_url:
          type: string
          format: uri
          description: |
            Capability URL — drop this into MapLibre as the `url` for a vector source. The opaque `grant_id` segment IS the credential, so treat the URL like an AWS S3 presigned URL.
        tilesets:
          type: array
          items:
            type: string
          description: Tileset slugs the grant covers.
        allowed_origins:
          type: array
          items:
            type: string
          description: Browser origins allowed to fetch TileJSON and tiles with this grant.
        expires_at:
          type: string
          format: date-time
          description: When the grant expires (ISO 8601).
        created_at:
          type: string
          format: date-time
          description: When the grant was created (ISO 8601).
    TileJsonManifest:
      type: object
      x-motionworks-status: production
      description: |
        TileJSON 3.0.0 manifest with a freshly-minted 24-hour tile-token embedded in `tiles[]`. Standard TileJSON consumers (MapLibre, Mapbox GL JS, deck.gl `MVTLayer`) handle the rest transparently.
      properties:
        tilejson:
          type: string
          example: 3.0.0
          description: TileJSON specification version (`3.0.0`).
        name:
          type: string
          description: Name of the tileset.
        tiles:
          type: array
          items:
            type: string
            format: uri
          description: Tile URL templates with the tile token already embedded. Your map library substitutes `{z}/{x}/{y}`.
        minzoom:
          type: integer
          description: Lowest zoom level served.
        maxzoom:
          type: integer
          description: Highest zoom level served.
        bounds:
          type: array
          items:
            type: number
          minItems: 4
          maxItems: 4
          description: Geographic extent `[west, south, east, north]` in WGS84.
        attribution:
          type: string
          description: Attribution text to show on the map.
        vector_layers:
          type: array
          items:
            type: object
          description: The MVT layers in the tiles and their field schemas.
        x-mw:
          type: object
          description: |
            Motionworks-specific metadata: the grant, the tilesets served, token expiry and refresh guidance, and per-tileset styling metadata.
          properties:
            grant_id:
              type: string
              description: The grant this manifest was minted for.
            product:
              type: string
              enum:
                - popcast
              description: Always `popcast`.
            tilesets:
              type: array
              items:
                type: string
              description: Tileset slugs this manifest serves right now.
            token_expires_at:
              type: string
              format: date-time
              description: When the embedded tile token expires (ISO 8601). Re-fetch the TileJSON before then.
            refresh_after:
              type: integer
              description: Seconds after minting at which to re-fetch this TileJSON — one hour before the tile token expires.
            extra_metadata:
              type: object
              description: |
                Per-tileset styling metadata keyed by slug: the segment, the coverage window, and density `breakpoints` per geography grain for building a choropleth ramp.
              additionalProperties:
                type: object
                required:
                  - breakpoints
                  - refreshed_at
                  - extra_metadata_verified
                  - source
                additionalProperties: true
                properties:
                  breakpoints:
                    type: object
                    required:
                      - d
                      - m
                      - p
                      - v
                    properties:
                      d:
                        type: object
                        description: |
                          Per-grain per-measure rank arrays. Each key is a grain name (e.g. 'ST', 'STCO', 'TRCT', 'BG', or future grains upstream publishes); each value is an array of arrays of numbers (one array per measure, aligned to breakpoints.p length).
                        additionalProperties:
                          type: array
                          items:
                            type: array
                            items:
                              type: number
                      m:
                        type: array
                        minItems: 1
                        description: |
                          Measure names (e.g. ['density', 'occupancy']).
                        items:
                          type: string
                      p:
                        type: array
                        minItems: 1
                        description: |
                          Percentile ranks the breakpoint arrays are sampled at.
                        items:
                          type: number
                      v:
                        type: number
                        description: |
                          Upstream extra_metadata payload version.
                  refreshed_at:
                    type: string
                    format: date-time
                    description: When this tileset's styling metadata was last refreshed (ISO 8601, UTC).
                  extra_metadata_verified:
                    type: boolean
                    const: true
                    description: Always `true` — metadata is only emitted for tilesets whose entry has been verified.
                  source:
                    type: string
                    const: bigquery-extra-metadata-kv
                    description: Where the metadata entry was produced. Informational.
paths:
  /popcast/segments:
    post:
      operationId: createPopcastSegment
      summary: Define a population segment
      description: |
        Creates a population segment from demographic filters. Every filter is optional — omit a filter to place no restriction on it — and filters compose as AND. The response is the resolved segment with its `motionworks_segment_id`, which you reuse in `GET /v2/popcast/at-home` and in Set Dynamics cohorts.

        This endpoint is being enabled and currently returns 501 `NOT_YET_AVAILABLE`. When live: 1 credit per call.
      tags:
        - Popcast
      x-credit-cost: 1
      x-motionworks-status: production
      x-motionworks-source-doc: https://docs.mworks.com/reference/apiref-popcast-cohort-search
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PopcastSegmentDefinition'
      responses:
        '201':
          description: Segment created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PopcastSegment'
                  meta:
                    $ref: '#/components/schemas/Meta'
    get:
      operationId: listPopcastSegments
      summary: List your organization's segments
      description: |
        Returns the segments your organization has defined, with cursor pagination. This endpoint is being enabled and currently returns 501 `NOT_YET_AVAILABLE`. When live: 1 credit per call.
      tags:
        - Popcast
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
          description: Cursor from the previous page's `pagination.cursor`. Omit for the first page.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: Results per page.
      responses:
        '200':
          description: A page of segments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PopcastSegment'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  meta:
                    $ref: '#/components/schemas/Meta'
  /popcast/segments/{segment_id}:
    get:
      operationId: getPopcastSegment
      summary: Read one segment
      description: |
        Returns one of your organization's segments by `motionworks_segment_id`. This endpoint is being enabled and currently returns 501 `NOT_YET_AVAILABLE`. When live: 1 credit per call.
      tags:
        - Popcast
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: segment_id
          in: path
          required: true
          schema:
            type: string
          description: The `motionworks_segment_id` returned when the segment was created.
      responses:
        '200':
          description: The segment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PopcastSegment'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '404':
          description: Unknown `segment_id`, or the segment belongs to a different organization.
  /popcast/at-home:
    get:
      operationId: getPopcastAtHome
      summary: Count a segment's residents by geography
      tags:
        - Popcast
      description: |
        Returns one row per geography: how many persons and households in the segment live there, alongside the geography's total population for context. Two ways to query:

        - **One geography** — pass `segment_id` and `geography_id` for a single row.
        - **Every geography of a type** — pass `segment_id` and `geography_type` (omit `geography_id`) and page through the rows with `cursor`.

        Counts come from an annual population vintage; pass `year` to pick one, or omit it for the current vintage. 2 credits per page.
      x-credit-cost: 2
      x-motionworks-status: production
      x-motionworks-source-doc: https://docs.mworks.com/docs/popcast-at-home
      parameters:
        - name: segment_id
          in: query
          required: true
          schema:
            type: string
          description: The `motionworks_segment_id` of the segment to count.
        - name: geography_id
          in: query
          schema:
            type: string
          description: |
            A single geography to count, for example `US2010XXBG360610031001` (block group) or `US2020STCO36061` (county). Omit it and pass `geography_type` to iterate across every geography of that type.
        - name: year
          in: query
          schema:
            type: integer
          description: Population vintage year. Omit for the current vintage.
        - name: geography_type
          in: query
          schema:
            type: string
            enum:
              - stco
              - bg
              - dma
              - cbsa
              - state
              - zcta
          description: |
            Geography level to iterate when `geography_id` is omitted: `bg` (Census block group), `zcta` (ZIP Code Tabulation Area), `stco` (county), `cbsa` (metropolitan or micropolitan statistical area), `dma` (Nielsen DMA) or `state`.
        - name: cursor
          in: query
          schema:
            type: string
          description: The `cursor` from the previous response's pagination block. Omit for the first page.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 5000
            default: 100
          description: Rows per page.
      responses:
        '200':
          description: A page of At Home rows.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PopcastAtHome'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  meta:
                    $ref: '#/components/schemas/Meta'
  /popcast/tiles/anytime/tilesets:
    get:
      tags:
        - Tile Discovery
      operationId: listPopcastAnytimeTilesets
      summary: List the Popcast Anytime tilesets
      description: |
        Returns the public tile catalog for Popcast Anytime: one entry per published tileset with its `slug`, `title`, segment (`audkey` and `display_name`), coverage window (`yyyymm_start` / `yyyymm_end`), zoom range (`minzoom` / `maxzoom`), `bounds`, the TileJSON `vector_layers[]` field schema (`layers`), `grant_create_url` and `licensed`. Use it to choose the `tilesets[]` for a grant, or to build a segment picker without hardcoding the catalog.

        Every row carries `grant_create_url` — the endpoint to POST to for a grant that covers it. The catalog can be empty when it is being refreshed; `metadata_stale: true` tells you so, and a retry a few minutes later returns the full list. Do not assume a fixed number of rows.

        No authentication required and no charge. Responses are not cached (`Cache-Control: private, no-store`).
      x-credit-cost: 0
      x-motionworks-status: production
      security: []
      responses:
        '200':
          description: Tile catalog.
          headers:
            Cache-Control:
              schema:
                type: string
                example: private, no-store
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TilesetCatalogResponse'
                  meta:
                    $ref: '#/components/schemas/TileMeta'
  /popcast/tiles/anytime/grants:
    post:
      tags:
        - Tile Grants
      operationId: createPopcastAnytimeTileGrant
      summary: Mint a Popcast Anytime vector-tile grant
      description: |
        Creates a tile-delivery grant for your organization and returns its capability `tile_json_url`. Hand that URL to MapLibre, Mapbox GL JS or any TileJSON-aware client and it starts rendering. Free — grant management costs 0 credits; only tile traffic is metered.

        A grant covers the tilesets you list, or every published Popcast Anytime tileset when `tilesets` is empty. Licensing is per organization rather than per segment: once your organization is licensed for Popcast Anytime tiles, one grant can cover the whole catalog.

        Authenticate with an org-scoped API key (`X-API-Key: mw_…`) or a signed-in session (`Authorization: Bearer <jwt>`); anonymous callers cannot mint grants. Popcast Anytime tiles are licensed by arrangement — contact sales to enable them for your organization.
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TileGrantCreateRequest'
      responses:
        '201':
          description: Grant created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TileGrantCreateResponse'
                  meta:
                    $ref: '#/components/schemas/TileMeta'
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            Common cases:
              * `FEATURE_NOT_LICENSED` — your organization is not licensed for Popcast Anytime vector tiles. Contact sales.
              * `TILE_GRANTS_REQUIRE_METERED` — tile delivery for your organization requires pay-as-you-go (metered) billing, which is not enabled. Contact sales to enable tile delivery.
              * `TILE_LAYER_NOT_LICENSED` — one or more `tilesets[]` entries name a tileset Popcast Anytime does not publish.
              * `API_KEY_NO_AUDIT_USER` — the API key is not tied to a user; mint with a signed-in session or a key created by a user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      tags:
        - Tile Grants
      operationId: listPopcastAnytimeTileGrants
      summary: List your organization's grants
      description: Returns your organization's Popcast Anytime tile grants, most recent first. Free.
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
        - apiKey: []
      responses:
        '200':
          description: Grant list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TileGrant'
                  meta:
                    $ref: '#/components/schemas/TileMeta'
        '401':
          description: Missing or invalid credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /popcast/tiles/anytime/grants/{id}:
    get:
      tags:
        - Tile Grants
      operationId: getPopcastAnytimeTileGrant
      summary: Read one grant
      description: Returns one grant by id, scoped to your organization. Free.
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Grant ULID returned by `POST /v2/popcast/tiles/anytime/grants`.
      responses:
        '200':
          description: Grant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TileGrant'
                  meta:
                    $ref: '#/components/schemas/TileMeta'
        '401':
          description: Missing or invalid credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            No grant with this id in your organization. Expired and revoked grants are still returned so you can inspect them.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /popcast/tiles/anytime/grants/{id}/revoke:
    post:
      tags:
        - Tile Grants
      operationId: revokePopcastAnytimeTileGrant
      summary: Revoke a grant immediately
      description: |
        Revokes the grant immediately. New TileJSON requests for it return 404, and tile requests using tokens already minted from it return 401 `UNAUTHORIZED` with `error.context.reason: grant_revoked`. Revoking an already-revoked grant returns 200 with the unchanged record. Free.
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Grant ULID returned by `POST /v2/popcast/tiles/anytime/grants`.
      responses:
        '200':
          description: Grant revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TileGrant'
                  meta:
                    $ref: '#/components/schemas/TileMeta'
        '401':
          description: Missing or invalid credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No grant with this id in your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /popcast/tiles/anytime/grants/{id}/tilejson:
    get:
      tags:
        - Tile TileJSON
      operationId: getPopcastAnytimeTileJson
      summary: Get the TileJSON manifest for a grant
      description: |
        Returns a TileJSON 3.0.0 manifest with a freshly minted 24-hour tile token embedded in `tiles[]`. This is the URL you hand to MapLibre, Mapbox GL JS or any TileJSON-aware client. The manifest covers the tilesets the grant authorizes that are currently published; `x-mw.tilesets` lists exactly what it serves and `x-mw.extra_metadata` carries per-tileset styling metadata such as density breakpoints.

        **This URL is a capability — treat it like a secret.** It carries the credential needed to fetch tiles inside the URL itself, like a presigned object-storage URL: anyone who has it can render your map until the grant expires or is revoked. That is by design — your front-end code can pass it straight to the map library without a separate auth header. Two safety nets are built in: the grant's `allowed_origins` pins which sites can fetch tiles, and `POST /v2/popcast/tiles/anytime/grants/{id}/revoke` ends the grant.

        Free — the tile bytes themselves are metered on `GET .../anytime/{layer}/{z}/{x}/{y}.mvt`.
      x-credit-cost: 0
      x-motionworks-status: production
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Grant ULID. Opaque bearer credential — treat as secret.
      responses:
        '200':
          description: TileJSON 3.0.0 manifest.
          headers:
            Cache-Control:
              schema:
                type: string
                example: private, max-age=3600, must-revalidate
            Vary:
              schema:
                type: string
                example: Origin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TileJsonManifest'
        '403':
          description: |
            `error.code` tells you which:
              * `ORIGIN_NOT_ALLOWED` — the request `Origin` is not in the grant's `allowed_origins` (`error.context.reason: origin_not_allowed`), or the request carried no `Origin` header at all (`origin_required`). Requests must carry an `Origin` header that matches one entry. To test from curl or a native app, include the literal string `null` in `allowed_origins`.
              * `FEATURE_NOT_LICENSED` — your organization is not licensed for Popcast Anytime vector tiles. Contact sales.
              * `TILE_GRANTS_REQUIRE_METERED` — tile delivery for your organization requires pay-as-you-go (metered) billing, which is not enabled. Contact sales to enable tile delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Grant not found, expired, or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /popcast/tiles/anytime/{layer}/{z}/{x}/{y}.mvt:
    get:
      tags:
        - Tile Data
      operationId: getPopcastAnytimeTileMvt
      summary: Fetch a single Popcast Anytime vector tile (binary MVT)
      description: |
        **Your map library calls this for you.** MapLibre, Mapbox GL JS and deck.gl's `MVTLayer` use the `tiles[]` URL templates from the manifest, which already carry the token.

        Returns a Mapbox Vector Tile (binary protobuf) for one Popcast Anytime tileset. The only accepted credential is the `?token=` query parameter — header-based auth is rejected by design, so the TileJSON URL alone is enough for a browser to render. Metered at **1 credit per 1,000 tiles served**. Responses may be gzip-encoded; honor `Content-Encoding`. A tile with no features returns HTTP 204.

        Each tileset serves zoom 0–13 and switches geography grain with zoom: states at low zooms, then counties, census tracts and finally block groups (or ZIP Code Tabulation Areas for the `zcta` family). Features carry one hourly occupancy field per hour (`occ_h00` … `occ_h23`), each holding four day-type slots (Mon–Thu, Fri, Sat, Sun), plus the area fields needed to render density. See the [Popcast Anytime Maptiles guide](https://www.mworks.com/docs/popcast/popcast-anytime-maptiles/) for the field layout and a styling recipe.
      x-credit-cost: 1
      x-motionworks-status: production
      security:
        - tileTokenAuth: []
      externalDocs:
        description: MapLibre vector source spec — explains how MapLibre fetches this endpoint for you.
        url: https://maplibre.org/maplibre-style-spec/sources/#vector
      parameters:
        - name: layer
          in: path
          required: true
          description: |
            A tileset slug from the catalog (see `TilesetCatalogEntry.slug`). A slug that is not published returns 404.
          schema:
            type: string
        - name: z
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 13
          description: |
            Tile zoom level. Popcast Anytime tilesets serve z 0–13; the exact range is in the catalog entry's `minzoom` / `maxzoom`. A zoom outside the range returns 400 `INVALID_REQUEST`.
        - name: x
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
          description: Tile column.
        - name: 'y'
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
          description: Tile row.
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: Opaque tile-token JWT extracted from TileJSON. Treat as secret.
      responses:
        '200':
          description: Vector tile bytes.
          headers:
            Cache-Control:
              schema:
                type: string
                example: private, max-age=300
            Content-Encoding:
              schema:
                type: string
                example: gzip
              description: Present when upstream returned gzip-encoded bytes. Clients MUST honor.
            Vary:
              schema:
                type: string
                example: Accept-Encoding, Origin
            X-MW-Snapshot:
              schema:
                type: string
                example: static
              description: |
                Always the literal `static`. Each Popcast Anytime tileset embeds its coverage window in its slug, so there is no separate per-request snapshot date.
            X-MW-Tileset:
              schema:
                type: string
              description: Resolved upstream tileset id (e.g. `tiles_anytime_v2_3_21d529097c81af04_202501_202512_bg_v1`).
          content:
            application/vnd.mapbox-vector-tile:
              schema:
                type: string
                format: binary
        '204':
          description: Empty tile — no features intersect this z/x/y. No body, no `Content-Encoding`.
        '400':
          description: '`error.code: INVALID_REQUEST` — zoom outside the tileset''s range, or non-integer `x`/`y`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: |
            `error.code: UNAUTHORIZED`. Disambiguated by `error.context.reason`:
              * (no reason) — `?token=` missing.
              * `expired` / `malformed` / `signature_mismatch` — the token is invalid or expired; fetch the TileJSON again.
              * `layer_not_in_tilesets` — the token does not authorize the requested tileset.
              * `grant_revoked` — the backing grant has been revoked.
              * `tile_grant_exhausted` — your organization's credits are exhausted (a billing signal, not an auth failure).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            `error.code: ORIGIN_NOT_ALLOWED` — the request `Origin` is not in the grant's `allowed_origins` (`error.context.reason: origin_not_allowed`), or the request carried no `Origin` header (`origin_required`). Map libraries send `Origin` automatically; to test from curl or a native app, include the literal string `null` in the grant's `allowed_origins`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            `error.code: NOT_FOUND`. Disambiguated by `error.context.reason`:
              * `layer_unknown` — the tileset slug is not one Popcast Anytime publishes.
              * `upstream_unavailable` — the tileset is temporarily unavailable; retry later.
              * (no reason) — the grant referenced by the tile token was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
x-tagGroups:
  - name: Segments & At Home
    tags:
      - Popcast
  - name: Anytime Maptiles
    tags:
      - Tile Discovery
      - Tile Grants
      - Tile TileJSON
      - Tile Data
x-customer-voice: true
