# Read a forecast's latest run

Fetch a forecast with [`GET /questions/{question_id}`](/api-reference/questions/get_question_questions__question_id__get):

```shell
curl "https://api.mantic.com/questions/QUESTION_ID?show_forecasts=true" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

The latest run is `latest_forecast`; its fields depend on its `type`.

## When there is no run yet

`GET /questions/{question_id}` returns `404` until the forecast has a completed run, and `GET /questions` leaves it out. `GET /dashboards/{dashboard_id}/questions` includes it with `latest_forecast: null`.

## What the forecast says

These fields define what is being forecast and stay the same across runs:

| Field                       | Contains                                                                                          |
| --------------------------- | ------------------------------------------------------------------------------------------------- |
| `title`                     | The forecast's title, phrased as a question.                                                      |
| `declarative_title`         | The title as a statement, or `null`.                                                              |
| `clarifications`            | The criteria that determine how the forecast resolves, or `null`.                                 |
| `supplementary_information` | Background and context, or `null`.                                                                |
| `resolution`                | `null` until the forecast resolves; then `resolved_at`, `value` (a string or `null`) and `notes`. |
| `tags`                      | Tag names, or `null` when the forecast has none.                                                  |

## Event

Each run of an event forecast gives the probability that the event happens by `by`:

```json
{
  "id": 987,
  "date": "2026-09-21T08:30:00Z",
  "type": "event",
  "probability": 0.64,
  "by": "2026-12-31T23:59:59Z"
}
```

`probability` is a fraction, so `0.64` is 64%. `date` is when this run completed; `by` is the outcome deadline.

## Numerical

A numerical run gives percentiles: `percentiles["25"]` is the value the outcome has a 25% chance of being at or below.

```json
{
  "id": 988,
  "date": "2026-09-21T08:30:00Z",
  "type": "numerical",
  "percentiles": {
    "25": 3.2,
    "50": 3.8,
    "75": 4.5
  },
  "thresholds": null,
  "intervals": null
}
```

The median is `percentiles["50"]`. Ignore the deprecated `median_value` field and `p25`/`p75` keys; skip keys starting with `p` when you iterate. `thresholds` and `intervals` are currently always `null`; to get the probability above or below a value, see [Get the probability above or below a value](/read-forecasts/probability-thresholds).

## Date

A date run has the same shape, with timestamps as values:

```json
{
  "id": 989,
  "date": "2026-09-21T08:30:00Z",
  "type": "date",
  "percentiles": {
    "25": "2026-11-04T00:00:00Z",
    "50": "2026-12-12T00:00:00Z",
    "75": "2027-02-03T00:00:00Z"
  },
  "thresholds": null,
  "intervals": null
}
```

The median is `percentiles["50"]`; `median_date` is deprecated. A date distribution may not reach every percentile, so don't assume `25`, `50` and `75` are all present.

## Categorical

Each run of a categorical forecast gives one probability per labelled outcome:

```json
{
  "id": 990,
  "date": "2026-09-21T08:30:00Z",
  "type": "categorical",
  "categories": [
    { "label": "Hold", "probability": 0.55 },
    { "label": "Cut", "probability": 0.35 },
    { "label": "Raise", "probability": 0.10 }
  ]
}
```

Match categories by `label`, not by their position in the array.

## Redacted and unknown

```json
{
  "id": 991,
  "date": "2026-09-21T08:30:00Z",
  "type": "redacted"
}
```

`redacted` means the run exists but its values were left out: the request set `show_forecasts=false`, or omitted it and your organisation's default is off. Send `show_forecasts=true` to every endpoint that accepts it.

`unknown` is a run the public API can't represent. Treat it, and any type your code doesn't handle, like `redacted`: rely only on `id` and `date`.

## Handle every forecast type

This function turns any `latest_forecast`, including `null`, into a one-line headline:

```python
from typing import Any


def headline(forecast: dict[str, Any] | None) -> str:
    if forecast is None:
        return "No completed run yet"
    match forecast["type"]:
        case "event":
            return f"{forecast['probability']:.0%}"
        case "numerical" | "date":
            return str(forecast["percentiles"].get("50", "Median unavailable"))
        case "categorical":
            leader = max(forecast["categories"], key=lambda item: item["probability"])
            return f"{leader['label']}: {leader['probability']:.0%}"
        case "redacted" | "unknown":
            return "Forecast values unavailable"
        case other:
            return f"Unsupported forecast type: {other}"
```
