openapi: 3.1.0
info:
  title: Placecast
  version: 2.1.0
  description: |
    The Placecast API is Motionworks' place intelligence family: who visits a place, how often, how long they stay, and where they come from, for any place in the Motionworks Places library.

    - **Placecast Profiles** — the continuous visit profile of one place: visits, dropoffs, pass-bys and stays, dwell time, visit frequency, unique visitors, and a reliability assessment against the place's peer group. 20 credits per call.
    - **Placecast Select** — an on-demand scenario: name a place or place set and a date-time window (a concert, a recurring market, a competitor set over a quarter) and receive the activity profile for exactly that window. Submitting a scenario costs 2,000 credits; listing and polling cost 1 credit.
    - **Placecast Maptiles** — vector tiles of the building footprints (`buildings`) and District Collection Area polygons (`dca`) of measured places. Mint a grant, hand its TileJSON URL to MapLibre or Mapbox GL JS, and render. Grant management is free; tile traffic is metered at 1 credit per 1,000 tiles.

    **Availability:** the Profiles and Select endpoints are not yet live — calls currently return 501 `NOT_YET_AVAILABLE`. The Maptiles endpoints are live.

    Authenticate with an org-scoped API key (`X-API-Key: mw_…`) or a signed-in session. The tile catalog is public. Prose guides: [Placecast Profiles](https://www.mworks.com/docs/placecast-profiles/), [Placecast Select](https://www.mworks.com/docs/placecast-select/) and the [Placecast family overview](https://www.mworks.com/docs/placecast/).
  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: Placecast
    description: |
      Placecast Profiles and Placecast Select: the continuous visit profile of a place, and on-demand activity analysis for a place or place set over a date-time window you choose. Profiles cost 20 credits per call; submitting a Select scenario costs 2,000 credits, listing and polling 1 credit. Not yet live — calls currently return 501 `NOT_YET_AVAILABLE`.
  - 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 Discovery
    description: |
      The public tile catalog: every layer Placecast publishes, with its title, zoom range and field schema. No authentication, no charge.
  - 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/placecast/tiles/grants/{id}/tilejson`. Accepted ONLY as the `?token=` query parameter — header form is rejected. Treat as a short-lived bearer credential.
  schemas:
    VisitsCi:
      type: array
      prefixItems:
        - type: integer
          description: Lower bound.
        - type: integer
          description: Upper bound.
      items:
        type: integer
      minItems: 2
      maxItems: 2
      description: |
        Confidence interval on the estimate as `[lower, upper]` integers. For Placecast Profiles this is a 90% interval: if the estimate were repeated with new samples of people, the interval would contain the result 90% of the time. The width is not configurable.
    PeerGroup:
      type: object
      description: |
        The group of similar places a place is benchmarked against, and whether the place itself is measured directly.
      required:
        - size
        - level
      properties:
        size:
          type: integer
          minimum: 0
          description: Number of places in the peer group.
        level:
          type: string
          enum:
            - direct
            - fallback
          description: |
            `direct` — the place is measured directly and the peer group is used for benchmarking only. `fallback` — the place has no direct measurement and its statistics are estimated from the peer group.
    PlacecastReliability:
      type: object
      description: |
        How reliable the place's measurement is, as four scores from 0 to 1 that compare the place with similar places (its peer group). 0 means the place behaves as expected for its peer group; values approaching 1 mean it departs increasingly from that expectation. `is_focused` tells you whether the statistics are measured directly (`true`) or estimated from the peer group (`false`): when it is `false`, `peer_group.level` is always `fallback`, and when `true`, always `direct`. Read the scores in the context of `placecast_type` and `methodology_version`.
      x-motionworks-status: production
      x-motionworks-source: placecast-validation
      x-motionworks-source-doc: https://docs.mworks.com/docs/placecast-validation
      required:
        - placecast_type
        - visit_score
        - dwell_score
        - profile_score
        - visitor_distance_score
        - is_focused
        - peer_group
        - methodology_version
      if:
        properties:
          is_focused:
            const: true
      then:
        properties:
          peer_group:
            properties:
              level:
                const: direct
      else:
        properties:
          peer_group:
            properties:
              level:
                const: fallback
      properties:
        placecast_type:
          type: string
          minLength: 1
          description: |
            The validation cohort — the kind of place — the scores are computed within, for example `transit_station`, `amusement_park` or `sitdown_restaurant`. Not a closed list.
        visit_score:
          type: number
          minimum: 0
          maximum: 1
          description: |
            Compares the place's visits per square foot against similar places. 0 = expected behaviour, 1 = significant outlier.
        dwell_score:
          type: number
          minimum: 0
          maximum: 1
          description: |
            Compares the average time visitors spend at the place against similar places. Both unusually short and unusually long dwell times produce a higher score.
        profile_score:
          type: number
          minimum: 0
          maximum: 1
          description: Compares the shape of the place's hourly visit pattern across the week against similar places.
        visitor_distance_score:
          type: number
          minimum: 0
          maximum: 1
          description: Compares the typical distance visitors travel from home to reach the place against similar places.
        is_focused:
          type: boolean
          description: |
            `true` when the place is measured directly; `false` when its statistics are estimated from its peer group (and `peer_group.level` is `fallback`).
        peer_group:
          $ref: '#/components/schemas/PeerGroup'
        methodology_version:
          type: string
          description: |
            Placecast methodology version the scores were produced under, for example `v2.3`. Score interpretation can differ between versions.
    PlacecastProfile:
      type: object
      x-motionworks-status: production
      x-motionworks-source: placecast-profiles
      x-motionworks-source-doc: https://docs.mworks.com/docs/placecast-profiles
      description: |
        The visit profile of one place for one publication (`published_date`): activity counts, dwell, frequency, unique visitors, location and a reliability assessment. Activities are visits plus dropoffs, split by the place's dwell threshold; pass-bys have zero dwell and are not activities; stays are overnight visits and are a subset of visits.
      required:
        - place_id
        - name
        - place_type_id
        - place_type
        - place_audit_status
        - place_modified_date
        - published_date
        - visits
        - dropoffs
        - activities
        - visits_ci
        - visits_observations
        - imputed
        - visits_avg_dwell
        - activities_dwell_threshold
        - visits_frequency_per_person
        - visits_unique_persons
        - reliability
      properties:
        place_id:
          type: integer
          description: Unique, persistent identifier of the place.
        name:
          type: string
          description: Name of the place.
        place_type_id:
          type: integer
          description: Identifier of the place type.
        place_type:
          type: string
          description: Description of the place type, for example `Retail`.
        place_audit_status:
          type: string
          description: Audit status of the place record, for example `Reviewed`.
        place_modified_date:
          type: string
          format: date
          description: The last date the place's metadata, polygon or audit status changed (ISO 8601 date).
        published_date:
          type: string
          format: date
          description: |
            Publication date of this report (ISO 8601 date). Pass it as `published_date` on the request to retrieve an earlier publication.
        visits:
          type: integer
          description: |
            Estimated visits at the place for this publication — activities with a dwell time at or above `activities_dwell_threshold`. A rolling estimate, not a daily count.
        dropoffs:
          type: integer
          description: Estimated dropoffs at the place — activities with a dwell time below `activities_dwell_threshold`.
        activities:
          type: integer
          description: Estimated activities at the place — any significant time spent there. Equals `visits` + `dropoffs`.
        passbys:
          type: integer
          description: |
            Estimated pass-bys — people who passed the place with zero dwell time. Pass-bys are not activities and are not counted as visits or dropoffs.
        stays:
          type: integer
          description: |
            Estimated overnight stays at the place. Stays are a subset of visits; the [Placecast Profiles guide](https://www.mworks.com/docs/placecast-profiles/) defines what counts as a stay.
        visits_ci:
          $ref: '#/components/schemas/VisitsCi'
        visits_observations:
          type: integer
          description: Number of visits observed directly in the sample behind the estimate.
        imputed:
          type: boolean
          description: |
            `true` when sparse data required a longer time window than the standard one; every metric on the row is then imputed.
        visits_avg_dwell:
          type: number
          description: |
            Average dwell time in minutes per visitor, excluding people who live in the block groups intersecting the place or who work there.
        activities_dwell_threshold:
          type: number
          description: |
            The dwell time, in minutes, that separates a visit from a dropoff at this place. Derived from observed behaviour across places of the same type.
        visits_frequency_per_person:
          type: number
          description: |
            Average visits per person, excluding residents of the intersecting block groups and people who work at the place.
        visits_unique_persons:
          type: integer
          description: Estimated unique persons visiting the place — `visits` divided by `visits_frequency_per_person`.
        visits_unique_long_trips:
          type: integer
          description: |
            Estimated unique long-distance trips to the place — visits by people whose home is far from the place. The [Placecast Profiles guide](https://www.mworks.com/docs/placecast-profiles/) defines the distance rule.
        city:
          type: string
          description: City of the place.
        state:
          type: string
          description: State or province of the place.
        lat:
          type: number
          description: Latitude of the place (WGS84 degrees).
        lon:
          type: number
          description: Longitude of the place (WGS84 degrees).
        reliability:
          $ref: '#/components/schemas/PlacecastReliability'
    PlacecastSelectRequest:
      type: object
      x-motionworks-status: production
      x-motionworks-source: placecast-select
      x-motionworks-source-doc: https://docs.mworks.com/docs/placecast-select
      required:
        - name
        - place_ids
        - start_datetime
        - end_datetime
      properties:
        name:
          type: string
          description: Your own name for the scenario.
        place_ids:
          type: array
          minItems: 1
          items:
            type: integer
          description: One or more Motionworks place identifiers — a single place or a place set.
        start_datetime:
          type: string
          description: Start of the window, ISO 8601, in the local time zone of the places.
        end_datetime:
          type: string
          description: End of the window, ISO 8601, in the local time zone of the places.
        segment_ids:
          type: array
          items:
            type: string
          description: Motionworks segment identifiers to include in the scenario.
        custom_dwell_threshold:
          type: number
          minimum: 2
          maximum: 30
          description: |
            Dwell threshold in minutes that separates a visit from a dropoff for this scenario, between 2 and 30. Defaults to each place's own `activities_dwell_threshold`.
      description: |
        A Placecast Select scenario: the places to measure, the date-time window, and optional segments and dwell threshold.
    PlacecastSelection:
      type: object
      properties:
        selection_id:
          type: string
          description: Identifier of the scenario. Use it to poll `GET /v2/placecast/select/{selection_id}`.
        name:
          type: string
          description: The name you gave the scenario.
        status:
          type: string
          enum:
            - pending
            - processing
            - complete
            - failed
          description: |
            `pending` (accepted, not yet started), `processing`, `complete` (results populated) or `failed` (see `error`).
        place_ids:
          type: array
          items:
            type: integer
          description: The places the scenario measures.
        start_datetime:
          type: string
          description: Start of the window, as submitted.
        end_datetime:
          type: string
          description: End of the window, as submitted.
        created_at:
          type: string
          format: date-time
          description: When the scenario was submitted (ISO 8601).
        updated_at:
          type: string
          format: date-time
          description: When the scenario last changed state (ISO 8601).
        activities:
          type: integer
          nullable: true
          description: |
            Activities at the places inside the window (`visits` + `dropoffs`). Null until `status` is `complete`.
        visits:
          type: integer
          nullable: true
          description: |
            Visits — activities with a dwell time at or above the dwell threshold. Null until `status` is `complete`.
        dropoffs:
          type: integer
          nullable: true
          description: |
            Dropoffs — activities with a dwell time below the dwell threshold. Null until `status` is `complete`.
        passbys:
          type: integer
          nullable: true
          description: Pass-bys — zero-dwell passes, not counted as activities. Null until `status` is `complete`.
        stays:
          type: integer
          nullable: true
          description: Stays — overnight visits, a subset of visits. Null until `status` is `complete`.
        visits_ci:
          $ref: '#/components/schemas/VisitsCi'
          nullable: true
        visits_observations:
          type: integer
          nullable: true
          description: Visits observed directly in the sample. Null until `status` is `complete`.
        imputed:
          type: boolean
          nullable: true
          description: |
            Whether the estimate needed a longer time window because of sparse data. Null until `status` is `complete`.
        visits_avg_dwell:
          type: number
          nullable: true
          description: Average dwell time in minutes per visitor. Null until `status` is `complete`.
        visits_unique_persons:
          type: integer
          nullable: true
          description: Estimated unique persons visiting. Null until `status` is `complete`.
        error:
          type: string
          nullable: true
          description: Why the scenario failed, when `status` is `failed`; otherwise null.
      description: |
        A Placecast Select scenario and, once complete, its results. Result fields are null until `status` is `complete`.
    Pagination:
      type: object
      properties:
        cursor:
          type: string
          nullable: true
          description: Opaque cursor for the next page. Pass it back as `cursor` to continue; absent on the last page.
        has_more:
          type: boolean
          description: Whether another page follows.
        total:
          type: integer
          description: Total matching results, when the endpoint reports one.
    Provenance:
      type: object
      if:
        required:
          - is_focused
          - measurement_method
      then:
        oneOf:
          - properties:
              is_focused:
                const: true
              measurement_method:
                const: direct
          - properties:
              is_focused:
                const: false
              measurement_method:
                const: estimated
      properties:
        source:
          type: string
          description: The Motionworks dataset or model the result came from.
        source_doc:
          type: string
          format: uri
          description: Link to the methodology or dataset documentation for this result.
        methodology_version:
          type: string
          description: Version of the methodology that produced the result.
        data_vintage:
          type: string
          format: date
          description: The date the underlying data represents.
        data_freshness:
          type: string
          description: How often the underlying data is refreshed (for example `daily`, `monthly`, `annually`).
        data_latency_days:
          type: integer
          description: Typical days between an observation and its availability in the API.
        data_maturity:
          type: string
          enum:
            - production
            - research-preview
            - synthetic-only
            - roadmap
          description: |
            Maturity of the data behind the result: `production` (supported and stable), `research-preview` (available, subject to change), `synthetic-only` or `roadmap` (not yet backed by production data).
        is_focused:
          type: boolean
          description: |
            Whether the place is directly measured (`true`) rather than estimated from its peer group (`false`). When present alongside `measurement_method`, the two agree.
        measurement_method:
          type: string
          enum:
            - direct
            - estimated
          description: |
            `direct` when the place is measured directly; `estimated` when its statistics come from peer-group inference. When present alongside `is_focused`, the two agree (`true` ↔ `direct`).
    Meta:
      type: object
      properties:
        request_id:
          type: string
          description: Unique id for this request. Quote it when contacting support.
        credits_used:
          type: integer
          description: Credits charged for this call.
        credits_remaining:
          type: integer
          description: Credits remaining on your account after this call.
        product:
          type: string
          description: The Motionworks product that served the call.
        version:
          type: string
          description: API version that served the call.
        provenance:
          $ref: '#/components/schemas/Provenance'
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Stable error code, for example `NOT_FOUND` or `UNAUTHORIZED`.
            message:
              type: string
              description: Human-readable explanation.
            status:
              type: integer
              description: HTTP status of the response.
            request_id:
              type: string
              description: Unique id for this request. Quote it when contacting support.
            docs_url:
              type: string
              description: Link to the documentation for this error code.
          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:
            - placecast
          description: Always `placecast`.
    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`). `default` for both Placecast layers — not the URL slug.
        description:
          type: string
          description: What the layer contains.
        minzoom:
          type: integer
          minimum: 0
          description: Lowest zoom level the layer serves.
        maxzoom:
          type: integer
          minimum: 0
          description: Highest zoom level the layer serves.
        fields:
          type: object
          description: Map of MVT feature property → TileJSON field type (`String` | `Number` | `Boolean`).
          additionalProperties:
            type: string
            enum:
              - String
              - Number
              - Boolean
    TilesetCatalogEntry:
      type: object
      x-motionworks-status: production
      required:
        - slug
        - title
        - minzoom
        - maxzoom
        - bounds
        - layers
      properties:
        slug:
          type: string
          description: Customer-facing layer slug. Matches the `{layer}` URL path segment used in `GET /v2/placecast/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 Placecast 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:
              - buildings
              - dca
          description: |
            Tileset slugs to include: `buildings` (building footprints of measured places) and `dca` (the District Collection Area polygon around each measured place), or both. Both are polygon layers serving zoom 0–14. Any other slug returns 403 `TILE_LAYER_NOT_LICENSED`.
        allowed_origins:
          type: array
          items:
            type: string
          description: |
            Browser `Origin` allowlist for the TileJSON and tile requests made with this grant. Requests must carry an `Origin` header that matches one entry. To test from curl or a native app, include the literal string `null` in `allowed_origins`. A request without an `Origin` header is otherwise refused (403 `ORIGIN_NOT_ALLOWED`, `error.context.reason: origin_required`). Wildcards are not supported — list each origin literally.
        expires_at:
          type: string
          format: date-time
          description: |
            ISO 8601 timestamp at which this grant expires. The tile-token JWT minted by `/tilejson` independently expires 24h after each mint — re-fetch TileJSON before then.
    TileGrant:
      type: object
      x-motionworks-status: production
      description: |
        A Placecast 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:
            - placecast
          description: Always `placecast`.
        name:
          type: string
          description: The label you gave the grant.
        tilesets:
          type: array
          items:
            type: string
            enum:
              - buildings
              - dca
          description: Tileset slugs the grant covers.
        allowed_origins:
          type: array
          items:
            type: string
          description: Browser origins allowed to fetch TileJSON and tiles with this grant.
        expires_at:
          type: string
          format: date-time
          description: When the grant expires (ISO 8601).
        revoked_at:
          type: string
          format: date-time
          nullable: true
          description: When the grant was revoked, or null while it is active.
        created_at:
          type: string
          format: date-time
          description: When the grant was created (ISO 8601).
        created_by:
          type: string
          format: uuid
          nullable: true
          description: |
            The user who created the grant. For a grant minted with an API key, this is the user who created that key.
        usage_30d:
          type: integer
          description: 30-day tile fetch count. Reported as 0 today; usage aggregation is coming.
    TileGrantCreateResponse:
      type: object
      x-motionworks-status: production
      properties:
        grant_id:
          type: string
          description: Opaque ULID. Treat as a secret.
        tile_json_url:
          type: string
          format: uri
          description: |
            Capability URL — drop this into MapLibre as the `url` for a vector source. The opaque `grant_id` segment IS the credential, so treat the URL like an AWS S3 presigned URL.
        tilesets:
          type: array
          items:
            type: string
          description: Tileset slugs the grant covers.
        allowed_origins:
          type: array
          items:
            type: string
          description: Browser origins allowed to fetch TileJSON and tiles with this grant.
        expires_at:
          type: string
          format: date-time
          description: When the grant expires (ISO 8601).
        created_at:
          type: string
          format: date-time
          description: When the grant was created (ISO 8601).
    TileJsonManifest:
      type: object
      x-motionworks-status: production
      description: |
        TileJSON 3.0.0 manifest with a freshly-minted 24-hour tile-token embedded in `tiles[]`. Standard TileJSON consumers (MapLibre, Mapbox GL JS, deck.gl `MVTLayer`) handle the rest transparently.
      properties:
        tilejson:
          type: string
          example: 3.0.0
          description: TileJSON specification version (`3.0.0`).
        name:
          type: string
          description: Name of the tileset.
        tiles:
          type: array
          items:
            type: string
            format: uri
          description: Tile URL templates with the tile token already embedded. Your map library substitutes `{z}/{x}/{y}`.
        minzoom:
          type: integer
          description: Lowest zoom level served.
        maxzoom:
          type: integer
          description: Highest zoom level served.
        bounds:
          type: array
          items:
            type: number
          minItems: 4
          maxItems: 4
          description: Geographic extent `[west, south, east, north]` in WGS84.
        attribution:
          type: string
          description: Attribution text to show on the map.
        vector_layers:
          type: array
          items:
            type: object
          description: The MVT layers in the tiles and their field schemas.
        x-mw:
          type: object
          description: 'Motionworks-specific metadata: the grant, the layers served, token expiry and refresh guidance.'
          properties:
            grant_id:
              type: string
              description: The grant this manifest was minted for.
            product:
              type: string
              enum:
                - placecast
              description: Always `placecast`.
            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:
  /placecast/profiles/{place_id}:
    get:
      operationId: getPlacecastProfile
      summary: Read a place's visit profile
      description: |
        Returns the Placecast Profile for one place: visits, dropoffs, pass-bys and stays, dwell time, visit frequency, unique visitors, the confidence interval on visits, and a reliability assessment against the place's peer group. Each publication is a rolling estimate identified by its `published_date`; you get the most recent publication unless you pass `published_date`.

        Authenticate with an org-scoped API key. 20 credits per call.

        **Availability:** not yet live — calls currently return 501 `NOT_YET_AVAILABLE`.
      tags:
        - Placecast
      x-credit-cost: 20
      x-motionworks-status: production
      x-motionworks-source-doc: https://docs.mworks.com/docs/placecast-profiles
      parameters:
        - name: place_id
          in: path
          required: true
          schema:
            type: integer
          description: Motionworks place identifier.
        - name: published_date
          in: query
          schema:
            type: string
            format: date
          description: Publication to return (ISO 8601 date). Defaults to the most recent publication.
      responses:
        '200':
          description: The place's profile.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PlacecastProfile'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '404':
          description: Unknown `place_id`.
  /placecast/select:
    post:
      operationId: createPlacecastSelection
      summary: Submit a Placecast Select scenario
      tags:
        - Placecast
      description: |
        Submits an on-demand scenario: one or more places (a place set) and a date-time window in the places' local time zone, plus an optional list of segments to include and an optional custom dwell threshold. Motionworks measures every activity at those places inside the window and returns the activity profile for exactly that scenario.

        The scenario is accepted immediately with `status: pending` and processed in the background — poll `GET /v2/placecast/select/{selection_id}` until `status` is `complete` (or `failed`). Results appear on the scenario record when it completes.

        Authenticate with an org-scoped API key. 2,000 credits per scenario.

        **Availability:** not yet live — calls currently return 501 `NOT_YET_AVAILABLE`.
      x-credit-cost: 2000
      x-motionworks-status: production
      x-motionworks-source-doc: https://docs.mworks.com/docs/placecast-select
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlacecastSelectRequest'
      responses:
        '202':
          description: Scenario accepted; poll for results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PlacecastSelection'
                  meta:
                    $ref: '#/components/schemas/Meta'
    get:
      operationId: listPlacecastSelections
      summary: List your organization's Select scenarios
      tags:
        - Placecast
      description: |
        Returns the Select scenarios your organization has submitted, with cursor pagination. Filter by `status` to see only pending or completed scenarios. Authenticate with an org-scoped API key. 1 credit per call.

        **Availability:** not yet live — calls currently return 501 `NOT_YET_AVAILABLE`.
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - pending
              - processing
              - complete
              - failed
          description: 'Return only scenarios in this state: `pending`, `processing`, `complete` or `failed`.'
        - name: cursor
          in: query
          schema:
            type: string
          description: Cursor from the previous page's `pagination.cursor`. Omit for the first page.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: Scenarios per page (default 25, at most 100).
      responses:
        '200':
          description: A page of scenarios.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PlacecastSelection'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  meta:
                    $ref: '#/components/schemas/Meta'
  /placecast/select/{selection_id}:
    get:
      operationId: getPlacecastSelection
      summary: Poll a Select scenario
      tags:
        - Placecast
      description: |
        Returns one scenario by `selection_id` with its status and, once `status` is `complete`, its results. Poll this endpoint after submitting a scenario. Authenticate with an org-scoped API key. 1 credit per call.

        **Availability:** not yet live — calls currently return 501 `NOT_YET_AVAILABLE`.
      x-credit-cost: 1
      x-motionworks-status: production
      parameters:
        - name: selection_id
          in: path
          required: true
          schema:
            type: string
          description: The `selection_id` returned when the scenario was submitted.
      responses:
        '200':
          description: The scenario, with results when complete.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PlacecastSelection'
                  meta:
                    $ref: '#/components/schemas/Meta'
  /placecast/tiles/tilesets:
    get:
      tags:
        - Tile Discovery
      operationId: listPlacecastTilesets
      summary: List the tilesets Placecast publishes
      description: |
        Returns the public tile catalog for Placecast: 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-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'
  /placecast/tiles/grants:
    post:
      tags:
        - Tile Grants
      operationId: createPlacecastTileGrant
      summary: Mint a Placecast vector-tile grant
      description: |
        Creates a tile-delivery grant for your organization and returns its capability `tile_json_url`. Hand that URL to MapLibre, Mapbox GL JS or any TileJSON-aware client and it starts rendering. Free — grant management costs 0 credits; only tile traffic is metered.

        A grant covers the layers you list: `buildings` (building footprints of measured places) and `dca` (the District Collection Area polygon around each measured place), or both.

        Authenticate with an org-scoped API key (`X-API-Key: mw_…`) or a signed-in session (`Authorization: Bearer <jwt>`); anonymous callers cannot mint grants. Placecast tiles are licensed by arrangement — contact sales to enable them for your organization.
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TileGrantCreateRequest'
      responses:
        '201':
          description: Grant created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TileGrantCreateResponse'
                  meta:
                    $ref: '#/components/schemas/TileMeta'
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            Common cases:
              * `FEATURE_NOT_LICENSED` — your organization is not licensed for Placecast 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 Placecast 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.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                tile_layer_not_licensed:
                  summary: A tileset slug is not published by Placecast
                  value:
                    error:
                      code: TILE_LAYER_NOT_LICENSED
                      message: One or more tilesets are not published by Placecast.
                      status: 403
                      request_id: req_abc123
                      product: placecast
                      context:
                        missing:
                          - collection_areas
                        supported:
                          - buildings
                          - dca
    get:
      tags:
        - Tile Grants
      operationId: listPlacecastTileGrants
      summary: List your organization's grants
      description: Returns your organization's Placecast 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'
  /placecast/tiles/grants/{id}:
    get:
      tags:
        - Tile Grants
      operationId: getPlacecastTileGrant
      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/placecast/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.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /placecast/tiles/grants/{id}/revoke:
    post:
      tags:
        - Tile Grants
      operationId: revokePlacecastTileGrant
      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-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/placecast/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'
  /placecast/tiles/grants/{id}/tilejson:
    get:
      tags:
        - Tile TileJSON
      operationId: getPlacecastTileJson
      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.

        **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/placecast/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 Placecast vector tiles. Contact sales.
              * `TILE_GRANTS_REQUIRE_METERED` — tile delivery for your organization requires pay-as-you-go (metered) billing, which is not enabled. Contact sales to enable tile delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Grant not found, expired, or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /placecast/tiles/{layer}/{z}/{x}/{y}.mvt:
    get:
      tags:
        - Tile Data
      operationId: getPlacecastTileMvt
      summary: Fetch a single Placecast 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 Placecast 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; respect `Content-Encoding`. A tile with no features returns HTTP 204.

        Both layers are polygons and serve zoom 0–14. `buildings` carries one polygon per building footprint of a measured place, with the place id, place name, brand and building id. `dca` carries one District Collection Area polygon per measured place, with the place id, place name, DCA id and DCA type. Both always serve the latest published release, so the `X-MW-Snapshot` response header reads `static`.
      x-credit-cost: 1
      x-motionworks-status: production
      x-motionworks-source-doc: https://docs.mworks.com/docs/placecast-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:
              - buildings
              - dca
          description: |
            Tileset slug: `buildings` (building footprints of measured places) and `dca` (the District Collection Area polygon around each measured place). Both are polygon layers serving zoom 0–14. A slug the token does not cover returns 401 `layer_not_in_tilesets`.
        - name: z
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 14
          description: |
            Tile zoom level. Both Placecast layers serve z 0–14; 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 the tile bytes are gzip-encoded; decode accordingly.
            Vary:
              schema:
                type: string
                example: Accept-Encoding, Origin
            X-MW-Tileset:
              schema:
                type: string
              description: Resolved upstream tileset id (e.g. `placecast.buildings`).
            X-MW-Snapshot:
              schema:
                type: string
                format: date
              description: |
                Data snapshot the tile came from. Both Placecast layers always serve the latest published release and carry the literal `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, 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).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: |
            `error.code: ORIGIN_NOT_ALLOWED` — the request `Origin` is not in the grant's `allowed_origins` (`error.context.reason: origin_not_allowed`), or the request carried no `Origin` header (`origin_required`). Map libraries send `Origin` automatically; to test from curl or a native app, include the literal string `null` in the grant's `allowed_origins`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            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.) `error.context.upstream` is set when the tileset itself is temporarily unavailable; retry later.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
x-tagGroups:
  - name: Profiles & Select
    tags:
      - Placecast
  - name: Placecast Maptiles
    tags:
      - Tile Discovery
      - Tile Grants
      - Tile TileJSON
      - Tile Data
x-customer-voice: true
