Read a forecast's latest run
Read a forecast's latest run by type, and the fields that define how the forecast resolves.
Fetch a forecast with GET /questions/{question_id}:
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:
{
"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.
{
"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.
Date
A date run has the same shape, with timestamps as values:
{
"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:
{
"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
{
"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:
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}"