# Prolepsia API

Use the Prolepsia API to create questions, start forecasts and read the results from your own code. Every example on this page comes in TypeScript, Python and curl.

- **Base URL:** `https://app.prolepsia.com/api/v1/external`
- **Format:** JSON in and out, UTF-8. File uploads use `multipart/form-data`.
- **Size:** 126 operations, all listed at the end of this page.
- **Try it in the browser:** [interactive API reference](https://app.prolepsia.com/api/external/docs/) · [OpenAPI schema](https://app.prolepsia.com/api/external/schema/)

> **A forecast takes from about 5 minutes to about 1.5 hours, depending on its run mode.** Starting one returns a task id within a second. The forecast keeps running on our servers after your request ends. Save the task id, then check on it every few minutes or let a [webhook](https://app.prolepsia.com/docs/developers/webhooks) tell you when it is done.

## Before you start

### Create an API key

1. In Prolepsia, open **Settings** and choose **API and AI clients**.
2. Under **API keys**, choose **Create new key**.
3. Name the key, tick the permissions it needs, pick when it expires and choose **Create key**.
4. Copy the key straight away. It starts with `pre_`, and Prolepsia shows it only once.

Keep the key on your server. The API sends no CORS headers, so a web page on another site cannot call it, and a key in browser code would leak.

### Permissions

A key can do only what its permissions (scopes) allow, and never more than its owner can do in the app.

| Scope | Lets the key |
| --- | --- |
| `questions:read` | Read questions, their tags, context notes and files, question subscriptions and question templates. |
| `questions:write` | Create, edit and import questions, draft questions with AI, manage notes, files, share links, subscriptions and question templates. |
| `forecasts:read` | Read forecasts and their status, your forecast chats, schedules, watches, forecast presets, webhooks and webhook deliveries. Check your credit balance. |
| `forecasts:write` | Start and cancel forecasts, chat about a forecast, manage schedules, watches, forecast presets and webhooks, and test or replay webhook deliveries. |
| `connections:read` | List your connected MCP servers and the tools they offer. Keys and sign-ins are never returned. |
| `connections:write` | Add, change, test and delete connected MCP servers, and start their sign-in. |

Most integrations need the first four. Add the connection scopes only if the key manages connected servers. Sending `context` notes along with a new forecast needs `questions:write` as well as `forecasts:write`.

### Credits

Every forecast costs credits. A forecast started through the API runs in your organization's default run mode. Check your balance and the prices with `GET /credits`. When the balance is too low, starting a forecast answers `409` and nothing is charged.

## Make your first call

Send your API key in the `Authorization` header, as `Authorization: Bearer pre_...`. The `X-Api-Key: pre_...` header works too.

Set two environment variables, then check the key by reading your credit balance:

```bash
export PROLEPSIA_API_KEY="pre_..."
export PROLEPSIA_BASE_URL="https://app.prolepsia.com/api/v1/external"
```

The TypeScript and Python examples on this page reuse the small helper below. It sends the key, turns JSON into objects and raises an error with the API's message when a call fails. The TypeScript version runs on Node.js 20 or later, Bun and Deno with no packages. The Python version needs `requests` (`pip install requests`).

```ts
const BASE_URL =
  process.env.PROLEPSIA_BASE_URL ?? "https://app.prolepsia.com/api/v1/external";
const API_KEY = process.env.PROLEPSIA_API_KEY ?? "";

/** Call the Prolepsia API and return the JSON answer. */
async function prolepsia<T = any>(
  path: string,
  init: { method?: string; body?: unknown; headers?: Record<string, string> } = {},
): Promise<T> {
  const isForm = init.body instanceof FormData;
  const response = await fetch(`${BASE_URL}${path}`, {
    method: init.method ?? (init.body === undefined ? "GET" : "POST"),
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      ...(init.body === undefined || isForm
        ? {}
        : { "Content-Type": "application/json" }),
      ...init.headers,
    },
    body: isForm
      ? (init.body as FormData)
      : init.body === undefined
        ? undefined
        : JSON.stringify(init.body),
  });
  if (response.status === 204) return undefined as T;
  const data = await response.json();
  if (!response.ok) {
    throw new Error(
      `${response.status} ${data.error.type}: ${data.error.message}`,
    );
  }
  return data as T;
}

const credits = await prolepsia("/credits");
console.log(credits.balance, credits.unlimited);
```

```python
import os

import requests

BASE_URL = os.environ.get(
    "PROLEPSIA_BASE_URL", "https://app.prolepsia.com/api/v1/external"
)
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['PROLEPSIA_API_KEY']}"


def prolepsia(method: str, path: str, **kwargs):
    """Call the Prolepsia API and return the JSON answer."""
    response = session.request(method, f"{BASE_URL}{path}", timeout=60, **kwargs)
    if response.status_code == 204:
        return None
    data = response.json()
    if not response.ok:
        error = data["error"]
        raise RuntimeError(f"{response.status_code} {error['type']}: {error['message']}")
    return data


credits = prolepsia("GET", "/credits")
print(credits["balance"], credits["unlimited"])
```

```bash
curl -sS "$PROLEPSIA_BASE_URL/credits" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY"
```

The answer:

```json
{
  "balance": 240,
  "unlimited": false,
  "topUps": [],
  "prices": [
    { "key": "forecast.custom_plan", "credits": 100 },
    { "key": "forecast.deep", "credits": 150 },
    { "key": "forecast.light", "credits": 2 },
    { "key": "forecast.max", "credits": 300 },
    { "key": "forecast.moderate", "credits": 10 },
    { "key": "forecast.standard", "credits": 100 },
    { "key": "monitoring.question_month", "credits": 5 }
  ],
  "plan": {
    "key": "pro",
    "name": "Pro",
    "status": "active",
    "currentPeriodEnd": "2026-11-01T12:00:00.000Z",
    "cancelAtPeriodEnd": false,
    "creditsPerPeriod": 135
  }
}
```

`plan` is your organization's plan. Its `key` is `payg` (Pay as you go),
`pro`, `team` or `enterprise`. `currentPeriodEnd` is the end of the paid
period, which is the renewal date unless `cancelAtPeriodEnd` is `true`. On
Pay as you go it is `null` and `creditsPerPeriod` is `0`. `status` is
`past_due` when the last renewal payment failed: the plan stays on and adds no
new monthly credits until a retry works or the plan ends after seven days.

A missing or wrong key answers `401` with the error type `invalid_api_key`.

## Forecast a question, step by step

### 1. Create the question

A question needs a `type` and a `title`. Write the `resolutionCriteria` with care: they decide what the forecast means. The [question types](#question-types) section lists the fields for each type.

```ts
const question = await prolepsia("/questions", {
  body: {
    type: "binary",
    title: "Will the Bank of England cut Bank Rate at its December 2026 meeting?",
    description:
      "The Monetary Policy Committee announces its decision on 17 December 2026.",
    resolutionCriteria:
      "Resolves YES if the Bank of England announces a cut to Bank Rate on 17 December 2026. Resolves NO otherwise.",
    tags: ["rates"],
  },
});
console.log(question.id);
```

```python
question = prolepsia(
    "POST",
    "/questions",
    json={
        "type": "binary",
        "title": "Will the Bank of England cut Bank Rate at its December 2026 meeting?",
        "description": "The Monetary Policy Committee announces its decision on 17 December 2026.",
        "resolutionCriteria": "Resolves YES if the Bank of England announces a cut to Bank Rate on 17 December 2026. Resolves NO otherwise.",
        "tags": ["rates"],
    },
)
print(question["id"])
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/questions" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "binary",
    "title": "Will the Bank of England cut Bank Rate at its December 2026 meeting?",
    "description": "The Monetary Policy Committee announces its decision on 17 December 2026.",
    "resolutionCriteria": "Resolves YES if the Bank of England announces a cut to Bank Rate on 17 December 2026. Resolves NO otherwise.",
    "tags": ["rates"]
  }'
```

The answer is the saved question, with status `201`:

```json
{
  "id": "2c54dab4-f4e6-439d-a35d-1b7c71205259",
  "type": "binary",
  "title": "Will the Bank of England cut Bank Rate at its December 2026 meeting?",
  "description": "The Monetary Policy Committee announces its decision on 17 December 2026.",
  "resolutionCriteria": "Resolves YES if the Bank of England announces a cut to Bank Rate on 17 December 2026. Resolves NO otherwise.",
  "finePrint": "",
  "options": [],
  "lowerBound": null,
  "upperBound": null,
  "openLowerBound": false,
  "openUpperBound": false,
  "scaleType": null,
  "callsign": "HYGROMETER-PLANET-MARGIN",
  "optionsAreMutuallyExclusive": true,
  "optionsAreComprehensive": true,
  "visibility": "private",
  "createdVia": "api",
  "tags": [{ "name": "rates", "slug": "rates" }],
  "createdAt": "2026-10-12T07:45:41.413Z",
  "updatedAt": "2026-10-12T07:45:41.413Z"
}
```

### 2. Start a forecast

Start a forecast with `POST /forecasts` and the `questionId`. Send an `Idempotency-Key` header with a value you choose, such as a UUID. If the answer gets lost and you send the same request again with the same key, Prolepsia returns the first answer instead of starting a second paid forecast.

```ts
const task = await prolepsia("/forecasts", {
  body: { questionId: question.id },
  headers: { "Idempotency-Key": crypto.randomUUID() },
});
console.log(task.id, task.status); // "queued"
```

```python
import uuid

task = prolepsia(
    "POST",
    "/forecasts",
    json={"questionId": question["id"]},
    headers={"Idempotency-Key": str(uuid.uuid4())},
)
print(task["id"], task["status"])  # "queued"
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/forecasts" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: boe-december-2026-run-1" \
  -d '{"questionId": "2c54dab4-f4e6-439d-a35d-1b7c71205259"}'
```

The answer has status `202` and a forecast task:

```json
{
  "id": "073193c3-bc0b-4218-b7a9-c2b54143b3d3",
  "type": "forecast_task",
  "status": "queued",
  "createdAt": "2026-10-12T07:45:46.551Z",
  "startedAt": null,
  "finishedAt": null,
  "question": { "id": "2c54dab4-f4e6-439d-a35d-1b7c71205259", "type": "binary", "title": "Will the Bank of England cut Bank Rate at its December 2026 meeting?" },
  "pollingUrl": "/api/v1/external/forecasts/073193c3-bc0b-4218-b7a9-c2b54143b3d3",
  "progressMessage": "Queued for forecast generation.",
  "partial": true,
  "completedSubforecastCount": 0,
  "plannedSubforecastCount": 3,
  "subforecasts": [],
  "creditsCharged": 0,
  "creditsRefunded": 0,
  "runMode": "moderate"
}
```

The `question` object in the answer holds every question field. It is shortened here.

Add `runMode` to choose how deep the forecast goes, as the run mode picker in the app does: `light`, `moderate`, `standard`, `deep` or `max`. A deeper mode uses stronger models and more research, takes longer and costs more credits. `GET /run-modes` lists each mode's rough time and price, and the mode a forecast gets when you leave `runMode` out:

```bash
curl -sS "$PROLEPSIA_BASE_URL/run-modes" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY"

curl -sS -X POST "$PROLEPSIA_BASE_URL/forecasts" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: boe-december-2026-deep-1" \
  -d '{"questionId": "2c54dab4-f4e6-439d-a35d-1b7c71205259", "runMode": "deep"}'
```

You can also skip step 1 and send the question inline: `{"question": {"type": "binary", "title": "...", ...}}`. Send `question` or `questionId`, not both. A question can have only one forecast running at a time; starting another answers `409` with the error type `forecast_already_running` and the running task's id in `error.activeTaskId`.

### 3. Wait for the result

Read the task with `GET /forecasts/{taskId}` until its `status` is one of the three final values: `completed`, `failed` or `cancelled`. While the task is still running, the answer carries a `Retry-After` header with the number of seconds to wait: 15 while queued and 120 while running. A [webhook](https://app.prolepsia.com/docs/developers/webhooks) saves you the polling.

| Status | Meaning |
| --- | --- |
| `queued` | Waiting to start. |
| `running_subforecasts` | Several separate forecasts, called subforecasts, are researching the question at the same time. |
| `synthesizing` | The subforecasts are being combined into one answer. |
| `completed` | Final. The answer is in `forecast`. |
| `failed` | Final. Nothing to read except the subforecasts that finished. `error.message` says what happened. |
| `cancelled` | Final. Someone cancelled it. |

```ts
const FINAL = new Set(["completed", "failed", "cancelled"]);

async function waitForForecast(taskId: string) {
  for (;;) {
    const response = await fetch(`${BASE_URL}/forecasts/${taskId}`, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    const task = await response.json();
    if (!response.ok && response.status !== 429) {
      throw new Error(`${task.error.type}: ${task.error.message}`);
    }
    if (response.ok && FINAL.has(task.status)) return task;
    const seconds = Number(response.headers.get("Retry-After") ?? "120");
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}

const finished = await waitForForecast(task.id);
console.log(finished.status, finished.forecast?.forecastData);
```

```python
import time

FINAL = {"completed", "failed", "cancelled"}


def wait_for_forecast(task_id: str) -> dict:
    while True:
        response = session.get(f"{BASE_URL}/forecasts/{task_id}", timeout=60)
        if response.status_code != 429:
            response.raise_for_status()
            task = response.json()
            if task["status"] in FINAL:
                return task
        time.sleep(int(response.headers.get("Retry-After", "120")))


finished = wait_for_forecast(task["id"])
print(finished["status"], finished.get("forecast", {}).get("forecastData"))
```

```bash
curl -sS -i "$PROLEPSIA_BASE_URL/forecasts/073193c3-bc0b-4218-b7a9-c2b54143b3d3" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY"
```

A response that says `partial: true` has no combined answer yet. Never report a subforecast's number as the forecast: subforecasts disagree with each other on purpose, and the final step settles the answer.

### 4. Read the result

A completed task has the answer in `forecast`:

```json
{
  "id": "2e1cc0b7-fa66-4ad4-a583-6da0e1f07ac7",
  "type": "forecast_task",
  "status": "completed",
  "createdAt": "2026-10-12T07:46:35.968Z",
  "startedAt": "2026-10-12T07:46:36.120Z",
  "finishedAt": "2026-10-12T08:31:02.114Z",
  "question": { "id": "2c54dab4-f4e6-439d-a35d-1b7c71205259", "type": "binary", "title": "Will the Bank of England cut Bank Rate at its December 2026 meeting?" },
  "pollingUrl": "/api/v1/external/forecasts/2e1cc0b7-fa66-4ad4-a583-6da0e1f07ac7",
  "progressMessage": "Forecast generation completed.",
  "forecast": {
    "id": "695142eb-203f-415b-81c7-ccaf2747b0a6",
    "generatedAt": "2026-10-12T08:29:44.984Z",
    "forecastData": {
      "format": "probability",
      "payload": { "probability": 0.34 },
      "questionType": "binary"
    },
    "report": {
      "status": "completed",
      "short": "Prolepsia puts the chance of a cut in December at 34%. ...",
      "medium": "## Summary\n...",
      "detailed": "## Summary\n..."
    },
    "sources": [
      {
        "url": "https://www.bankofengland.co.uk/monetary-policy-summary-and-minutes/2026/november-2026",
        "kind": "web",
        "display_name": "Bank of England: Monetary Policy Summary, November 2026"
      }
    ]
  },
  "partial": false,
  "completedSubforecastCount": 3,
  "plannedSubforecastCount": 3,
  "subforecasts": [
    { "sequenceIndex": 0, "status": "completed", "completedAt": "2026-10-12T08:12:40.118Z" }
  ],
  "creditsCharged": 10,
  "creditsRefunded": 0
}
```

- `id` is the **task** id. `forecast.id` is a different id: the forecast's own.
- `forecast.forecastData` is the number. Its shape depends on the question type; see [Forecast results](#forecast-results).
- `forecast.report` is the written report at three lengths. It is written after the number, so it can still be `pending` or `writing` when the task is already `completed`.
- `forecast.sources` lists what the forecast relied on. Each source has a `kind` and a `display_name`, and usually a `url`.
- `pollingUrl` is a path that already starts with `/api/v1/external`. Join it with the host, not with the base URL.

### The whole flow in one file

Copy one of these files, set `PROLEPSIA_API_KEY`, and run it. It creates a question, starts a forecast and prints the result when it is ready.

```ts
// forecast.ts. Run with: npx tsx forecast.ts, bun forecast.ts or deno run -A forecast.ts
const BASE_URL =
  process.env.PROLEPSIA_BASE_URL ?? "https://app.prolepsia.com/api/v1/external";
const HEADERS = {
  Authorization: `Bearer ${process.env.PROLEPSIA_API_KEY}`,
  "Content-Type": "application/json",
};
const FINAL = new Set(["completed", "failed", "cancelled"]);

async function call(path: string, init: RequestInit = {}) {
  const response = await fetch(`${BASE_URL}${path}`, {
    ...init,
    headers: { ...HEADERS, ...init.headers },
  });
  const data = await response.json();
  if (!response.ok) {
    throw new Error(`${response.status} ${data.error.type}: ${data.error.message}`);
  }
  return { data, retryAfter: Number(response.headers.get("Retry-After") ?? 120) };
}

const { data: task } = await call("/forecasts", {
  method: "POST",
  headers: { "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({
    question: {
      type: "binary",
      title: "Will Brent crude settle above $90 a barrel on 31 December 2026?",
      resolutionCriteria:
        "Resolves YES if the ICE Brent front-month settlement price on 31 December 2026 is above 90.00 USD. Resolves NO otherwise.",
    },
  }),
});
console.log(`Started task ${task.id} in ${task.runMode} mode.`);

for (;;) {
  const { data: latest, retryAfter } = await call(`/forecasts/${task.id}`);
  if (FINAL.has(latest.status)) {
    if (latest.status !== "completed") {
      console.log(`The forecast ended as ${latest.status}.`, latest.error ?? "");
      break;
    }
    const probability = latest.forecast.forecastData.payload.probability;
    console.log(`Chance of YES: ${(probability * 100).toFixed(1)}%`);
    console.log(latest.forecast.report?.short ?? "The report is still being written.");
    break;
  }
  console.log(`${latest.status}: ${latest.completedSubforecastCount} subforecasts done`);
  await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
}
```

```python
# forecast.py. Run with: python forecast.py
import os
import time
import uuid

import requests

BASE_URL = os.environ.get("PROLEPSIA_BASE_URL", "https://app.prolepsia.com/api/v1/external")
FINAL = {"completed", "failed", "cancelled"}
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['PROLEPSIA_API_KEY']}"


def call(method: str, path: str, **kwargs):
    response = session.request(method, f"{BASE_URL}{path}", timeout=60, **kwargs)
    data = response.json()
    if not response.ok:
        raise RuntimeError(f"{response.status_code} {data['error']['type']}: {data['error']['message']}")
    return data, int(response.headers.get("Retry-After", "120"))


task, _ = call(
    "POST",
    "/forecasts",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
        "question": {
            "type": "binary",
            "title": "Will Brent crude settle above $90 a barrel on 31 December 2026?",
            "resolutionCriteria": "Resolves YES if the ICE Brent front-month settlement price on 31 December 2026 is above 90.00 USD. Resolves NO otherwise.",
        }
    },
)
print(f"Started task {task['id']} in {task['run_mode']} mode.")

while True:
    latest, retry_after = call("GET", f"/forecasts/{task['id']}")
    if latest["status"] in FINAL:
        if latest["status"] != "completed":
            print(f"The forecast ended as {latest['status']}.", latest.get("error", ""))
            break
        probability = latest["forecast"]["forecastData"]["payload"]["probability"]
        print(f"Chance of YES: {probability:.1%}")
        report = latest["forecast"].get("report") or {}
        print(report.get("short") or "The report is still being written.")
        break
    print(f"{latest['status']}: {latest['completed_subforecast_count']} subforecasts done")
    time.sleep(retry_after)
```

## Question types

Every question has a `type` and a `title` of up to 500 characters. These fields are optional on every type: `description`, `resolutionCriteria`, `finePrint`, `tags` (up to 20 labels for your own filing, which the forecaster never sees) and `visibility` (`private` or `organization`).

| Type | Asks | Needs | Must not have |
| --- | --- | --- | --- |
| `binary` | Yes or no | Nothing more | `options`, bounds |
| `multiple_choice` | Which of several outcomes | 2 to 200 different `options` | Bounds |
| `numeric` | A number | `lowerBound`, `upperBound` and `scaleType` (`linear` or `logarithmic`; logarithmic needs positive bounds) | `options` |
| `discrete` | A whole number | Whole-number bounds, at least 2 and at most 500 apart | `scaleType` |
| `date` | When something happens | Bounds as whole Unix timestamps in seconds | `scaleType`, `options` |

`openLowerBound` and `openUpperBound` (default `false`) say whether the answer can fall outside the bounds. `optionsAreMutuallyExclusive` and `optionsAreComprehensive` (default `true`) describe the options of a multiple choice question.

Numeric question:

```json
{
  "type": "numeric",
  "title": "What will NVIDIA report as data-center revenue for its fiscal Q4 2027, in USD billions?",
  "resolutionCriteria": "The data-center segment revenue in NVIDIA's fiscal Q4 2027 earnings press release.",
  "lowerBound": 20,
  "upperBound": 120,
  "scaleType": "linear",
  "openUpperBound": true
}
```

Multiple choice question:

```json
{
  "type": "multiple_choice",
  "title": "Which company will have the largest market value on 1 January 2028?",
  "resolutionCriteria": "The company with the highest market capitalization at the close of US trading on 31 December 2027, as reported by companiesmarketcap.com.",
  "options": ["Apple", "Microsoft", "NVIDIA", "Other"]
}
```

Date question (the bounds are 1 January 2026 and 1 January 2034):

```json
{
  "type": "date",
  "title": "When will the first crewed Artemis lunar landing take place?",
  "resolutionCriteria": "The UTC date on which a crewed vehicle under NASA's Artemis program lands on the Moon, as announced by NASA.",
  "lowerBound": 1767225600,
  "upperBound": 2019686400
}
```

A question that breaks a rule answers `400` with the error type `invalid_request_error`. The message names the field, such as `question: lowerBound: Numeric, discrete, and date questions require bounds.`

### Other ways to create a question

- **Draft it with AI.** `POST /questions/normalize` with `{"title": "..."}` turns a rough question into a complete one, using a model and web search. It saves nothing and takes tens of seconds. Pass the returned `question` to `POST /questions` or `POST /forecasts`. When the answer is `status: "unsupported"`, read `suggestedReframe`.
- **Import it from a market.** `POST /questions/import` with `{"reference": "<a Metaculus, Kalshi or Polymarket link>"}` copies the question. No forecast starts.
- **Start from a template.** Send `templateId` and `templateValues` and the template writes the question. See [Start from a question template](#start-from-a-question-template).
- **Edit it.** `PATCH /questions/{questionId}` changes only the fields you send. Once a question has a forecast, only `description`, `resolutionCriteria`, `finePrint`, `visibility` and `tags` can change.

## Forecast results

`forecast.forecastData` holds `questionType`, `format` and `payload`. The `payload` depends on the question type.

| Question type | `format` | `payload` |
| --- | --- | --- |
| `binary` | `probability` | `probability`: the chance of YES, from 0 to 1. |
| `multiple_choice` | `option_probabilities` | `probabilities`: each option's chance, keyed by the option text. |
| `numeric` and `date` | `probability_mass_function` | `percentiles` (`p10`, `p50`, `p90`), `probability_mass_function`, `cumulative_distribution_function`, `bucket_edges`, `above_upper_probability` and `scaleType`. Dates are Unix timestamps in seconds. |
| `discrete` | `discrete_probability_array` | `probabilities` for each value in `nominal_values`, `percentiles` (`p10`, `p50`, `p90`) and `cell_boundaries`. |

For most numeric and date questions, `percentiles.p50` (the median) with `p10` and `p90` (the range the answer falls in 80% of the time) is all you need.

### The written report

| Field | What it holds |
| --- | --- |
| `status` | `pending`, `writing`, `completed` or `failed`. |
| `short` | One paragraph, up to about 120 words. |
| `medium` | Markdown with headings, up to about 600 words. |
| `detailed` | Markdown with headings, up to about 2,000 words. |

The three texts are `null` until `status` is `completed`. The `forecast.report.completed` webhook tells you when the report is ready.

### Smaller answers and work in progress

- `?fields=numeric` leaves out the report and the sources. Use it when you only need the number.
- `?includeSubforecasts=true` adds each finished subforecast's `forecastData`, `writeUp` and `sources`. Use it once, when you want to read them: the answer can grow past a megabyte.

### List forecasts

- `GET /forecasts` lists forecasts across your questions, newest first. Filter with `status`, `questionId` or `taskId` (repeat the parameter, or separate ids with commas), and `autoforecast` (`watch`, `schedule` or `any`) for questions that rerun on their own. `scope=all_visible` adds forecasts others shared with you. Each item's `report` holds only `status` and `short`.
- `GET /questions/{questionId}/forecasts` lists the forecasts of one question, in every state unless you pass `status`.

```ts
const page = await prolepsia(
  `/forecasts?questionId=${question.id}&fields=numeric&limit=20`,
);
for (const item of page.data) {
  console.log(item.taskId, item.status, item.forecastData?.payload);
}
```

```python
page = prolepsia(
    "GET",
    "/forecasts",
    params={"questionId": question["id"], "fields": "numeric", "limit": 20},
)
for item in page["data"]:
    print(item["taskId"], item["status"], (item.get("forecastData") or {}).get("payload"))
```

```bash
curl -sS "$PROLEPSIA_BASE_URL/forecasts?questionId=2c54dab4-f4e6-439d-a35d-1b7c71205259&fields=numeric&limit=20" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY"
```

### Many forecasts at once

`POST /forecasts/batch` starts up to 25 forecasts in one call. Each item in `items` is a whole `POST /forecasts` body. A bad item is reported on its own and the others still start, so read `counts` and each item's `status`. Always send an `Idempotency-Key`: without one, a retry starts every accepted item again.

```ts
const batch = await prolepsia("/forecasts/batch", {
  headers: { "Idempotency-Key": crypto.randomUUID() },
  body: {
    items: [
      { questionId: "2c54dab4-f4e6-439d-a35d-1b7c71205259" },
      {
        question: {
          type: "binary",
          title: "Will the Federal Reserve cut rates at its December 2026 meeting?",
          resolutionCriteria:
            "Resolves YES if the FOMC statement on 9 December 2026 lowers the target range.",
        },
      },
    ],
  },
});
console.log(batch.counts); // { total: 2, accepted: 2, rejected: 0 }
const taskIds = batch.items
  .filter((item: any) => item.status === "accepted")
  .map((item: any) => item.taskId);
```

```python
batch = prolepsia(
    "POST",
    "/forecasts/batch",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
        "items": [
            {"questionId": "2c54dab4-f4e6-439d-a35d-1b7c71205259"},
            {
                "question": {
                    "type": "binary",
                    "title": "Will the Federal Reserve cut rates at its December 2026 meeting?",
                    "resolutionCriteria": "Resolves YES if the FOMC statement on 9 December 2026 lowers the target range.",
                }
            },
        ]
    },
)
print(batch["counts"])  # {'total': 2, 'accepted': 2, 'rejected': 0}
task_ids = [item["taskId"] for item in batch["items"] if item["status"] == "accepted"]
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/forecasts/batch" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: rates-batch-2026-10-12" \
  -d '{"items": [{"questionId": "2c54dab4-f4e6-439d-a35d-1b7c71205259"}]}'
```

Each item in the answer has an `index`, a `status` of `accepted` or `rejected`, and either the `taskId` or an `error`. Nothing stores the batch itself, so keep the task ids.

### Cancel forecasts

`POST /forecasts/{taskId}/cancel` stops one queued or running forecast. `POST /forecasts/cancel` with `{"taskIds": [...]}` stops up to 25 at once and reports each one: `cancelled`, `already_finished` or `rejected`. Cancelling again is safe. Work already done is not refunded.

## Add your own context

The forecaster researches the web on its own. Add what it cannot find: your own data, a decision you have made, or a reading of the question you want it to follow. Context applies to the **next** forecast, not to forecasts already made.

### Notes

`POST /questions/{questionId}/context` adds a note. `treatment` says how the forecaster uses it:

| `treatment` | The forecaster |
| --- | --- |
| `consider` | Weighs it as evidence. This is the default. |
| `look_into` | Researches it further. |
| `assume_true` | Takes it as given. |
| `ignore` | Is told not to rely on it, for a claim or source you know is wrong. |

```ts
const note = await prolepsia(`/questions/${question.id}/context`, {
  body: {
    text: "Our economics desk expects UK CPI for November to print at 2.4%.",
    treatment: "consider",
  },
});
```

```python
note = prolepsia(
    "POST",
    f"/questions/{question['id']}/context",
    json={
        "text": "Our economics desk expects UK CPI for November to print at 2.4%.",
        "treatment": "consider",
    },
)
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/questions/2c54dab4-f4e6-439d-a35d-1b7c71205259/context" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Our economics desk expects UK CPI for November to print at 2.4%.", "treatment": "consider"}'
```

You can also send notes with the forecast itself: `POST /forecasts` takes a `context` list of the same note objects.

### Files

`POST /questions/{questionId}/files` uploads a document as `multipart/form-data`. A file can be up to 10 MiB, and a question can hold 10 files and 30 MiB in all. Optional form fields:

- `artifactType`: what the file is. `user_context` (the default, for something you wrote), `background`, `evidence_source` (a report or filing), `dataset` (a spreadsheet) or `assumption`.
- `treatment`: `consider`, `look_into` or `assume_true`. A file cannot be ignored; remove it instead.
- `userNote`: a short note about the file, up to 2,000 characters.

```ts
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append(
  "file",
  new Blob([await readFile("cpi-preview.pdf")], { type: "application/pdf" }),
  "cpi-preview.pdf",
);
form.append("artifactType", "evidence_source");
form.append("userNote", "Internal preview of the November CPI release");

const file = await prolepsia(`/questions/${question.id}/files`, { body: form });
console.log(file.id, file.status); // "processing"
```

```python
with open("cpi-preview.pdf", "rb") as handle:
    file = prolepsia(
        "POST",
        f"/questions/{question['id']}/files",
        files={"file": ("cpi-preview.pdf", handle, "application/pdf")},
        data={
            "artifactType": "evidence_source",
            "userNote": "Internal preview of the November CPI release",
        },
    )
print(file["id"], file["status"])  # "processing"
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/questions/2c54dab4-f4e6-439d-a35d-1b7c71205259/files" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -F "file=@cpi-preview.pdf" \
  -F "artifactType=evidence_source" \
  -F "userNote=Internal preview of the November CPI release"
```

A new file has the status `processing` while Prolepsia reads its text. It reaches a forecast only once it is `ready`. Check with `GET /questions/{questionId}/context/ready`, which answers `{"ready": true}` when every file is ready. Starting a forecast while a file is still processing answers `409` with the error type `context_not_ready`, so you never get a forecast that quietly skipped your file. Send `allowIncompleteContext: true` with the forecast to run without it anyway. A file whose status is `failed` never becomes ready: remove it with `DELETE /questions/{questionId}/files/{fileId}`.

## Start from a question template

A question template is the shape of a question you ask again and again, with blanks for the parts that change. `GET /question-templates` lists the templates you can use: Prolepsia's built-in templates, your organization's and your own. Add `?scope=builtin`, `organization` or `personal` to list one kind. Each template lists its `blanks`, and each blank has a `key`, a `label` and a `hint`.

To make a question from a template, send its `templateId` and a value for each blank in `templateValues`. The template writes the title, the description, the resolution criteria and every other field you leave out. It also adds its notes, files, tags and forecast preset. A field you send wins over the template's. This example uses the built-in template "Company earnings above a target":

```ts
const question = await prolepsia("/questions", {
  body: {
    templateId: "12463a68-aa03-5f51-9fde-b80f94d5fa07",
    templateValues: {
      company: "Nvidia",
      metric: "revenue",
      target: "$60 billion",
      period: "the fourth quarter of fiscal 2027",
    },
  },
});
console.log(question.title);
// "Will Nvidia report revenue above $60 billion for the fourth quarter of fiscal 2027?"
```

```python
question = prolepsia(
    "POST",
    "/questions",
    json={
        "templateId": "12463a68-aa03-5f51-9fde-b80f94d5fa07",
        "templateValues": {
            "company": "Nvidia",
            "metric": "revenue",
            "target": "$60 billion",
            "period": "the fourth quarter of fiscal 2027",
        },
    },
)
print(question["title"])
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/questions" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"templateId": "12463a68-aa03-5f51-9fde-b80f94d5fa07", "templateValues": {"company": "Nvidia", "metric": "revenue", "target": "$60 billion", "period": "the fourth quarter of fiscal 2027"}}'
```

The new question names its template in `sourceTemplate`. Editing or deleting the template later never changes the question.

- **Fill the blanks with AI.** `POST /question-templates/{templateId}/fill` with a short `prompt`, such as `"Nvidia, next quarter, revenue above $60 billion"`, answers `templateValues` for every blank. Blanks you send in `templateValues` keep their value. Nothing is saved: check the values, then create the question as above.
- **Save a question as a template.** `POST /question-templates/from-question` with `questionId`, `scope` (`personal` or `organization`) and `name`.
- **Make your own.** `POST /question-templates` with `scope`, `name`, `questionType`, the question `fields` and a `blanks` list. Write each blank as `{key}` in the fields, and declare each key once in `blanks`. A blank that is used but not declared, or declared but not used, is refused. Write `{{` or `}}` for a brace that is not a blank. An organization template needs an organization admin.

`POST /forecasts` takes `templateId` and `templateValues` in its inline `question` too. When people besides you can read the new question and the template holds notes or files, the answer carries `warnings` with the code `template_content_readable_by_others`.

## Choose a forecast preset

A forecast preset is a saved set of choices for a forecast: the connections it should not use, standing notes and files. `GET /forecast-presets` lists your presets and your organization's.

Every call that starts a forecast takes two optional fields:

- `presetId`: a preset id, or `"none"` for no preset. Leave it out to use the defaults: the question's own preset, then your default preset, then your organization's.
- `connections`: `{"off": [...], "on": [...]}` with connection ids. It switches single connections off, or back on, for this forecast only.

```ts
const task = await prolepsia(`/questions/${question.id}/forecasts`, {
  body: {
    presetId: "e2b86642-72c4-4490-981e-aa9474f24e38",
    connections: { off: ["dce1bdbc-1a94-4570-a6b5-66e7a26839fc"] },
  },
});
console.log(task.id, task.preset, task.connections);
```

```python
task = prolepsia(
    "POST",
    f"/questions/{question['id']}/forecasts",
    json={
        "presetId": "e2b86642-72c4-4490-981e-aa9474f24e38",
        "connections": {"off": ["dce1bdbc-1a94-4570-a6b5-66e7a26839fc"]},
    },
)
print(task["id"], task["preset"], task["connections"])
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/questions/2c54dab4-f4e6-439d-a35d-1b7c71205259/forecasts" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"presetId": "e2b86642-72c4-4490-981e-aa9474f24e38", "connections": {"off": ["dce1bdbc-1a94-4570-a6b5-66e7a26839fc"]}}'
```

A forecast read carries `preset`. It is `null` when the forecast used no preset. Otherwise it holds `used: true`, and the preset's `id` and `name` when you may see them.

- **Add a preset.** `POST /forecast-presets` with `scope` (`personal` or `organization`), `name`, `disabledConnectionIds` and `notes`, each note with `text` and `treatment`. An organization preset needs an organization admin. A connection you add later is on in every preset until a preset switches it off.
- **Add a file.** `POST /forecast-presets/{presetId}/files` takes a `multipart/form-data` upload, or JSON with `filename` and `contentBase64`. While a preset file is processing, starting a forecast with that preset answers `409 context_not_ready`, as a question's own file does.
- **Set a default.** `PUT /forecast-presets/defaults` with `userPresetId` sets your own default. An organization admin can set `organizationPresetId` too.
- **Keep a pick.** `PUT /questions/{questionId}/schedule` and `PUT /questions/{questionId}/watch` take the same `presetId`, and every forecast they start uses it.

When people besides you can read the forecast and its preset holds notes or files, the answer carries `warnings` with the code `preset_content_may_show_in_forecast`.

## Connect your own servers

A connection is a remote MCP server that your forecasts can call, such as a company wiki or a data service. Every forecast you start can use your connections and your organization's. Managing them needs an API key with the `connections:read` and `connections:write` permissions.

`POST /connections` adds a server and tests it straight away:

```ts
const connection = await prolepsia("/connections", {
  body: {
    scope: "personal",
    name: "Research notes",
    url: "https://notes.example.com/mcp",
    authKind: "api_key",
    api_key: "nk_live_...",
  },
});
console.log(connection.status, connection.tools.map((tool) => tool.name));
```

```python
connection = prolepsia(
    "POST",
    "/connections",
    json={
        "scope": "personal",
        "name": "Research notes",
        "url": "https://notes.example.com/mcp",
        "authKind": "api_key",
        "api_key": "nk_live_...",
    },
)
print(connection["status"], [tool["name"] for tool in connection["tools"]])
```

```bash
curl -sS -X POST "$PROLEPSIA_BASE_URL/connections" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scope": "personal", "name": "Research notes", "url": "https://notes.example.com/mcp", "authKind": "api_key", "api_key": "nk_live_..."}'
```

- `scope` is `personal`, for forecasts you start, or `organization`, for every member's forecasts. An organization connection needs an organization admin.
- `name` is the name forecasts see. The `url` must start with `https://` and reach a public address.
- `authKind` is `none`, `api_key` or `oauth`. Prolepsia sends a key as `Authorization: Bearer <key>`, or as it is in the header you name in `apiKeyHeader`. It never returns the key: reads show `hasApiKey` instead.
- For `oauth`, call `POST /connections/{connectionId}/sign-in` and open the `authorizationUrl` it returns in a browser. Only a person in a browser can finish a sign-in.
- The answer lists the server's `tools`, each with `allowed`. Tools the server marks as changing data start with `allowed: false`. `PATCH /connections/{connectionId}` with `{"tools": {"add_note": true}}` switches one on, and `{"enabled": false}` switches the whole connection off.
- `POST /connections/{connectionId}/test` tests the server again. `PUT /connections/{connectionId}/member-switch` with `{"enabled": false}` leaves an organization connection out of your own forecasts.

A forecast read lists the connections the forecast could call in `connections`, each with its `name`, its `owner`, the number of `calls` and a `status`: `used`, `not_used`, `failed` or `unavailable`. A connection that fails never stops a forecast. When people besides you can read the question, the answer that starts a forecast with connections carries `warnings` with the code `connection_results_readable_by_others`.

## Keep forecasts current

- **Schedule.** `PUT /questions/{questionId}/schedule` reruns a question on a fixed cadence, such as every 7 days until an end date. The cadence can be no faster than every 3 hours, and one schedule can make at most 50 forecasts. Each of them is a paid forecast.
- **Automatic reruns.** `PUT /questions/{questionId}/watch` with `{"enabled": true}` starts a new forecast when the news changes the picture by more than `changeThreshold`, in the question's own units. Automatic reruns cost a monthly fee (`monitoring.question_month` in the prices) plus each forecast they start.

```ts
const schedule = await prolepsia(`/questions/${question.id}/schedule`, {
  method: "PUT",
  body: { intervalCount: 7, intervalUnit: "days", endAt: "2026-12-16T00:00:00Z" },
});
console.log(schedule.nextDueAt, schedule.occurrenceCount);
```

```python
schedule = prolepsia(
    "PUT",
    f"/questions/{question['id']}/schedule",
    json={"intervalCount": 7, "intervalUnit": "days", "endAt": "2026-12-16T00:00:00Z"},
)
print(schedule["nextDueAt"], schedule["occurrenceCount"])
```

```bash
curl -sS -X PUT "$PROLEPSIA_BASE_URL/questions/2c54dab4-f4e6-439d-a35d-1b7c71205259/schedule" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"intervalCount": 7, "intervalUnit": "days", "endAt": "2026-12-16T00:00:00Z"}'
```

`intervalUnit` is `hours`, `days` or `months`. `startAt` defaults to now. Sending the schedule again replaces it. `DELETE` on the same path stops it and leaves a forecast already running alone. `GET /schedules` lists schedules and `GET /watches` lists automatic reruns across your questions.

## Ask about a forecast

`POST /forecasts/{taskId}/chat` with `{"message": "..."}` asks a question about a completed forecast, such as why the number is what it is or what would change it. The answer comes from the reasoning stored with that forecast. Send back the returned `id` as `threadId` to continue the same conversation. The last `assistant` message in `messages` is the answer. Each message costs a model call and takes several seconds.

## Conventions

### Paths

Paths have no trailing slash: `/questions`, not `/questions/`. A path that ends in `/` answers `404`.

### Times

Times are ISO 8601 in UTC with milliseconds, such as `2026-10-12T07:45:41.413Z`. Date bounds on questions are Unix timestamps in seconds.

### Pages

Most lists answer one page at a time:

```json
{ "type": "list", "data": [], "hasMore": true, "nextCursor": "MjAyNi0xMC0xMlQwNzo0..." }
```

Pass `limit` (1 to 100, default 20) and, for the next page, `cursor` set to the `nextCursor` you got. `GET /questions/browse` and `GET /decisions` page with `limit` (up to 200, default 50) and `offset` instead.

```ts
async function* allQuestions() {
  let cursor: string | null = null;
  do {
    const query = new URLSearchParams({ limit: "100" });
    if (cursor) query.set("cursor", cursor);
    const page = await prolepsia(`/questions?${query}`);
    yield* page.data;
    cursor = page.hasMore ? page.nextCursor : null;
  } while (cursor);
}

for await (const item of allQuestions()) console.log(item.id, item.title);
```

```python
def all_questions():
    params = {"limit": 100}
    while True:
        page = prolepsia("GET", "/questions", params=params)
        yield from page["data"]
        if not page["hasMore"]:
            return
        params["cursor"] = page["nextCursor"]


for item in all_questions():
    print(item["id"], item["title"])
```

```bash
# The first page.
curl -sS -G "$PROLEPSIA_BASE_URL/questions" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  --data-urlencode "limit=100"

# The next page: set NEXT_CURSOR to the nextCursor of the answer above.
curl -sS -G "$PROLEPSIA_BASE_URL/questions" \
  -H "Authorization: Bearer $PROLEPSIA_API_KEY" \
  --data-urlencode "limit=100" \
  --data-urlencode "cursor=$NEXT_CURSOR"
```

### Safe retries

Four calls accept an `Idempotency-Key` header: `POST /forecasts`, `POST /forecasts/batch`, `POST /decisions` and `POST /decisions/{decisionId}/forecasts`. The key is any text up to 128 characters, kept for 24 hours for each API key.

- The same key with the same body returns the first answer again, with the header `Idempotent-Replayed: true`. Nothing new starts.
- The same key with a different body answers `400`.
- A second request while the first is still being handled answers `409` with the error type `idempotency_in_progress`. Wait a second and try again.
- When the first request failed, the key is free again, so a retry runs normally.

Other calls ignore the header. `POST /questions/{questionId}/forecasts` is safe to retry for another reason: while the first forecast runs, a second request answers `409` `forecast_already_running` with the running task's id.

### Rate limits

Each API key may make 60 requests a minute unless Prolepsia set a different limit for it. Some calls also count against a shared limit:

| Calls | Limit |
| --- | --- |
| Drafting questions with AI (`normalize`, `normalize/revise`, `normalize/clarify`) | 20 a minute for each key |
| Forecast chat messages | 30 a minute for each key |
| Decision calls that use a model: creating a decision, drafting or keeping questions, conditional links, refreshing the summary | 10 a minute for each key |
| Testing and replaying webhook deliveries | 20 a minute for each user |

Going over a limit answers `429` with a `Retry-After` header in seconds. Separately, one user can have at most 50 forecasts started through the API running at once; past that, starting another answers `429` with the error type `too_many_active_forecasts`.

### Errors

Every error has the same shape:

```json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "question: lowerBound: Numeric, discrete, and date questions require bounds."
  }
}
```

| Status | `error.type` | What to do |
| --- | --- | --- |
| 400 | `invalid_request_error` | Fix the request. The message names the field. |
| 401 | `invalid_api_key` | The key is missing, wrong, revoked or expired. |
| 403 | `permission_denied` | The key lacks a scope, or its owner cannot do this. |
| 403 | `uploads_disabled` | Your organization has turned file uploads off. |
| 404 | `not_found` | The id is wrong or not visible to this key. Also check for a trailing slash. |
| 405 | `invalid_request_error` | This path does not take that method. The `Allow` header lists the ones it takes. |
| 409 | `forecast_already_running` | Wait for the task in `error.activeTaskId`, or cancel it. |
| 409 | `context_not_ready` | A file is still processing. `error.files` lists them. Wait and try again. |
| 409 | `idempotency_in_progress` | The first request with this key is still running. Try again in a second. |
| 409 | `invalid_request_error` | Not enough credits, or another conflict. Read the message. |
| 409 | Decision conflicts, such as `generation_in_progress` or `decision_archived` | Wait, or change the request. |
| 429 | `rate_limit_error`, `too_many_active_forecasts` | Wait for the seconds in `Retry-After`. |
| 503 | `overloaded_error` | Try again shortly. |

## Decisions

A decision is a choice between 2 to 4 options, with a map of questions that bear on it. The `/decisions` calls create a decision, draft questions for it with AI, keep or discard the drafts, link questions, price and start their forecasts, and write a summary that weighs each option. Price a batch of forecasts with `POST /decisions/{decisionId}/forecasts/estimate` before you start it, and pass the approved total as `maxTotalCredits`. Every decision call is listed in the [endpoint reference](#endpoint-reference).

## Endpoint reference

Every path below starts with `https://app.prolepsia.com/api/v1/external`. The [interactive API reference](https://app.prolepsia.com/api/external/docs/) shows each call's fields and answers, and lets you try it with your key.

#### Forecasts

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/run-modes` | `forecasts:read` | List run modes |
| GET | `/forecasts` | `forecasts:read` | List forecasts |
| POST | `/forecasts` | `forecasts:write` | Create a forecast |
| GET | `/forecasts/{task_id}` | `forecasts:read` | Get a forecast task |
| POST | `/forecasts/{task_id}/cancel` | `forecasts:write` | Cancel a forecast task |
| POST | `/forecasts/batch` | `forecasts:write` | Create several forecasts |
| POST | `/forecasts/cancel` | `forecasts:write` | Cancel several forecasts |
| GET | `/questions/{question_id}/forecasts` | `forecasts:read` | List a question's forecasts |
| POST | `/questions/{question_id}/forecasts` | `forecasts:write` | Run a forecast on a question |

#### Questions

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/questions` | `questions:read` | List questions |
| POST | `/questions` | `questions:write` | Create a question |
| GET | `/questions/{question_id}` | `questions:read` | Get a question |
| PATCH | `/questions/{question_id}` | `questions:write` | Edit a question |
| GET | `/questions/browse` | `questions:read` | Search and filter questions |
| POST | `/questions/import` | `questions:write` | Import a question from a market |
| POST | `/questions/normalize` | `questions:write` | Normalize a question |
| POST | `/questions/normalize/clarify` | `questions:write` | Answer a requested clarification |
| POST | `/questions/normalize/revise` | `questions:write` | Revise a normalized question with guidance |
| GET | `/tags` | `questions:read` | List tags |

#### Question context

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/questions/{question_id}/context` | `questions:read` | List a question's context notes |
| POST | `/questions/{question_id}/context` | `questions:write` | Add a context note to a question |
| PATCH | `/questions/{question_id}/context/{note_id}` | `questions:write` | Edit a context note |
| DELETE | `/questions/{question_id}/context/{note_id}` | `questions:write` | Remove a context note |
| GET | `/questions/{question_id}/context/ready` | `questions:read` | Check whether a question's files are ready |
| GET | `/questions/{question_id}/files` | `questions:read` | List a question's context files |
| POST | `/questions/{question_id}/files` | `questions:write` | Upload a context file |
| GET | `/questions/{question_id}/files/{file_id}` | `questions:read` | Get a context file's status |
| PATCH | `/questions/{question_id}/files/{file_id}` | `questions:write` | Edit a context file's settings |
| DELETE | `/questions/{question_id}/files/{file_id}` | `questions:write` | Remove a context file |
| POST | `/questions/{question_id}/files/{file_id}/replace` | `questions:write` | Replace a context file's bytes |

#### Forecast chat

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/forecasts/{task_id}/chat` | `forecasts:read` | List conversations about a forecast |
| POST | `/forecasts/{task_id}/chat` | `forecasts:write` | Ask a question about a forecast |
| GET | `/forecasts/{task_id}/chat/{thread_id}` | `forecasts:read` | Read one conversation |
| POST | `/forecasts/chat/messages/{message_id}/accept-context` | `questions:write` | Save a chat suggestion as context |

#### Schedules and automatic reruns

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/questions/{question_id}/schedule` | `forecasts:read` | Get a question's forecast schedule |
| PUT | `/questions/{question_id}/schedule` | `forecasts:write` | Create or replace a forecast schedule |
| DELETE | `/questions/{question_id}/schedule` | `forecasts:write` | Delete a forecast schedule |
| GET | `/questions/{question_id}/watch` | `forecasts:read` | Get a question's auto-reforecast watch |
| PUT | `/questions/{question_id}/watch` | `forecasts:write` | Set a question's auto-reforecast watch |
| DELETE | `/questions/{question_id}/watch` | `forecasts:write` | Stop a question's auto-reforecast watch |
| GET | `/schedules` | `forecasts:read` | List forecast schedules |
| GET | `/watches` | `forecasts:read` | List auto-reforecast watches |

#### Sharing and notifications

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/questions/{question_id}/share-links` | `questions:write` | List a question's share links |
| POST | `/questions/{question_id}/share-links` | `questions:write` | Create a share link |
| GET | `/questions/{question_id}/share-links/{link_id}` | `questions:write` | Get a share link |
| PATCH | `/questions/{question_id}/share-links/{link_id}` | `questions:write` | Edit a share link |
| DELETE | `/questions/{question_id}/share-links/{link_id}` | `questions:write` | Delete a share link |
| POST | `/questions/{question_id}/share-links/{link_id}/revoke` | `questions:write` | Revoke a share link |
| GET | `/questions/{question_id}/subscription` | `questions:read` | Get your notification subscription |
| PUT | `/questions/{question_id}/subscription` | `questions:write` | Follow a public question |
| DELETE | `/questions/{question_id}/subscription` | `questions:write` | Unfollow a public question |

#### Webhooks

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/webhook-deliveries` | `forecasts:read` | List webhook deliveries |
| GET | `/webhook-deliveries/{delivery_id}` | `forecasts:read` | Get a webhook delivery |
| GET | `/webhook-deliveries/{delivery_id}/attempts` | `forecasts:read` | List a delivery's attempts |
| POST | `/webhook-deliveries/{delivery_id}/replay` | `forecasts:write` | Replay a webhook delivery |
| GET | `/webhooks` | `forecasts:read` | List webhooks |
| POST | `/webhooks` | `forecasts:write` | Register a webhook |
| GET | `/webhooks/{webhook_id}` | `forecasts:read` | Get a webhook |
| PATCH | `/webhooks/{webhook_id}` | `forecasts:write` | Edit a webhook |
| DELETE | `/webhooks/{webhook_id}` | `forecasts:write` | Delete a webhook |
| POST | `/webhooks/{webhook_id}/rotate` | `forecasts:write` | Rotate a webhook's signing secret |
| POST | `/webhooks/{webhook_id}/test` | `forecasts:write` | Send a test webhook |

#### Connections

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/connections` | `connections:read` | List connections |
| POST | `/connections` | `connections:write` | Add a connection |
| GET | `/connections/{connection_id}` | `connections:read` | Get a connection |
| PATCH | `/connections/{connection_id}` | `connections:write` | Edit a connection |
| DELETE | `/connections/{connection_id}` | `connections:write` | Delete a connection |
| POST | `/connections/{connection_id}/test` | `connections:write` | Test a connection |
| PUT | `/connections/{connection_id}/member-switch` | `connections:write` | Switch an organization connection for yourself |
| POST | `/connections/{connection_id}/sign-in` | `connections:write` | Start a connection sign-in |

#### Forecast presets

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/forecast-presets` | `forecasts:read` | List forecast presets |
| POST | `/forecast-presets` | `forecasts:write` | Add a forecast preset |
| GET | `/forecast-presets/defaults` | `forecasts:read` | Get the default forecast presets |
| PUT | `/forecast-presets/defaults` | `forecasts:write` | Set the default forecast presets |
| GET | `/forecast-presets/{preset_id}` | `forecasts:read` | Get a forecast preset |
| PATCH | `/forecast-presets/{preset_id}` | `forecasts:write` | Edit a forecast preset |
| DELETE | `/forecast-presets/{preset_id}` | `forecasts:write` | Delete a forecast preset |
| POST | `/forecast-presets/{preset_id}/files` | `forecasts:write` | Add a file to a forecast preset |
| GET | `/forecast-presets/{preset_id}/files/{file_id}` | `forecasts:read` | Get a forecast preset file |
| PATCH | `/forecast-presets/{preset_id}/files/{file_id}` | `forecasts:write` | Edit a forecast preset file |
| DELETE | `/forecast-presets/{preset_id}/files/{file_id}` | `forecasts:write` | Delete a forecast preset file |

#### Question templates

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/question-templates` | `questions:read` | List question templates |
| POST | `/question-templates` | `questions:write` | Add a question template |
| POST | `/question-templates/from-question` | `questions:write` | Save a question as a template |
| GET | `/question-templates/{template_id}` | `questions:read` | Get a question template |
| PATCH | `/question-templates/{template_id}` | `questions:write` | Edit a question template |
| DELETE | `/question-templates/{template_id}` | `questions:write` | Delete a question template |
| POST | `/question-templates/{template_id}/apply` | `questions:read` | Show what a question template gives |
| POST | `/question-templates/{template_id}/fill` | `questions:write` | Fill a template's blanks with the AI |
| POST | `/question-templates/{template_id}/files` | `questions:write` | Add a file to a question template |
| GET | `/question-templates/{template_id}/files/{file_id}` | `questions:read` | Get a question template file |
| PATCH | `/question-templates/{template_id}/files/{file_id}` | `questions:write` | Edit a question template file |
| DELETE | `/question-templates/{template_id}/files/{file_id}` | `questions:write` | Delete a question template file |

#### Decisions

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/decisions` | `questions:read` | List decisions |
| POST | `/decisions` | `questions:write` | Create a decision |
| GET | `/decisions/{decision_id}` | `questions:read` | Read a decision |
| PATCH | `/decisions/{decision_id}` | `questions:write` | Edit a decision |
| POST | `/decisions/{decision_id}/archive` | `questions:write` | Archive a decision |
| POST | `/decisions/{decision_id}/unarchive` | `questions:write` | Unarchive a decision |
| POST | `/decisions/{decision_id}/generate` | `questions:write` | Draft more questions |
| PATCH | `/decisions/{decision_id}/drafts/{draft_id}` | `questions:write` | Edit a question draft |
| POST | `/decisions/{decision_id}/drafts/discard` | `questions:write` | Discard question drafts |
| POST | `/decisions/{decision_id}/drafts/keep` | `questions:write` | Keep question drafts |
| POST | `/decisions/{decision_id}/forecasts` | `forecasts:write` | Start forecasts for a decision |
| POST | `/decisions/{decision_id}/forecasts/estimate` | `forecasts:read` | Price forecasts for a decision |
| POST | `/decisions/{decision_id}/nodes` | `questions:write` | Put a question on a decision |
| DELETE | `/decisions/{decision_id}/nodes/{node_id}` | `questions:write` | Take a question off a decision |
| POST | `/decisions/{decision_id}/links` | `questions:write` | Link two questions on a decision |
| PATCH | `/decisions/{decision_id}/links/{link_id}` | `questions:write` | Edit a failed conditional link |
| DELETE | `/decisions/{decision_id}/links/{link_id}` | `questions:write` | Remove a link |
| POST | `/decisions/{decision_id}/notes` | `questions:write` | Add a note to a decision |
| PATCH | `/decisions/{decision_id}/notes/{note_id}` | `questions:write` | Edit a decision note |
| DELETE | `/decisions/{decision_id}/notes/{note_id}` | `questions:write` | Remove a decision note |
| GET | `/decisions/{decision_id}/comments` | `questions:read` | List the comments on a decision |
| POST | `/decisions/{decision_id}/comments` | `questions:write` | Comment on a decision |
| PATCH | `/decisions/{decision_id}/comments/{comment_id}` | `questions:write` | Edit a comment |
| DELETE | `/decisions/{decision_id}/comments/{comment_id}` | `questions:write` | Delete a comment |
| GET | `/decisions/{decision_id}/conditions` | `questions:read` | List the conditions on a decision |
| POST | `/decisions/{decision_id}/conditions` | `questions:write` | Add a condition to a decision |
| PATCH | `/decisions/{decision_id}/conditions/{condition_id}` | `questions:write` | Change a condition or arm it again |
| DELETE | `/decisions/{decision_id}/conditions/{condition_id}` | `questions:write` | Delete a condition |
| GET | `/decisions/{decision_id}/summary` | `questions:read` | Read a decision summary |
| POST | `/decisions/{decision_id}/summary/refresh` | `questions:write` | Write a new decision summary |
| PUT | `/decisions/{decision_id}/live` | `forecasts:write` | Turn live mode on or off |
| PUT | `/decisions/{decision_id}/follow` | `questions:write` | Follow or unfollow a decision |

#### Account

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/credits` | `forecasts:read` | Get organization credits |
