openapi: 3.1.0
# ShowMeOnMap REST API — the blessed v1 surface (Move 5 of the codex
# playbook: the MCP tools wrap exactly these routes; anything an MCP tool
# can do, a plain HTTP caller can do with the same smo_live_ key).
#
# Maintained by hand from the route zod schemas — when a route's boundary
# changes, this file changes in the same PR. Served statically at
# https://showmeonmap.com/openapi.yaml.
info:
  title: ShowMeOnMap API
  version: 1.0.0
  description: |
    Build rendered, shareable maps from natural language, then refine and
    export them. Authentication uses per-account API keys (`smo_live_…`,
    minted in the web app under account menu → API keys); every call bills
    the key owner's prepaid credits at web-app rates — 1 credit per
    standard map or mutation, 3 per agent-grade map, reads and exports
    free, failed requests refunded automatically.

    Prefer tools over raw HTTP? The same surface is exposed as an MCP
    server (hosted at `POST /api/mcp`, or `@showmeonmap/mcp` on npm) —
    see https://showmeonmap.com/developers.
  termsOfService: https://showmeonmap.com/terms
  contact:
    url: https://showmeonmap.com/developers
servers:
  - url: https://showmeonmap.com
security:
  - apiKey: []
tags:
  - name: maps
    description: Create and refine maps
  - name: export
    description: Get data and share links out

paths:
  /api/workspace/new:
    post:
      tags: [maps]
      operationId: buildMap
      summary: Build a map from a natural-language query
      description: |
        The core verb. Send a plain-English request ("wildfire risk in
        California", "GDP per capita across Europe") and receive a full
        map plan plus the workspace that produced it. Costs 1 credit
        (3 with `useAgent: true`, which runs the multi-turn GIS agent for
        analysis-grade questions: site selection, spatial analysis, open
        data discovery). Refunded automatically when no map is delivered.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                  maxLength: 10000
                  description: The natural-language map request.
                executionMode:
                  type: string
                  enum: [fast, balanced]
                  default: fast
                  description: >
                    `balanced` allows longer model passes for harder
                    queries; `fast` (default) optimizes latency.
                useAgent:
                  type: boolean
                  default: false
                  description: >
                    Force the 5-credit multi-turn GIS agent. The server
                    also auto-selects the agent for agent-native intents
                    (satellite imagery, spatial analysis, site selection,
                    open data) regardless of this flag.
                stream:
                  type: boolean
                  default: false
                  description: >
                    When true the response is a `text/event-stream` of
                    progress events ending in a `final` frame whose
                    payload matches the JSON response shape.
                conversationHistory:
                  type: array
                  description: >
                    Prior turns for follow-up context ("filter those",
                    "same but for Texas"). Server-bounded: oldest turns
                    beyond internal length/character caps are dropped.
                  items:
                    type: object
                    required: [role, content]
                    properties:
                      role: { type: string, enum: [user, assistant] }
                      content: { type: string }
                currentMapView:
                  type: object
                  description: Optional current camera (center/zoom) for "near me / around here" queries.
                  additionalProperties: true
                userLocation:
                  type: object
                  description: Optional caller location (coords + place names) for location-relative queries.
                  additionalProperties: true
                workspace:
                  $ref: '#/components/schemas/Workspace'
                  description: >
                    Advanced — carry an existing workspace so this turn
                    composes onto it (continuity) instead of starting a
                    fresh map.
      responses:
        '200':
          description: >
            The result. `type: "visualization"` carries `mapPlan` (and
            usually `workspace`); `type: "message"` is a textual answer or
            typed decline; `type: "clarification"` asks a follow-up
            question and costs nothing further to answer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuildMapResponse'
            text/event-stream:
              schema:
                type: string
                description: >
                  When `stream: true` — `data:` frames of JSON events
                  (`reasoning`, `tool_call`, `tool_result`, `layer`,
                  `heartbeat`, `error`, `final`), each carrying a `stage`
                  label (`plan` / `fetch` / `analyze` / `render` /
                  `finalize`). The `final` frame's `response` field is a
                  `BuildMapResponse`.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/workspace/mutate:
    post:
      tags: [maps]
      operationId: mutateMap
      summary: Refine an existing map with a natural-language instruction
      description: |
        Send the current `workspace` (from a previous response) plus an
        instruction ("only show magnitude 5+", "make it a heatmap",
        "zoom to Texas"). Returns the updated workspace and map plan.
        1 credit per applied mutation; out-of-scope instructions return a
        typed no-op instead of guessing.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [instruction, workspace]
              properties:
                instruction:
                  type: string
                  description: The natural-language refinement.
                workspace:
                  $ref: '#/components/schemas/Workspace'
                rationale:
                  type: string
                  maxLength: 300
                  description: Optional one-line reason (used for audit/telemetry).
      responses:
        '200':
          description: Updated workspace + map plan, or a typed out-of-scope/clarification result.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/workspace/{id}:
    get:
      tags: [maps]
      operationId: getWorkspace
      summary: Fetch a saved workspace by id
      description: Free read. Returns the persisted workspace (owner-gated).
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The workspace.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  workspace: { $ref: '#/components/schemas/Workspace' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: 'Not the owner.' }
        '404': { description: 'No such workspace.' }

  /api/maps:
    post:
      tags: [export]
      operationId: saveMap
      summary: Save a map so it has a stable id and a shareable /m/ page
      description: |
        Free. Persists a build result under a stable `map_id`; the rendered
        share page lives at `https://showmeonmap.com/m/{map_id}`. The
        `blob` is the share snapshot — pass the `mapPlan` (and `query`)
        from a build response. Re-posting with the same `id` updates the
        stored map (owner-gated).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, blob]
              properties:
                id:
                  type: string
                  minLength: 1
                  maxLength: 64
                  description: Optional caller-chosen id (e.g. the workspace id) so links are stable across updates.
                title: { type: string }
                query: { type: string, description: 'The originating natural-language query.' }
                blob:
                  type: object
                  required: [mapPlan]
                  properties:
                    mapPlan: { $ref: '#/components/schemas/MapPlan' }
                    query: { type: string }
                    version: { type: integer, description: 'Snapshot format version; use 1.' }
                  additionalProperties: true
                isPublic:
                  type: boolean
                  description: Link-reachable share (unlisted). Listing in Discover is a separate owner action in the web app.
      responses:
        '200':
          description: Saved. The response includes the map's id — the `/m/{id}` share URL is derived from it.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400': { $ref: '#/components/responses/BadRequest' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/export/layer-data:
    post:
      tags: [export]
      operationId: exportLayerData
      summary: Download one layer's data as CSV or GeoJSON
      description: >
        Free. `map_id` is a saved map id (see saveMap); `layer_id` comes
        from the map plan's layers. Public maps export without auth; a
        private map exports only for its owner's API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [map_id, layer_id, format]
              properties:
                map_id: { type: string }
                layer_id: { type: string }
                format: { type: string, enum: [csv, geojson] }
      responses:
        '200':
          description: The file, as an attachment.
          content:
            text/csv: { schema: { type: string } }
            application/geo+json: { schema: { type: object, additionalProperties: true } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { description: 'Unknown map or layer.' }

  /api/export/ensure-shared:
    post:
      tags: [export]
      operationId: ensureShared
      summary: Ensure a saved map has a shareable /m/ page
      description: >
        Free. Idempotent — returns the share state for `map_id`; the
        rendered page lives at `https://showmeonmap.com/m/{map_id}`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [map_id]
              properties:
                map_id: { type: string, minLength: 1 }
      responses:
        '200':
          description: Share state (e.g. `{"status":"already_shared","map_id":"…"}`).
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { description: 'Unknown map.' }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: smo_live_…
      description: >
        Per-account API key from the web app (account menu → API keys).
        Shown once at mint; up to 10 active keys per account; revocation
        is immediate. All spend lands on the key owner's credit balance.
  schemas:
    Workspace:
      type: object
      description: >
        The append-only ops-log document a map is derived from. Treat it
        as opaque: pass back exactly what a previous response returned.
      additionalProperties: true
    MapPlan:
      type: object
      description: >
        The renderable map specification (layers, camera, basemap, legend,
        title, citations). Layer entries carry `id`, `title`, data and
        style; sources carry attribution. Shape is documented by example —
        fetch one and look — and is stable but intentionally not frozen
        field-by-field in this spec.
      additionalProperties: true
    BuildMapResponse:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [visualization, message, clarification, search_needed]
        message:
          type: string
          description: Human-readable summary, answer, or clarifying question.
        mapPlan:
          $ref: '#/components/schemas/MapPlan'
        workspace:
          $ref: '#/components/schemas/Workspace'
        memo:
          type: object
          description: Agent-written analysis memo (agent runs only).
          additionalProperties: true
        metadata:
          type: object
          description: Diagnostics — cache hits, degradation notices, capabilities.
          additionalProperties: true
      additionalProperties: true
    Error:
      type: object
      properties:
        error: { type: string }
        code:
          type: string
          description: Stable machine code (e.g. AUTH_REQUIRED, NO_CREDITS, RATE_LIMITED).
  responses:
    BadRequest:
      description: Malformed body (never billed — validation runs before any credit deduction).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: "Missing/invalid key (`code: AUTH_REQUIRED`)."
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PaymentRequired:
      description: "Not enough credits (`code: NO_CREDITS`). Buy a pack at https://showmeonmap.com/pricing."
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Too many requests — honor `Retry-After`. Throttled requests never spend credits.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
