> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mireye.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# POST /v1/ask

> Natural-language Q&A over US coordinates with per-citation provenance.

## When to use this

Use `/v1/ask` when the caller has a question phrased in natural language
("is this in a flood zone?", "what's the wildfire risk here?") and the
right answer depends on combining one or more fields with light
interpretation. The endpoint runs a two-model pipeline that picks fields
from the catalog, fetches them, and synthesizes the answer with citations.

Use [`/v1/fetch`](/api-reference/fetch) instead when the caller already
knows which fields they want, or when you are building a deterministic
workflow that should not pay LLM latency on every call.

## How it works

1. **Planner (Claude Haiku 4.5)** — receives the question, the
   coordinate, and the field catalog rendered into the system prompt.
   Returns a structured tool call naming the catalog fields it wants
   fetched (capped at 15 per question — note that preset expansions
   larger than the cap, e.g. `site_selection` or `data_center_siting`,
   are truncated to the first 15 fields of the preset). The planner system prompt is
   **prompt-cached** (catalog-sized), so steady-state input cost is \~90% below the
   uncached path.
2. **Fetch (deterministic, parallel)** — the orchestrator groups the
   selected fields by layer and dispatches one fan-out per layer. Each
   layer returns `Field_[T]` wrappers carrying value + provenance.
3. **Synthesizer (Claude Sonnet 4.6)** — receives the question and the
   rendered values, returns a structured tool call with the prose answer
   and the list of fields it actually used.
4. **Citation extraction (deterministic)** — walks `fields_used`, groups
   them by source, and emits one citation per source.

## Worked example: wildfire risk in rural Minnesota

Aitkin County, Minnesota — a rural parcel of open herbaceous cover and
wetlands. A small-business insurer wants a wildfire underwriting read.

```bash theme={null}
curl -s https://api.mireye.com/v1/ask \
  -H "Authorization: Bearer $MIREYE_API_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "lat": 46.6,
    "lng": -93.7,
    "question": "What is the wildfire risk at this location and what fuel is on the ground?"
  }' | jq
```

```json theme={null}
{
  "lat": 46.6,
  "lng": -93.7,
  "question": "What is the wildfire risk at this location and what fuel is on the ground?",
  "answered_at": "2026-06-12T07:31:33.971740+00:00",
  "answer": "Wildfire risk at this location (46.6, -93.7) is LOW. The land cover is classified as Grass/Forb/Herb with 0.0% tree canopy cover, meaning there is no forested fuel structure present — the primary surface fuel is herbaceous/grassland material. The terrain is essentially flat at 0.05 degrees slope, which provides no topographic amplification of fire spread. Current vegetation greenness is low (NDVI 0.18), suggesting sparse or stressed herbaceous cover, which could carry fire under dry conditions, but the absence of woody fuels and dense canopy significantly limits fire intensity and continuity.\n\nOne notable concern is the severe 5-year NDVI decline of -0.49, indicating substantial vegetation loss over the past five years. This level of change warrants investigation into whether drought, land conversion, or prior disturbance has left dead or dry fine fuels on the ground, which could temporarily elevate short-range fire risk. However, without tree canopy or shrub structure, this location does not present the elevated wildfire risk profile associated with forested or WUI (Wildland-Urban Interface) settings.",
  "confidence": "medium",
  "citations": [
    {
      "source": "COPERNICUS_S2_SR_HARMONIZED",
      "source_url": "https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_SR_HARMONIZED",
      "fields": [
        "ndvi_change_5y",
        "ndvi_current"
      ],
      "fetched_at": "2026-06-12T07:28:10.065940+00:00",
      "confidence": "high"
    },
    {
      "source": "USFS_LCMS",
      "source_url": "https://developers.google.com/earth-engine/datasets/catalog/USFS_GTAC_LCMS_v2024-10",
      "fields": [
        "lcms_class"
      ],
      "fetched_at": "2026-06-12T07:28:08.500290+00:00",
      "confidence": "high"
    },
    {
      "source": "USGS_3DEP_COG",
      "source_url": "s3://prd-tnm/StagedProducts/Elevation/13/TIFF/current/n47w094/USGS_13_n47w094.tif",
      "fields": [
        "slope_degrees"
      ],
      "fetched_at": "2026-06-12T07:31:39.962998+00:00",
      "confidence": "medium"
    },
    {
      "source": "USGS_EPQS",
      "source_url": "https://epqs.nationalmap.gov/v1/json?x=-93.7&y=46.6&wkid=4326&units=Meters",
      "fields": [
        "elevation"
      ],
      "fetched_at": "2026-06-12T07:31:40.667956+00:00",
      "confidence": "high"
    },
    {
      "source": "USGS_NLCD_TCC_V2023_5",
      "source_url": "https://developers.google.com/earth-engine/datasets/catalog/USGS_NLCD_RELEASES_2023_REL_TCC_v2023-5",
      "fields": [
        "tree_canopy_pct"
      ],
      "fetched_at": "2026-06-12T07:28:10.065891+00:00",
      "confidence": "high"
    }
  ],
  "fields_used": [
    "elevation",
    "lcms_class",
    "ndvi_change_5y",
    "ndvi_current",
    "slope_degrees",
    "tree_canopy_pct"
  ]
}
```

The planner here expanded the `wildfire_underwrite` preset
(`preset_expanded` in the trace) — exactly its six fields. Its visible
reasoning weighed adding `land_use_class` and `cdl_class` for the
"fuel on the ground" angle and judged the preset's signals sufficient.
Note the `medium` answer confidence: all six fields returned (so no
calibration downgrade fired), but the slope citation carries `medium`
source confidence and the answer itself flags the −0.49 five-year NDVI
decline as the open question — the synthesizer's self-report is bounded
accordingly.

## Confidence calibration

`confidence` is a three-bucket string: `high`, `medium`, `low`. The
synthesizer self-reports a bucket, then a deterministic calibration step
bounds its optimism:

| Bucket   | Meaning                                                                                            |
| -------- | -------------------------------------------------------------------------------------------------- |
| `high`   | All planner-selected fields fetched cleanly; sources are direct (USGS, NOAA, USFS) and current.    |
| `medium` | One or more selected fields downgraded (e.g., NDVI is satellite-derived) OR some fields nullified. |
| `low`    | Substantial nulls/failures among selected fields; synthesizer answered with partial data.          |

The null downgrade is automatic: if **more than 30%** of the
planner-selected fields came back null, the response `confidence` drops
one bucket (`high` → `medium`, `medium` → `low`) regardless of what the
synthesizer self-reported.

## Latency and cost

`/v1/ask` is LLM-backed and **much slower than `/v1/fetch`** — it runs a serial
plan → fetch → synthesize pipeline. **Set your client timeout to at least 120
seconds** (or use [streaming](#streaming-responses)). A 30 s timeout will
intermittently abort otherwise-successful requests — and the request keeps
running, and billing, on the server after your client gives up.

| stage      | typical  | tail         | hard bound                        |
| ---------- | -------- | ------------ | --------------------------------- |
| end-to-end | \~6–20 s | up to \~90 s | **110 s**, then `504 ask_timeout` |

* **Steady-state warm path**: \~6–15 s — single-field questions at the low end,
  multi-field synthesis at the high end. Planner 2–6 s, fetch fan-out
  sub-second when the layer cache is warm, synthesizer 3–8 s.
* **Tail latency comes from the fetch stage, not the models.** A question that
  selects a slow source lets that source's own budget dominate: Earth Engine
  fields allow up to 60 s, road/building fields 30–35 s. One such field can push
  a single request well past 30 s while the models stay fast — expected, not a
  failure.
* **Deadline**: requests are bounded at **110 seconds** end-to-end — past that
  you get a structured `504 ask_timeout` (carrying a `Retry-After` header)
  instead of a hang. Per-stage timeouts inside the pipeline: planner 20 s,
  synthesizer 30 s, one SDK retry.
* **Cold start**: the hosted deploy keeps machines running, but the first
  requests after a deploy can be slower while geospatial sources warm in the
  background (`/readyz` reports warm state). Self-hosted instances pay this on
  every boot.
* **Cache savings**: the planner system prompt is catalog-sized; cache reads land
  at a \~90% input-token discount. Per-question incremental cost is dominated by
  synthesizer output (\~500 tokens at Sonnet rates).

## Streaming responses

For latency-sensitive or interactive clients, `POST /v1/ask/stream` streams the
answer over [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
so you get **first tokens in \~5–7 s** (planner + fetch) instead of awaiting the
whole synthesis. It takes the **same request body** as `/v1/ask` and enforces the
**same 110 s deadline** and error codes.

Frames arrive as named SSE events:

| event   | payload                                                                                   | count                 |
| ------- | ----------------------------------------------------------------------------------------- | --------------------- |
| `delta` | `{"text": "..."}` — the next chunk of answer prose                                        | zero or more          |
| `final` | the complete `/v1/ask` body (answer, confidence, citations, fields\_used, optional trace) | exactly one, terminal |
| `error` | `{"error", "message", "retryable"}` — a mid-stream failure                                | at most one, terminal |

```bash theme={null}
curl -sN https://api.mireye.com/v1/ask/stream \
  -H "Authorization: Bearer $MIREYE_API_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"lat": 46.6, "lng": -93.7, "question": "Wildfire risk here?"}'
```

Failures **before the first byte** (planner error, connection error, deadline,
or an at-capacity `ask_busy`) surface as a normal HTTP status; once the `200`
body has started, a later failure can only arrive as a terminal `error` frame.
Consume the `final` frame for the authoritative body — the `delta` stream is a
progressive preview of the same prose.

## Including the trace

Pass `"include_trace": true` to get planner-internal diagnostics:

```json theme={null}
"trace": {
  "planner_model": "claude-haiku-4-5",
  "synthesizer_model": "claude-sonnet-4-6",
  "planner_reasoning": "The user is asking about wildfire risk and ground fuel at this location. This is a broad wildfire underwriting/risk assessment question that combines multiple factors: land cover/fuel type (lcms_class, tree_canopy_pct), vegetation health and change (ndvi_current, ndvi_change_5y), and terrain (slope_degrees, elevation). This matches the wildfire_underwrite preset directly. Additionally, for \"fuel on the ground\" context, land_use_class and cdl_class could add detail, but the preset already covers the primary fuel signals.",
  "fields_requested": [
    "lcms_class",
    "tree_canopy_pct",
    "ndvi_current",
    "ndvi_change_5y",
    "slope_degrees",
    "elevation"
  ],
  "preset_expanded": "wildfire_underwrite",
  "latency_ms": {
    "planner_ms": 5977,
    "fetch_ms": 719,
    "synth_ms": 7504,
    "total_ms": 14200
  },
  "cache_creation_input_tokens": 431,
  "cache_read_input_tokens": 11767
}
```

The `cache_read_input_tokens` field is the load-bearing signal that the
planner system prompt cached correctly (11,767 tokens measured live
against the 0.5.0 catalog; the cached prompt scales with the catalog —
roughly 19 K tokens at 0.6.0, since the rendered field catalog dominates
the prompt).
First-ever calls show the same number under
`cache_creation_input_tokens` instead.

## Errors

Beyond the [common error codes](/api-reference/errors), `/v1/ask` maps
LLM-upstream failures to structured codes — all with the
`{"detail": {"error", "message", "retryable"}}` shape:

| `error`                     | HTTP | Meaning                                                                       | Retryable                              |
| --------------------------- | ---- | ----------------------------------------------------------------------------- | -------------------------------------- |
| `ask_busy`                  | 429  | The fleet-wide `/v1/ask` concurrency cap is saturated; no model work started. | yes                                    |
| `ask_timeout`               | 504  | The request exceeded the 110 s end-to-end deadline.                           | yes                                    |
| `ask_upstream_rate_limited` | 429  | The LLM API rate-limited the call.                                            | yes                                    |
| `ask_upstream_unreachable`  | 502  | Could not reach the LLM API (connection error).                               | yes                                    |
| `ask_upstream_error`        | 502  | The LLM API returned an error.                                                | only when the upstream error was a 5xx |

Honor `retryable` rather than the status code: a `502 ask_upstream_error`
caused by an upstream 4xx will not succeed on retry. Retryable failures
(`ask_busy`, `ask_timeout`, and the upstream `429`/`502`s) carry a
**`Retry-After`** response header in seconds — wait at least that long before
retrying, and prefer exponential backoff on repeats.

## Partial-failure handling

`/v1/ask` is resilient to source failures. If 2 of 5 planner-selected
fields fail (timeout, source returned null, layer crashed), the
synthesizer still answers with the 3 that came back, the response
`confidence` drops one bucket (see the >30% rule above), and the answer
prose notes the gap ("…tree canopy was unavailable; assessment based on
LCMS class alone…"). Failed fields are not silently hidden: diff the
trace's `fields_requested` against the top-level `fields_used` to see
exactly which planner-selected fields the answer could not use. For
field-level failure records (`source`, `error`, `retryable`), make the
same request via [`/v1/fetch`](/api-reference/fetch), which returns a
structured `partial_failures` array.

Every `/v1/ask` response also carries a top-level **`data_gaps`** array: the
requested fields that resolved to no value, each with the fetch layer's reason.
It is computed from the fetched result itself — not from the prose — so it is
authoritative missing-data you can read next to the answer without trusting the
wording or diffing `fields_requested` against `fields_used`:

```json theme={null}
"data_gaps": [
  {
    "field": "parcel_zoning",
    "reason": "source returned null: no Regrid parcel at this point"
  }
]
```

`data_gaps` is `[]` when every requested field returned a value, and it appears
on both the buffered response and the streaming `final` frame.


## OpenAPI

````yaml POST /v1/ask
openapi: 3.1.0
info:
  title: Mireye Earth
  description: >-
    Provenance-tagged geospatial data for US coordinates. POST /v1/fetch for
    deterministic field values; POST /v1/ask for natural-language Q&A; GET
    /v1/meta/fields for the catalog.
  version: 0.14.0
servers: []
security: []
paths:
  /v1/ask:
    post:
      summary: Natural-language Q&A over a US coordinate (LLM-backed)
      description: >-
        Plans which catalog fields to fetch, fetches them, and synthesizes a
        cited answer. Much slower than `/v1/fetch`: typically ~6-20 s,
        hard-bounded at 110 s (then a `504 ask_timeout`). **Set a client timeout
        of at least 120 s.** For progressive output (first tokens ~5-7 s) use
        `POST /v1/ask/stream`. Returns `429 ask_busy` when the fleet-wide
        concurrency cap is saturated; retryable failures carry a `Retry-After`
        header.
      operationId: ask_v1_ask_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AskRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
components:
  schemas:
    AskRequest:
      properties:
        lat:
          type: number
          title: Lat
        lng:
          type: number
          title: Lng
        question:
          type: string
          maxLength: 2000
          minLength: 1
          title: Question
        include_trace:
          type: boolean
          title: Include Trace
          default: false
      type: object
      required:
        - lat
        - lng
        - question
      title: AskRequest
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError

````