openapi: 3.1.0
info:
  title: Popcast (Population Intelligence)
  version: 2.0.0
  description: |
    Segment-based population counts. Customer defines a cohort via demographic filters (Cohort Search), then queries Popcast At Home for how many matching people live in each geography.
    The v2 scaffold modeled Popcast as location profiles with lat/lng. The real product is fundamentally segment x geography x year → counts. There is no lat/lng at the record level; geography is a polygon reference via geography_id.
    Sources:
      https://docs.mworks.com/docs/popcast-at-home
      https://docs.mworks.com/docs/ref-segments
      API: apiref-popcast-at-home, apiref-popcast-cohort-search
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com
servers:
  - url: https://api.mworks.com/v2
    description: Production
tags:
  - name: Tile Discovery
    description: Public capability catalog — list the tilesets the Popcast Anytime product publishes (slugs, titles, zoom ranges, layer schemas, naming-convention metadata). No auth required; free.
  - name: Tile Grants
    description: Mint, list, read, and revoke Popcast Anytime vector-tile delivery grants. Supabase JWT or org-scoped `X-API-Key` (the MA-32 router Tier-A regex widening for the interposed `anytime` segment makes X-API-Key live on these four operations). Product activation via MA-31; worker delivery code lands with MA-32.
  - name: Tile TileJSON
    description: TileJSON capability URL — the discovery endpoint MapLibre calls to learn where to fetch tiles.
  - name: Tile Data
    description: Binary Mapbox Vector Tile bytes. Called by MapLibre/Mapbox GL on your behalf; you will not call this directly in normal use.
security:
  - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Supabase user JWT (`Authorization: Bearer <jwt>`). Used by portal/account flows that mint and manage tile grants for a logged-in user's 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: |
        Customer-defined population segment from demographic filters. Every filter is optional (omit = no restriction). Filters compose as AND.
      properties:
        customer_segment_id:
          type: string
        customer_segment_name:
          type: string
        age_min:
          type: integer
          minimum: 0
          maximum: 100
        age_max:
          type: integer
          minimum: 0
          maximum: 100
        gender:
          type: string
          enum:
            - f
            - m
        races:
          type: array
          items:
            type: string
            enum:
              - white
              - black
              - native
              - asian
              - islander
              - other
              - multiple
        hispanic:
          type: boolean
        worker:
          type: boolean
        language:
          type: array
          items:
            type: string
            enum:
              - english
              - spanish
              - other
        household_size_min:
          type: integer
          minimum: 1
          maximum: 20
        household_size_max:
          type: integer
          minimum: 1
          maximum: 20
        household_children:
          type: boolean
        household_income_min:
          type: integer
          description: USD, inflation-adjusted. May be negative.
        household_income_max:
          type: integer
        household_vehicles_min:
          type: integer
          minimum: 0
          maximum: 5
        household_vehicles_max:
          type: integer
          minimum: 0
          maximum: 5
        prizm_segments:
          type: array
          items:
            type: integer
        consumer_segments:
          type: array
          items:
            type: string
        year:
          type: integer
    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
        customer_segment_id:
          type: string
        customer_segment_name:
          type: string
        year:
          type: integer
        geography_id:
          type: string
          description: |
            e.g. US2010XXBG360610031001 (block group), US2020STCO36061 (county), US2020XDMA532 (DMA).
        persons:
          type: integer
          description: Count matching the segment in this geography.
        households:
          type:
            - integer
            - 'null'
          description: null until the data service exposes household-level breakdown (MA-5 gap).
        all_persons:
          type: integer
          description: Total persons in the geography (no segment filter).
        all_households:
          type:
            - integer
            - 'null'
          description: null until the data service exposes household-level breakdown (MA-5 gap).
        hh_persons:
          type:
            - integer
            - 'null'
          description: Non-group-quarters persons. null — not returned by intermx-data-service.
        hh_households:
          type:
            - integer
            - 'null'
        gq_persons:
          type:
            - integer
            - 'null'
          description: Group-quarters persons (dorms, prisons, military, etc.). null — not returned by intermx-data-service.
        gq_households:
          type:
            - integer
            - 'null'
        vintage:
          type: string
    Pagination:
      type: object
      properties:
        cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
        total:
          type: integer
    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
        source_doc:
          type: string
          format: uri
        methodology_version:
          type: string
        data_vintage:
          type: string
          format: date
        data_freshness:
          type: string
        data_latency_days:
          type: integer
        data_maturity:
          type: string
          enum:
            - production
            - research-preview
            - synthetic-only
            - roadmap
        is_focused:
          type: boolean
          description: |
            Optional: whether the place/entity is directly measured (Placecast focused) rather than estimated via peer-group inference. Mirrors ProvenanceSchema.is_focused in packages/types/src/schemas/provenance.ts. Paired with measurement_method by invariant F2 above.
        measurement_method:
          type: string
          enum:
            - direct
            - estimated
          description: |
            Optional explicit measurement provenance. "direct" means the place is focused and its statistics are measured; "estimated" means the place is unfocused and its statistics use peer-group inference. When both this field and is_focused are present they must agree (is_focused=true => direct, false => estimated).
    Meta:
      type: object
      properties:
        request_id:
          type: string
        credits_used:
          type: integer
        credits_remaining:
          type: integer
        product:
          type: string
        version:
          type: string
        provenance:
          $ref: '#/components/schemas/Provenance'
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            status:
              type: integer
            request_id:
              type: string
            product:
              type: string
            docs_url:
              type: string
            context:
              type: object
              description: 'Per-finding structured fields. `context.reason` discriminates sub-causes on several statuses: on `.mvt` 404s it distinguishes `layer_unknown` (slug outside the published twelve) from a missing grant; the 401 block above enumerates its auth-side reasons.'
              additionalProperties: true
    TileMeta:
      type: object
      x-motionworks-status: production
      required:
        - request_id
        - credits_used
        - product
      properties:
        request_id:
          type: string
        credits_used:
          type: integer
          enum:
            - 0
        product:
          type: string
          enum:
            - popcast
    TilesetCatalogLayer:
      type: object
      x-motionworks-status: production
      description: |
        TileJSON 3.0.0 vector_layers entry — one per MVT layer emitted by the upstream tileset.
      required:
        - id
        - fields
      properties:
        id:
          type: string
          description: MVT-side layer id (matches the layer name CARTO emits inside the tile bytes).
        description:
          type: string
        minzoom:
          type: integer
          minimum: 0
        maxzoom:
          type: integer
          minimum: 0
        fields:
          type: object
          description: Map of MVT feature property to TileJSON field type (String, Number, or 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: Customer-facing tileset slug derived from the BQ table naming convention.
        title:
          type: string
          description: Human-readable display title for this tileset.
        minzoom:
          type: integer
          minimum: 0
        maxzoom:
          type: integer
          minimum: 0
        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'
        audkey:
          type: string
          description: motionworks_segment_id (audkey) from the BQ table name.
        display_name:
          type: string
          description: Customer-facing display name for the segment.
        yyyymm_start:
          type: string
          description: YYYYMM start of the tile coverage window.
        yyyymm_end:
          type: string
          description: YYYYMM end of the tile coverage window.
        format_tag:
          type: string
          description: Naming-convention format tag (metadata constant from the MA-19 naming convention).
        grains:
          type: array
          items:
            type: string
          description: Temporal grains (e.g. ST, STCO, TRCT, BG for Popcast Anytime).
        day_types:
          type: array
          items:
            type: integer
          description: Day types (e.g. 1 Mon-Thu, 2 Fri, 3 Sat, 4 Sun).
        hours:
          type: integer
          description: Hours of coverage (e.g. 24 = occupancy hours 0-23).
        grant_create_url:
          type: string
          format: uri
          description: |
            ADR-027 grant-creation endpoint for this product surface. Emitted on every row (MA-44, 2026-08-11). The only supported path from this catalog to tilejson: POST this URL with a bearer token (Supabase JWT or Tier-A X-API-Key) to mint a grant, GET the returned grant_url appended with /tilejson, receive signed worker-relative tiles[] URLs (token in the path segment, never a query param). The retired tilejson_url_template field (bound to www.mworks.com/ api/carto/tilejson/anytimepop) leaked a live CARTO access_token to any anonymous caller and has been removed from the wire shape entirely.
        licensed:
          type: boolean
          description: |
            Per-caller entitlement flag. Phase-2: hardcoded false (no per-caller entitlement read on this hot path; the ADR-029 gate is the eventual flip).
    TilesetCatalogResponse:
      type: object
      x-motionworks-status: production
      required:
        - tilesets
        - metadata_stale
      properties:
        tilesets:
          type: array
          items:
            $ref: '#/components/schemas/TilesetCatalogEntry'
        metadata_stale:
          type: boolean
          description: |
            MA-40 freshness flag, always present. False means the catalog was filtered to the manifest-verified slug set from POPCAST_EXTRA_METADATA KV (fresh manifest, within 3h). True means the fail-stale floor: KV unbound, missing, malformed, or stale, so the full static projection is served and individual rows may lack a verified extra_metadata entry. Consumers MUST NOT treat row count as constant; twelve is the ceiling, not the guarantee.
    TileBreakpointArray:
      type: array
      minItems: 5
      maxItems: 5
      items:
        type: number
      description: |
        Exactly five strictly-ascending breakpoint numbers for one grain (ST, STCO, TRCT, or BG). The worker enforces strict ascent at merge time (one-ULP-at-magnitude nudge on tie edges) so MapLibre `interpolate` never sees duplicate 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: |
            Anytime tileset slugs to include in this grant, from the discovery catalog (`GET /v2/popcast/tiles/anytime/tilesets`). Slugs follow `anytime_<16hex-audkey>_202501-202512_bg_tiles_v4`; the entitlement is one-feature-all-tilesets (`popcast.anytime.segment_occ_tiles`), so listing any of the twelve published slugs implies access to all of them (all-you-can-eat per the MA-31 ruling). An EMPTY array means "all twelve published tilesets" (MA-31 ruling); the worker expands it and the persisted grant row carries the expanded concrete set. An unknown slug is rejected with 403 `TILE_LAYER_NOT_LICENSED`.
        allowed_origins:
          type: array
          items:
            type: string
          description: |
            Browser `Origin` allowlist for `/tilejson` and `.mvt` fetches. Use the explicit string `"null"` to allow requests that omit `Origin` (e.g. `curl`, server-side rendering). 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: |
        Server representation of a Popcast tile-delivery grant. The `grant_id` is a Crockford-base32 ULID and is itself a bearer credential — anyone who knows the ULID 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
        product:
          type: string
          enum:
            - popcast
        name:
          type: string
        tilesets:
          type: array
          items:
            type: string
        allowed_origins:
          type: array
          items:
            type: string
        expires_at:
          type: string
          format: date-time
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        created_by:
          type: string
          format: uuid
          nullable: true
        usage_30d:
          type: integer
          description: 30-day tile fetch count (placeholder — outbox aggregate lands in a follow-up; reported as 0 today).
    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
        allowed_origins:
          type: array
          items:
            type: string
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    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
        name:
          type: string
        tiles:
          type: array
          items:
            type: string
            format: uri
        minzoom:
          type: integer
        maxzoom:
          type: integer
        bounds:
          type: array
          items:
            type: number
          minItems: 4
          maxItems: 4
        attribution:
          type: string
        vector_layers:
          type: array
          items:
            type: object
        x-mw:
          type: object
          description: Motionworks-specific tile-token refresh metadata.
          properties:
            grant_id:
              type: string
            product:
              type: string
              enum:
                - popcast
            tilesets:
              type: array
              items:
                type: string
            token_expires_at:
              type: string
              format: date-time
            refresh_after:
              type: integer
              description: Seconds-since-mint after which clients should re-fetch this TileJSON. Set to 82,800 (23h, one hour before the tile-token JWT expires).
            extra_metadata:
              type: object
              description: |
                Per-tileset renderer metadata block (ADR-027 MA-40 amendment). Keyed by tileset slug. Each value is a FAITHFUL PASS-THROUGH of the upstream BigQuery extra_metadata block (probed at the z=-1 / carto_partition=1 row) with ONLY three added provenance stamps: refreshed_at (ISO-8601), extra_metadata_verified (const true), and source (const 'bigquery-extra-metadata-kv'). The breakpoints object carries per-grain per-measure rank arrays (breakpoints.d), a non-empty measure-name list (breakpoints.m), a non-empty percentile-rank list (breakpoints.p), and an upstream payload version (breakpoints.v). Upstream sub-objects (breakpoints_spec, derived_grains, encoding, months, segment, zoom_bands, and any future additions) pass through VERBATIM under additionalProperties; the API computes NOTHING from the metadata (no 5-stop projection, no palette ramp, no entry-level version literal). FALLBACK VARIANT: for slugs lacking a verified KV entry, the tilejson response falls back to the hand-authored v2 shape in services/popcast/src/scale.ts (PER_TILESET_EXTRA_METADATA): five strictly-ascending density-only breakpoint numbers per grain (ST, STCO, TRCT, BG), a five-color RGB palette, a version literal (2), source 'extra_metadata.breakpoints payload v2', and extra_metadata_verified: false. The two shapes are observably different; the dynamic path does NOT mirror the floor. The floor is the fail-stale guarantee that the surface never goes empty. Shape pinned by tests/contract/popcast-anytime-tilejson-scale.* and emitted by the grant-scoped /tilejson handler.
              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: |
                      ISO-8601 UTC timestamp of the last KV write by the Go refresher.
                  extra_metadata_verified:
                    type: boolean
                    const: true
                    description: |
                      True when the entry was sourced from a verified KV probe. The fallback floor sets this to false.
                  source:
                    type: string
                    const: bigquery-extra-metadata-kv
                    description: |
                      Identifies the entry as a BigQuery-sourced KV entry. The fallback floor uses 'extra_metadata.breakpoints payload v2'.
paths:
  /popcast/segments:
    post:
      operationId: createPopcastSegment
      summary: Define a custom population segment
      description: |
        Submits a segment definition from demographic filters. Returns the resolved segment with motionworks_segment_id that can be used in At Home queries and Set Dynamics cohorts.
      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 segments defined for this org
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Paginated 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: Get a segment by motionworks_segment_id
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: segment_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Segment
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PopcastSegment'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '404':
          description: Segment not found
  /popcast/at-home:
    get:
      operationId: getPopcastAtHome
      summary: Count matching persons per geography for a segment
      description: |
        Returns one row per geography matching the query. Provide both segment_id and geography_id for a single row; omit geography_id and provide geography_type to iterate across all geographies of that type. Counts are from the segment's year Digital Population.
      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
        - name: geography_id
          in: query
          schema:
            type: string
        - name: year
          in: query
          schema:
            type: integer
        - name: geography_type
          in: query
          schema:
            type: string
            enum:
              - stco
              - bg
              - dma
              - cbsa
              - state
              - zcta
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 5000
            default: 100
      responses:
        '200':
          description: Paginated PopcastAtHome 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 Anytime Population tilesets Popcast publishes
      description: |
        Returns the customer-facing tile catalog for the Popcast Anytime product. The catalog's static ceiling is twelve rows (All Persons plus eleven audience segments), one per motionworks_segment_id, covering 2025-01 through 2025-12 on the `_bg_tiles_v4` family (MA-19 ruling 2026-08-05: keep the filter tight, no suffixes, `_draft` versions excluded). Segment display names are joined from `mworks-product.reference.popcast_segment_definitions_v2`. The static content is derived from the committed `popcast-anytime.snapshot.json` in `@mworks/tile-schema` plus the popcast worker's `CATALOG_PROJECTION_OVERLAY`. Rows carry the naming-convention fields (audkey, display_name, yyyymm_start, yyyymm_end, format_tag, grains, day_types, hours) plus a `licensed` flag (Phase-2: hardcoded false; the ADR-029 entitlement read is the eventual flip). `grant_create_url` is emitted on EVERY row (MA-44, 2026-08-11) as the navigable pointer to the ADR-027 grant surface (`POST /v2/popcast/tiles/anytime/grants`). The retired `tilejson_url_template` field (formerly present only on the All Persons row, bound to www.mworks.com/api/carto/tilejson/ anytimepop) has been removed entirely: that URL was a public CARTO-tilejson proxy that leaked a live access_token and the internal tileset FQNs to any anonymous caller, and is now retired (MA-44). The API-side catalog no longer advertises ANY direct CARTO-tilejson URL; the grant flow is the only supported path from discovery to tilejson.
        MA-40 (2026-08-08): cardinality is KV-dependent, not constant. The handler reads the `popcast:em:_manifest` blob from the POPCAST_EXTRA_METADATA KV binding on the request hot path and filters the static projection down to slugs the manifest verifies (a slug present with `verified: false` is excluded). When the namespace is unbound, the manifest missing, malformed, or older than 3h, the handler serves the full static projection as the fail-stale floor and sets `data.metadata_stale: true` (the flag is `false` only on the KV-verified path). No Supabase or CARTO calls on the request hot path; the only binding read is the catalog's own KV manifest.
        Public: no auth header required. Free: 0 credits. Response carries `Cache-Control: private, no-store` (matches the router's global egress policy).
      x-credit-cost: 0
      x-motionworks-status: production
      security: []
      responses:
        '200':
          description: Anytime 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 new tile-delivery grant covering ALL TWELVE Popcast Anytime Population tilesets (the `_bg_tiles_v4` Phase-2 family) and returns a capability `tile_json_url`. Drop the URL into MapLibre or Mapbox GL JS to start rendering. One grant is all-you-can-eat across the twelve audience-segment tilesets; there is no per-segment scoping (MA-31 ruling). Free — control- plane endpoint, 0 credits.

        Auth: Supabase JWT (`Authorization: Bearer <jwt>`) or org-scoped `X-API-Key`, the same posture as the sibling adjacent-path families (`/v2/<product>/tiles/grants`): the MA-32 router `TILE_GRANT_CRUD_PATTERN` widening accepts one optional interposed consumer-product segment, so this family's `/v2/popcast/tiles/anytime/grants` paths run the Tier-A X-API-Key check exactly like the adjacent families. Anonymous callers cannot mint grants. The caller's organization must be licensed for Popcast Anytime vector tiles (`popcast.anytime.segment_occ_tiles` — manual-grant-only: an explicit `org_feature_grants` row is required).
      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: |
            One of three cases:
              * `FEATURE_NOT_LICENSED` — caller's org has no explicit
                grant for `popcast.anytime.segment_occ_tiles`
                (manual-grant-only feature). Contact sales for an
                explicit grant.
              * `TILE_GRANTS_REQUIRE_METERED` — caller's org has no
                metered Stripe billing rail (credit-pack only). Contact
                sales to enable Popcast Anytime vector-tile delivery.
                Gate order note: the license check runs FIRST at mint
                and tilejson time; the rail check only runs under
                `TILE_METER_MODE=direct`. Under the deployed
                `accumulate` mode this code is unreachable (credit-pack
                wallets are billed in-transaction by `accrue_tile`, so
                the license gate is the sole gate).
              * `TILE_LAYER_NOT_LICENSED` — one or more `tilesets[]`
                entries name a slug Popcast Anytime does not publish.
                Note the entitlement is one-feature-all-tilesets: any
                of the twelve published slugs implies access to all of
                them, so this code only fires on an UNKNOWN slug, not a
                licensed-subset scope rejection.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      tags:
        - Tile Grants
      operationId: listPopcastAnytimeTileGrants
      summary: List active grants for the caller's org
      description: |
        Returns the caller-org's Popcast Anytime tile grants, most recent first. Free — control-plane endpoint, 0 credits.
      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 a single grant by ULID, scoped to the caller's org. Free — control-plane endpoint, 0 credits.
      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: Grant not found, expired, or belongs to a different org.
          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: |
        Marks the grant revoked. The 24-hour tile-token JWTs already minted by `/tilejson` will continue to validate until they expire; for an instant cutoff in production, rotate `MW_VECTOR_TILE_JWT_KEY` (operator runbook). Free — 0 credits.
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      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: Grant not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /popcast/tiles/anytime/grants/{id}/tilejson:
    get:
      tags:
        - Tile TileJSON
      operationId: getPopcastAnytimeTileJson
      summary: TileJSON manifest for a grant (capability URL — no auth header)
      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 all twelve Anytime audience-segment tilesets authorized by the grant.

        ## This URL is a capability — treat it like a secret
        The TileJSON URL returned by `POST /v2/popcast/tiles/anytime/grants` is a **capability URL** — it carries the credentials needed to fetch tiles embedded in the URL itself. Treat it like an AWS S3 presigned URL: anyone who has the URL can render your map until the underlying grant expires or is revoked. This is by design — your front-end JavaScript can pass it straight to MapLibre without a separate auth header. Two safety nets are built in: the grant's `allowed_origins` list pins which sites can fetch tiles (Origin-enforced), and `POST /v2/popcast/tiles/anytime/grants/{id}/revoke` kills the grant.

        Free — control-plane endpoint, 0 credits. 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: |
            Origin not in the grant's `allowed_origins`, or the org is not licensed/metered for Popcast Anytime vector tiles (`FEATURE_NOT_LICENSED` / `TILE_GRANTS_REQUIRE_METERED` / `ORIGIN_NOT_ALLOWED`). The license check runs first at mint time; the rail check runs second only under `TILE_METER_MODE=direct` (see `create`'s 403).
          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: |
        **You will not call this endpoint directly in normal use.** MapLibre, Mapbox GL JS, and deck.gl's `MVTLayer` extract the `?token=` JWT from the TileJSON manifest above and append it to every tile request on your behalf.

        Returns a Mapbox Vector Tile (binary protobuf) for the requested Anytime audience-segment layer. Auth is the `?token=<opaque-jwt>` query parameter ONLY — header form is rejected by design (capability semantics). Metered at **1 credit per 1,000 tiles served** (op id `popcast_anytime_tiles_fetch`, meter divisor 1000). Response may be gzip-encoded — clients MUST honor `Content-Encoding`. Empty tiles (z/x/y with no features) return HTTP 204.

        See [ADR-027](https://github.com/InterMx/api-mworks-com/blob/main/docs/architecture/27-vector-tile-endpoints.md) for the full tile-delivery contract.
      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: |
            Anytime tileset slug from the discovery catalog — `anytime_<16hex-audkey>_202501-202512_bg_tiles_v4`. Twelve slugs are published (one per audience segment, All Persons included); list them via `GET /v2/popcast/tiles/anytime/tilesets`.
          schema:
            type: string
        - name: z
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 13
          description: Tile zoom level. Popcast Anytime `maxzoom` is 13 (`_bg_tiles_v4` hand-authored snapshot); the valid range follows the tileset's published zoom bounds in the discovery catalog.
        - 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: Carries the literal `static`, matching the Viewcast convention. Popcast Anytime tilesets are date-embedded in the tileset id itself (`anytime_<16hex>_<yyyymm_start>-<yyyymm_end>_bg_tiles_v4`), so there is no per-request snapshot date to surface; CARTO resolves the single published snapshot of each date-embedded tileset server-side.
            X-MW-Tileset:
              schema:
                type: string
              description: Resolved upstream tileset id (e.g. `anytime_21d529097c81af04_202501-202512_bg_tiles_v4`).
          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 out of range for the layer''s published zoom bounds, 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) — missing, invalid, or expired `?token=` tile-token JWT.
              * `layer_not_in_tilesets` — token does not authorize the requested layer.
              * `grant_revoked` — the backing grant has been revoked.
              * `tile_grant_exhausted` — the org's credit wallet is exhausted (billing signal, not auth).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: 'Request `Origin` is not in the grant''s `allowed_origins` (`error.code: ORIGIN_NOT_ALLOWED`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |-
            `error.code: NOT_FOUND`. Disambiguated by `error.context.reason`:
              * `layer_unknown` — the layer slug is outside the published
                twelve Popcast Anytime tilesets.
              * `upstream_unavailable` — the CARTO upstream answered 404:
                the published tileset is gone upstream (indicates a
                snapshot/tileset outage, not a caller error).
              * (no reason) — the backing grant referenced by the tile token
                was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
