openapi: '3.1.0'
info:
  title: Motionworks API - Paths Tiles (Vector Map Delivery)
  version: 2.0.0
  description: >
    Mapbox Vector Tile (`.mvt`) delivery for Motionworks Paths data —
    drop Motionworks roadway-volume tiles directly into MapLibre,
    Mapbox GL JS, or any TileJSON-compatible map. Mint a grant scoped
    to your origins, fetch the TileJSON capability URL, and point your
    map at it; tiles render in seconds.


    Tile traffic is metered at **1 credit per 1,000 tiles served**
    (op id `paths_tiles_fetch`). The grant + TileJSON control-plane
    endpoints are free. For the underlying path geometry, viewsheds,
    and hourly traffic measurements, see the
    [Pathcast product page](./pathcast/).


    ## Workflow

    1. **Mint a grant** (`POST /v2/paths/tiles/grants`) — server-side,
       authenticated with your Supabase JWT or `X-API-Key`. Specify
       `allowed_origins` for the browser sites that will render the map.
    2. **Use the returned TileJSON URL** (`GET /v2/paths/tiles/grants/{id}/tilejson`)
       — this is a **capability URL**: the opaque `grant_id` in the path
       IS the credential. Drop it straight into MapLibre's vector source.
    3. **MapLibre fetches the tiles for you** (`GET /v2/paths/tiles/{layer}/{z}/{x}/{y}.mvt?token=…`)
       — extracts the `?token=` JWT from TileJSON and appends it on every
       tile request. You will not call the `.mvt` endpoint directly.


    Source: https://docs.mworks.com/docs/paths-tiles


    Designed per [ADR-027 — Vector Tile Endpoints](https://github.com/InterMx/api-mworks-com/blob/main/docs/architecture/27-vector-tile-endpoints.md).
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com

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

externalDocs:
  description: MapLibre vector source spec (how MapLibre consumes the TileJSON URL returned by this API)
  url: https://maplibre.org/maplibre-style-spec/sources/#vector

security:
  - apiKey: []

tags:
  - name: Tile Discovery
    description: Public capability catalog — list the tilesets Paths publishes (slugs, zoom range, bounds, layer schema). No auth required.
  - name: Grants
    description: Mint, list, read, and revoke tile-delivery grants. JWT or X-API-Key authenticated.
  - name: TileJSON
    description: TileJSON capability URL — the discovery endpoint MapLibre calls to learn where to fetch tiles.
  - name: Tile Data
    description: Binary vector tile bytes. Called by MapLibre/Mapbox GL on your behalf; you will not call this directly in normal use.

components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
      description: >
        Org-scoped API key (`mw_<64-hex>`). Used by server-side callers
        managing grants on behalf of their organization.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        Supabase user JWT (`Authorization: Bearer <jwt>`). Used by
        portal/account flows that manage 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/paths/tiles/grants/{id}/tilejson`. Accepted ONLY as
        the `?token=` query parameter — header form is rejected. Treat
        as a short-lived bearer credential.

  schemas:
    Meta:
      type: object
      description: >-
        Response envelope metadata. Note: unlike the JSON product surfaces,
        the Paths tile-delivery worker does not emit `meta.provenance`
        (TF-93) — a deliberate deviation for a binary/control-plane delivery
        surface. This schema mirrors the shipped fields.
      properties:
        request_id:
          type: string
        credits_used:
          type: integer
        credits_remaining:
          type: integer
        product:
          type: string
          example: paths

    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
              additionalProperties: true

    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 — often `default` for single-layer tilesets, NOT the URL slug).
        description:
          type: string
        minzoom:
          type: integer
          minimum: 0
        maxzoom:
          type: integer
          minimum: 0
        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/paths/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
        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'

    TilesetCatalogResponse:
      type: object
      x-motionworks-status: production
      required: [tilesets]
      properties:
        tilesets:
          type: array
          items:
            $ref: '#/components/schemas/TilesetCatalogEntry'

    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: [path_line]
          description: >
            Tileset slugs to include in this grant. Today only
            `path_line` (roadway-volume polylines) is available.
        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 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: [paths]
        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
              example: paths
            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).

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

  /paths/tiles/tilesets:
    get:
      tags: [Tile Discovery]
      operationId: listPathsTilesets
      summary: List the tilesets Paths publishes
      description: >
        Returns the customer-facing tile catalog for the Paths product —
        every layer slug, human-readable title, zoom range, geographic
        bounds, and TileJSON `vector_layers[]` schema. Use this to pick
        a `tilesets[]` slug before minting a grant via
        `POST /v2/paths/tiles/grants`, or to render a "supported layers"
        UI without hardcoding the catalog client-side.


        Public — no auth header required. Free — 0 credits. Response
        carries `Cache-Control: private, no-store` (matches the router's
        global egress policy on `api2.mworks.com`, which overwrites
        upstream cache headers — same posture as
        `/v2/places/autocomplete`).
      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/Meta'

  # ─── Grants (control plane) ──────────────────────────────────────────

  /paths/tiles/grants:
    post:
      tags: [Grants]
      operationId: createPathsTileGrant
      summary: Mint a Paths vector-tile grant
      description: >
        Creates a new tile-delivery grant scoped to the caller's org
        and returns a capability `tile_json_url`. Drop the URL into
        MapLibre or Mapbox GL JS to start rendering. Free — control-
        plane endpoint, 0 credits.


        Auth: Supabase JWT (`Authorization: Bearer <jwt>`) OR
        org-scoped API key (`X-API-Key: mw_…`). Anonymous callers
        cannot mint grants.
      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/Meta'
        '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: >
            Caller is not licensed for Paths vector tiles
            (`FEATURE_NOT_LICENSED`), or the org's billing rail is
            not yet enabled for tile delivery
            (`TILE_GRANTS_REQUIRE_METERED`). Contact sales.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      tags: [Grants]
      operationId: listPathsTileGrants
      summary: List active grants for the caller's org
      description: >
        Returns the caller-org's Paths 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/Meta'
        '401':
          description: Missing or invalid credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /paths/tiles/grants/{id}:
    get:
      tags: [Grants]
      operationId: getPathsTileGrant
      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/paths/tiles/grants`.
      responses:
        '200':
          description: Grant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TileGrant'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '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'

  /paths/tiles/grants/{id}/revoke:
    post:
      tags: [Grants]
      operationId: revokePathsTileGrant
      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/Meta'
        '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'

  # ─── TileJSON (capability URL) ───────────────────────────────────────

  /paths/tiles/grants/{id}/tilejson:
    get:
      tags: [TileJSON]
      operationId: getPathsTileJson
      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.


        ## This URL is a capability — treat it like a secret

        The TileJSON URL returned by `POST /v2/paths/tiles/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 (24 hours) 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/paths/tiles/grants/{id}/revoke` kills the grant.
        Mint a fresh grant per browser session for the tightest scope.


        Free — control-plane endpoint, 0 credits. 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: Origin not in the grant's `allowed_origins`, or the org is not licensed for Paths vector tiles.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Grant not found, expired, or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ─── Tile Data (binary) ──────────────────────────────────────────────

  /paths/tiles/{layer}/{z}/{x}/{y}.mvt:
    get:
      tags: [Tile Data]
      operationId: getPathsTileMvt
      summary: Fetch a single Paths 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). 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 `paths_tiles_fetch`).
        Response is 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
      x-motionworks-source-doc: https://docs.mworks.com/docs/paths-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: [path_line]
          description: Tileset slug. Today only `path_line` is available.
        - name: z
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 22
          description: Tile zoom level.
        - 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:
            Content-Encoding:
              schema:
                type: string
                example: gzip
              description: Always `gzip` for 200 responses. Clients MUST honor.
            Vary:
              schema:
                type: string
                example: Accept-Encoding, Origin
            X-MW-Tileset:
              schema:
                type: string
              description: Resolved upstream tileset name (e.g. snapshot-stripped CARTO ID).
            X-MW-Snapshot:
              schema:
                type: string
                format: date
              description: Tileset snapshot date (YYYY-MM-DD).
          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: >-
            Invalid tile coordinates — zoom out of range, or non-integer
            `x`/`y` (`error.code: INVALID_REQUEST`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: >-
            `error.code: UNAUTHORIZED`. Three distinct cases, disambiguated by
            `error.context.reason`:
              * (no reason) — missing, invalid, or expired `?token=` tile-token JWT.
              * `grant_revoked` — the backing grant has been revoked.
              * `tile_grant_exhausted` — the wallet has insufficient credits.
                This is a **billing** signal, not an auth failure: top up
                credits rather than refreshing the token.
          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: Unknown layer slug, or the backing grant was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
