openapi: '3.1.0'
info:
  title: Motionworks API - Markets Maptiles
  version: 1.0.0
  description: |
    Markets Maptiles delivers Motionworks reference geography as Mapbox Vector Tiles (`.mvt`) for any TileJSON-compatible map (MapLibre GL, Mapbox GL JS, deck.gl). Use it to draw the boundaries your Popcast, Placecast and Displays data is reported against, without hosting your own geography.

    Seven polygon layers ship today, all US 2020-vintage reference boundaries served at z 0–12: `blockgroup` (Census block groups), `county`, `metro_division` (metropolitan divisions), `metro` (metropolitan statistical areas), `micro` (micropolitan statistical areas), `state` and `zipcode` (ZIP Code Tabulation Areas). Every feature carries the same six fields: `geography_id` (the Motionworks geography identifier used across every product), `geography_name`, `geography_type`, `national_id` (the Census or OMB code), `country_code` and `year`.

    **Workflow**

    1. **Mint a grant** (`POST /v2/markets/tiles/grants`) from your backend, authenticated with an org-scoped API key or a signed-in session. List the browser origins that will render the map in `allowed_origins`.
    2. **Hand the returned TileJSON URL** (`GET /v2/markets/tiles/grants/{id}/tilejson`) to your map library. It is a capability URL — the grant id in the path is the credential — so it needs no separate auth header.
    3. **Your map library fetches the tiles** (`GET /v2/markets/tiles/{layer}/{z}/{x}/{y}.mvt?token=…`) using the token embedded in the TileJSON. You will not call the `.mvt` endpoint yourself.

    The tile catalog is public and grant management is free; tile traffic is metered at **1 credit per 1,000 tiles served** and charged to your organization. Your organization must be licensed for Markets tiles; the license is included for signed-in and pay-as-you-go accounts. The geography definitions and cross-references are documented under [Geography](https://www.mworks.com/docs/reference-libraries/geography/).
  x-internal-notes: |
    ADR-027 markets follow-up (api#422 PR1 catalog, PR2 delivery). Metering op id markets_tiles_fetch (meter divisor 1000). Feature key markets.reference.geography_tiles is matrix-licensed (authenticated + PAYG default-allow; anon default-deny) so signed-in + PAYG callers mint without an org_feature_grants row; credit-pack-only orgs get 403 TILE_GRANTS_REQUIRE_METERED under TILE_METER_MODE=direct (unreachable under accumulate). Layers are static CARTO tilesets client-services-tiles.geography.us_<layer>_tiles_2020; zoom range 0..12 comes from packages/tile-schema markets-reference.snapshot.json; MVT source-layer id equals the slug (no default alias). Design doc: docs/architecture/27-vector-tile-endpoints.md.

    Round-2 (code check 2026-09-06): dropped "needs pay-as-you-go (metered) billing … on by default" — under TILE_METER_MODE=accumulate (wrangler default) assertTileBillingAllowed never runs the stripe_customer_id rail check, so credit-pack orgs can mint; the sentence now matches the grant-create op.
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com

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

security:
  - apiKey: []

tags:
  - name: Tile Discovery
    description: |
      The public tile catalog: every Markets tileset, with its title, zoom range, bounds 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. It carries a short-lived tile token, so it needs no separate 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.


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/markets/tiles/grants/{id}/tilejson`. Accepted ONLY as
        the `?token=` query parameter — header form is rejected. Treat
        as a short-lived bearer credential.

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            status:
              type: integer
            request_id:
              type: string
            docs_url:
              type: string
          description: "The error: a stable `code`, a human-readable `message`, and optional `context` with the specifics."
    TileMeta:
      type: object
      x-motionworks-status: production
      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: [markets]
          description: "Always `markets`."

    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`). For Markets this is the same as the slug — not `default`.
        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]
      properties:
        slug:
          type: string
          description: Customer-facing layer slug. Matches the `{layer}` URL path segment used in `GET /v2/markets/tiles/{layer}/{z}/{x}/{y}.mvt` and the values accepted in `TileGrantCreateRequest.tilesets`.
        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."
    TilesetCatalogResponse:
      type: object
      x-motionworks-status: production
      required: [tilesets]
      properties:
        tilesets:
          type: array
          items:
            $ref: '#/components/schemas/TilesetCatalogEntry'
          description: Every tileset Markets publishes.
    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: 1
          items:
            type: string
            enum: [blockgroup, county, metro_division, metro, micro, state, zipcode]
          description: |
            Tileset slugs to include. Seven polygon layers ship today, all US 2020-vintage reference boundaries served at z 0–12: `blockgroup` (Census block groups), `county`, `metro_division` (metropolitan divisions), `metro` (metropolitan statistical areas), `micro` (micropolitan statistical areas), `state` and `zipcode` (ZIP Code Tabulation Areas). Any other slug returns 403 `TILE_LAYER_NOT_LICENSED`.
          x-internal-notes: |
            All seven slugs are gated by the single bundled feature key markets.reference.geography_tiles. Grant create rejects any slug not in MARKETS_LAYER_SLUGS (context.missing / context.supported).
        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.
          x-internal-notes: |
            Round-2 (code check 2026-09-06): tileOriginAllowed: a null Origin passes only when allowed_origins includes the literal "null" (index.ts l.211); /tilejson answers 403 ORIGIN_NOT_ALLOWED context.reason origin_required on a missing Origin (l.944-952), origin_not_allowed on a mismatch (l.935-942); the .mvt handler repeats both checks (l.1160-1178).
        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 Markets 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: [markets]
          description: "Always `markets`."
        name:
          type: string
          description: The label you gave the grant.
        tilesets:
          type: array
          items:
            type: string
            enum: [blockgroup, county, metro_division, metro, micro, state, zipcode]
          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.
          x-internal-notes: |
            Round-2 (code check 2026-09-06): always auth.writerUserId — the session JWT sub, or api_keys.created_by resolved in resolveTileJwtAuth (index.ts l.320-352, l.436); a key with no created_by is refused before insert with 403 API_KEY_NO_AUDIT_USER (l.584), and the insert stamps created_by: auth.writerUserId (l.622). Never an API-key id.
        usage_30d:
          type: integer
          description: 30-day tile fetch count. Reported as 0 today; usage aggregation is coming.
          x-internal-notes: "Placeholder — the outbox aggregate query lands in a follow-up; the list handler stamps usage_30d: 0."

    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."
          properties:
            grant_id:
              type: string
              description: The grant this manifest was minted for.
            product:
              type: string
              enum: [markets]
              description: "Always `markets`."
            tilesets:
              type: array
              items:
                type: string
              description: Tileset slugs this manifest serves.
            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.

paths:
  # ─── Tile Discovery (public capability catalog) ──────────────────────

  /markets/tiles/tilesets:
    get:
      tags: [Tile Discovery]
      operationId: listMarketsTilesets
      summary: List the tilesets Markets publishes
      description: |
        Returns the public tile catalog for Markets: every layer slug with its title, zoom range, geographic bounds and TileJSON `vector_layers[]` field schema. Use it to choose the `tilesets[]` for a grant, or to build a layer picker without hardcoding the catalog.

        No authentication required and no charge. Responses are not cached (`Cache-Control: private, no-store`).
      x-internal-notes: |
        The no-store header matches the router's global egress policy on api2.mworks.com, which overwrites upstream cache headers — same posture as /v2/places/autocomplete. Pure in-memory projection via projectCatalog('markets-reference', CATALOG_PROJECTION_OVERLAY); no Supabase / KV / CARTO on the request path.
      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'

  /markets/tiles/grants:
    post:
      tags: [Tile Grants]
      operationId: createMarketsTileGrant
      summary: Mint a Markets 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.

        Authenticate with an org-scoped API key (`X-API-Key: mw_…`) or a signed-in session (`Authorization: Bearer <jwt>`); anonymous callers cannot mint grants. Your organization must be licensed for Markets tiles; the license is included for signed-in and pay-as-you-go accounts. Tile traffic is charged to your organization at 1 credit per 1,000 tiles served. If your organization's billing setup cannot carry tile traffic you receive `403 TILE_GRANTS_REQUIRE_METERED`; contact sales to enable tile delivery.
      x-internal-notes: |
        Licensing: markets.reference.geography_tiles is matrix-licensed for authenticated + PAYG callers (no explicit org_feature_grants row needed). Gate order: license check first; rail check only under TILE_METER_MODE=direct. An API key with no provisioning user recorded (api_keys.created_by null) gets 403 API_KEY_NO_AUDIT_USER because tile_grants.created_by FKs auth.users.
      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 Markets 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 layer Markets does not publish.
              * `API_KEY_NO_AUDIT_USER` — the API key is not tied to a user. Mint with a signed-in session, or with a key created by a user.
          x-internal-notes: |
            A fourth, rarer 403 is API_KEY_NO_AUDIT_USER (API key with no created_by user for grant audit attribution) — not documented customer-side pending a decision on whether to keep that constraint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      tags: [Tile Grants]
      operationId: listMarketsTileGrants
      summary: "List your organization's grants"
      description: "Returns your organization's Markets 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'

  /markets/tiles/grants/{id}:
    get:
      tags: [Tile Grants]
      operationId: getMarketsTileGrant
      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/markets/tiles/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.
          x-internal-notes: |
            Round-2 (code check 2026-09-06): the read/revoke 404 condition is not_found || org mismatch || product mismatch only (index.ts l.757-761 read, l.798-802 revoke); expires_at is checked only by /tilejson (l.922). The old "expired" wording was wrong.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /markets/tiles/grants/{id}/revoke:
    post:
      tags: [Tile Grants]
      operationId: revokeMarketsTileGrant
      summary: Revoke a grant immediately
      description: |
        Revokes the grant immediately: new TileJSON requests for it fail with 404. Tile requests that use tokens already minted from it are refused as well (401 `UNAUTHORIZED` with `error.context.reason: grant_revoked`), so revoking is the instant cutoff for a leaked URL. Revoking an already-revoked grant returns 200 with the unchanged record. Free.
      x-internal-notes: |
        Instant cutoff = rotate MW_VECTOR_TILE_JWT_KEY (operator runbook). Revocation is also mirrored to KV (60s TTL) so the .mvt path rejects tokens of a revoked grant with 401 grant_revoked.
      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/markets/tiles/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'

  /markets/tiles/grants/{id}/tilejson:
    get:
      tags: [Tile TileJSON]
      operationId: getMarketsTileJson
      summary: Get the 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. When a grant covers several layers the manifest's zoom range and bounds are the union across them; each layer keeps its own entry in `vector_layers[]`.

        **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/markets/tiles/grants/{id}/revoke` ends the grant.

        Free — the tile bytes themselves are metered on `GET .../{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 Markets 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.
          x-internal-notes: |
            A request with no Origin header is 403 ORIGIN_NOT_ALLOWED (context.reason origin_required) unless the grant lists the literal "null" origin. License check first; rail check only under TILE_METER_MODE=direct.

            Round-2 (code check 2026-09-06): tileOriginAllowed: a null Origin passes only when allowed_origins includes the literal "null" (index.ts l.211); /tilejson answers 403 ORIGIN_NOT_ALLOWED context.reason origin_required on a missing Origin (l.944-952), origin_not_allowed on a mismatch (l.935-942); the .mvt handler repeats both checks (l.1160-1178).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: "Grant not found, expired, or revoked."
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /markets/tiles/{layer}/{z}/{x}/{y}.mvt:
    get:
      tags: [Tile Data]
      operationId: getMarketsTileMvt
      summary: Fetch a single Markets vector tile (binary MVT)
      description: |
        **Your map library calls this for you.** MapLibre, Mapbox GL JS and deck.gl's `MVTLayer` read the `?token=` from the TileJSON manifest and append it to every tile request.

        Returns a Mapbox Vector Tile (binary protobuf) for one Markets layer. 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.

        Every Markets layer serves z 0–12. Inside the tile bytes the layer id (the MVT `source-layer`) is the same as the slug — `county` tiles carry a `county` layer — and every feature has the six geography fields listed in the catalog. The `X-MW-Snapshot` response header reads `static`: Markets layers always serve the latest published release.
      x-internal-notes: |
        Op id markets_tiles_fetch, meter divisor 1000. All seven layers are static CARTO tilesets (no YYYYMMDD suffix; fixed 2020 vintage on a 10-year cadence) so X-MW-Snapshot carries the literal "static". Colo cache key uses the underlying CARTO name, not the URL path. Full tile-delivery contract: ADR-027 (docs/architecture/27-vector-tile-endpoints.md).
      x-credit-cost: 1
      x-motionworks-status: production
      x-motionworks-source-doc: https://docs.mworks.com/docs/markets-tiles
      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
          schema:
            type: string
            enum: [blockgroup, county, metro_division, metro, micro, state, zipcode]
          description: |
            Tileset slug — one of the seven Markets layers: `blockgroup`, `county`, `metro_division`, `metro`, `micro`, `state`, `zipcode`. A slug the token does not cover returns 401 `layer_not_in_tilesets`.
          x-internal-notes: |
            Round-2 (code check 2026-09-06): the .mvt handler checks payload.tilesets.includes(layer) (401 layer_not_in_tilesets, l.1114-1123) BEFORE TILESETS[layer] (404, l.1125-1133). Grant create rejects slugs outside the published set (403 TILE_LAYER_NOT_LICENSED, l.565), so a token can only name a published slug and the unknown-slug 404 is reachable only if a layer is delisted after the grant was minted.
        - name: z
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 12
          description: |
            Tile zoom level. Every Markets layer serves z 0–12. A zoom outside that 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-Tileset:
              schema:
                type: string
              description: "Resolved tileset id (e.g. `markets.county`)."
            X-MW-Snapshot:
              schema:
                type: string
                format: date
              description: "Reads `static`: Markets layers always serve the latest published release."
              x-internal-notes: "resolveCartoName returns snapshotDate \"static\" for every markets layer (snapshotDateFormat static)."
          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 layer's range (z 0–12), 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 layer.
              * `grant_revoked` — the backing grant has been revoked.
              * `tile_grant_exhausted` — your organization's credits are exhausted (a billing signal, not an auth failure).
          x-internal-notes: |
            Round-2 (code check 2026-09-06): missing token → 401 with no context (index.ts l.1082-1089); every verifyTileToken failure carries context.reason = the VerifyResult reason (l.1100-1111): malformed, unsupported_alg, signature_mismatch, expired, aud_mismatch, product_mismatch, missing_claim (packages/vector-tile-auth/src/index.ts l.81-93). Only the three a customer can plausibly hit are listed.
          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`.
          x-internal-notes: "Round-2 (code check 2026-09-06): same two branches as /tilejson (index.ts l.1160-1178)."
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            The grant behind the token no longer exists, or the layer is no longer published. (A slug the token does not cover returns 401, not 404.)
          x-internal-notes: |
            Round-2 (code check 2026-09-06): two 404 branches, neither with context.reason: TILESETS[layer] miss (l.1125-1133, reachable only for a delisted layer — see the layer param note) and loadTileGrant not_found / product mismatch (l.1151-1158).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
x-tagGroups:
  - name: Markets Maptiles
    tags:
      - Tile Discovery
      - Tile Grants
      - Tile TileJSON
      - Tile Data
x-customer-voice: true
