Docs
For developers

API

Create questions, start forecasts and read results from your own code, with examples in TypeScript, Python and curl.

View as Markdown

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 · OpenAPI 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 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.

ScopeLets the key
questions:readRead questions, their tags, context notes and files, question subscriptions and question templates.
questions:writeCreate, edit and import questions, draft questions with AI, manage notes, files, share links, subscriptions and question templates.
forecasts:readRead forecasts and their status, your forecast chats, schedules, watches, forecast presets, webhooks and webhook deliveries. Check your credit balance.
forecasts:writeStart and cancel forecasts, chat about a forecast, manage schedules, watches, forecast presets and webhooks, and test or replay webhook deliveries.
connections:readList your connected MCP servers and the tools they offer. Keys and sign-ins are never returned.
connections:writeAdd, 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);

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 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);

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"

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 saves you the polling.

StatusMeaning
queuedWaiting to start.
running_subforecastsSeveral separate forecasts, called subforecasts, are researching the question at the same time.
synthesizingThe subforecasts are being combined into one answer.
completedFinal. The answer is in forecast.
failedFinal. Nothing to read except the subforecasts that finished. error.message says what happened.
cancelledFinal. 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);

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.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.tsconst 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));}

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).

TypeAsksNeedsMust not have
binaryYes or noNothing moreoptions, bounds
multiple_choiceWhich of several outcomes2 to 200 different optionsBounds
numericA numberlowerBound, upperBound and scaleType (linear or logarithmic; logarithmic needs positive bounds)options
discreteA whole numberWhole-number bounds, at least 2 and at most 500 apartscaleType
dateWhen something happensBounds as whole Unix timestamps in secondsscaleType, 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.
  • 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 typeformatpayload
binaryprobabilityprobability: the chance of YES, from 0 to 1.
multiple_choiceoption_probabilitiesprobabilities: each option's chance, keyed by the option text.
numeric and dateprobability_mass_functionpercentiles (p10, p50, p90), probability_mass_function, cumulative_distribution_function, bucket_edges, above_upper_probability and scaleType. Dates are Unix timestamps in seconds.
discretediscrete_probability_arrayprobabilities 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

FieldWhat it holds
statuspending, writing, completed or failed.
shortOne paragraph, up to about 120 words.
mediumMarkdown with headings, up to about 600 words.
detailedMarkdown 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);}

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);

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:

treatmentThe forecaster
considerWeighs it as evidence. This is the default.
look_intoResearches it further.
assume_trueTakes it as given.
ignoreIs 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",  },});

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"

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?"

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);

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));
  • 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);

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);

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:

CallsLimit
Drafting questions with AI (normalize, normalize/revise, normalize/clarify)20 a minute for each key
Forecast chat messages30 a minute for each key
Decision calls that use a model: creating a decision, drafting or keeping questions, conditional links, refreshing the summary10 a minute for each key
Testing and replaying webhook deliveries20 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."  }}
Statuserror.typeWhat to do
400invalid_request_errorFix the request. The message names the field.
401invalid_api_keyThe key is missing, wrong, revoked or expired.
403permission_deniedThe key lacks a scope, or its owner cannot do this.
403uploads_disabledYour organization has turned file uploads off.
404not_foundThe id is wrong or not visible to this key. Also check for a trailing slash.
405invalid_request_errorThis path does not take that method. The Allow header lists the ones it takes.
409forecast_already_runningWait for the task in error.activeTaskId, or cancel it.
409context_not_readyA file is still processing. error.files lists them. Wait and try again.
409idempotency_in_progressThe first request with this key is still running. Try again in a second.
409invalid_request_errorNot enough credits, or another conflict. Read the message.
409Decision conflicts, such as generation_in_progress or decision_archivedWait, or change the request.
429rate_limit_error, too_many_active_forecastsWait for the seconds in Retry-After.
503overloaded_errorTry 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

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

Forecasts

MethodPathScopeWhat it does
GET/run-modesforecasts:readList run modes
GET/forecastsforecasts:readList forecasts
POST/forecastsforecasts:writeCreate a forecast
GET/forecasts/{task_id}forecasts:readGet a forecast task
POST/forecasts/{task_id}/cancelforecasts:writeCancel a forecast task
POST/forecasts/batchforecasts:writeCreate several forecasts
POST/forecasts/cancelforecasts:writeCancel several forecasts
GET/questions/{question_id}/forecastsforecasts:readList a question's forecasts
POST/questions/{question_id}/forecastsforecasts:writeRun a forecast on a question

Questions

MethodPathScopeWhat it does
GET/questionsquestions:readList questions
POST/questionsquestions:writeCreate a question
GET/questions/{question_id}questions:readGet a question
PATCH/questions/{question_id}questions:writeEdit a question
GET/questions/browsequestions:readSearch and filter questions
POST/questions/importquestions:writeImport a question from a market
POST/questions/normalizequestions:writeNormalize a question
POST/questions/normalize/clarifyquestions:writeAnswer a requested clarification
POST/questions/normalize/revisequestions:writeRevise a normalized question with guidance
GET/tagsquestions:readList tags

Question context

MethodPathScopeWhat it does
GET/questions/{question_id}/contextquestions:readList a question's context notes
POST/questions/{question_id}/contextquestions:writeAdd a context note to a question
PATCH/questions/{question_id}/context/{note_id}questions:writeEdit a context note
DELETE/questions/{question_id}/context/{note_id}questions:writeRemove a context note
GET/questions/{question_id}/context/readyquestions:readCheck whether a question's files are ready
GET/questions/{question_id}/filesquestions:readList a question's context files
POST/questions/{question_id}/filesquestions:writeUpload a context file
GET/questions/{question_id}/files/{file_id}questions:readGet a context file's status
PATCH/questions/{question_id}/files/{file_id}questions:writeEdit a context file's settings
DELETE/questions/{question_id}/files/{file_id}questions:writeRemove a context file
POST/questions/{question_id}/files/{file_id}/replacequestions:writeReplace a context file's bytes

Forecast chat

MethodPathScopeWhat it does
GET/forecasts/{task_id}/chatforecasts:readList conversations about a forecast
POST/forecasts/{task_id}/chatforecasts:writeAsk a question about a forecast
GET/forecasts/{task_id}/chat/{thread_id}forecasts:readRead one conversation
POST/forecasts/chat/messages/{message_id}/accept-contextquestions:writeSave a chat suggestion as context

Schedules and automatic reruns

MethodPathScopeWhat it does
GET/questions/{question_id}/scheduleforecasts:readGet a question's forecast schedule
PUT/questions/{question_id}/scheduleforecasts:writeCreate or replace a forecast schedule
DELETE/questions/{question_id}/scheduleforecasts:writeDelete a forecast schedule
GET/questions/{question_id}/watchforecasts:readGet a question's auto-reforecast watch
PUT/questions/{question_id}/watchforecasts:writeSet a question's auto-reforecast watch
DELETE/questions/{question_id}/watchforecasts:writeStop a question's auto-reforecast watch
GET/schedulesforecasts:readList forecast schedules
GET/watchesforecasts:readList auto-reforecast watches

Sharing and notifications

MethodPathScopeWhat it does
GET/questions/{question_id}/share-linksquestions:writeList a question's share links
POST/questions/{question_id}/share-linksquestions:writeCreate a share link
GET/questions/{question_id}/share-links/{link_id}questions:writeGet a share link
PATCH/questions/{question_id}/share-links/{link_id}questions:writeEdit a share link
DELETE/questions/{question_id}/share-links/{link_id}questions:writeDelete a share link
POST/questions/{question_id}/share-links/{link_id}/revokequestions:writeRevoke a share link
GET/questions/{question_id}/subscriptionquestions:readGet your notification subscription
PUT/questions/{question_id}/subscriptionquestions:writeFollow a public question
DELETE/questions/{question_id}/subscriptionquestions:writeUnfollow a public question

Webhooks

MethodPathScopeWhat it does
GET/webhook-deliveriesforecasts:readList webhook deliveries
GET/webhook-deliveries/{delivery_id}forecasts:readGet a webhook delivery
GET/webhook-deliveries/{delivery_id}/attemptsforecasts:readList a delivery's attempts
POST/webhook-deliveries/{delivery_id}/replayforecasts:writeReplay a webhook delivery
GET/webhooksforecasts:readList webhooks
POST/webhooksforecasts:writeRegister a webhook
GET/webhooks/{webhook_id}forecasts:readGet a webhook
PATCH/webhooks/{webhook_id}forecasts:writeEdit a webhook
DELETE/webhooks/{webhook_id}forecasts:writeDelete a webhook
POST/webhooks/{webhook_id}/rotateforecasts:writeRotate a webhook's signing secret
POST/webhooks/{webhook_id}/testforecasts:writeSend a test webhook

Connections

MethodPathScopeWhat it does
GET/connectionsconnections:readList connections
POST/connectionsconnections:writeAdd a connection
GET/connections/{connection_id}connections:readGet a connection
PATCH/connections/{connection_id}connections:writeEdit a connection
DELETE/connections/{connection_id}connections:writeDelete a connection
POST/connections/{connection_id}/testconnections:writeTest a connection
PUT/connections/{connection_id}/member-switchconnections:writeSwitch an organization connection for yourself
POST/connections/{connection_id}/sign-inconnections:writeStart a connection sign-in

Forecast presets

MethodPathScopeWhat it does
GET/forecast-presetsforecasts:readList forecast presets
POST/forecast-presetsforecasts:writeAdd a forecast preset
GET/forecast-presets/defaultsforecasts:readGet the default forecast presets
PUT/forecast-presets/defaultsforecasts:writeSet the default forecast presets
GET/forecast-presets/{preset_id}forecasts:readGet a forecast preset
PATCH/forecast-presets/{preset_id}forecasts:writeEdit a forecast preset
DELETE/forecast-presets/{preset_id}forecasts:writeDelete a forecast preset
POST/forecast-presets/{preset_id}/filesforecasts:writeAdd a file to a forecast preset
GET/forecast-presets/{preset_id}/files/{file_id}forecasts:readGet a forecast preset file
PATCH/forecast-presets/{preset_id}/files/{file_id}forecasts:writeEdit a forecast preset file
DELETE/forecast-presets/{preset_id}/files/{file_id}forecasts:writeDelete a forecast preset file

Question templates

MethodPathScopeWhat it does
GET/question-templatesquestions:readList question templates
POST/question-templatesquestions:writeAdd a question template
POST/question-templates/from-questionquestions:writeSave a question as a template
GET/question-templates/{template_id}questions:readGet a question template
PATCH/question-templates/{template_id}questions:writeEdit a question template
DELETE/question-templates/{template_id}questions:writeDelete a question template
POST/question-templates/{template_id}/applyquestions:readShow what a question template gives
POST/question-templates/{template_id}/fillquestions:writeFill a template's blanks with the AI
POST/question-templates/{template_id}/filesquestions:writeAdd a file to a question template
GET/question-templates/{template_id}/files/{file_id}questions:readGet a question template file
PATCH/question-templates/{template_id}/files/{file_id}questions:writeEdit a question template file
DELETE/question-templates/{template_id}/files/{file_id}questions:writeDelete a question template file

Decisions

MethodPathScopeWhat it does
GET/decisionsquestions:readList decisions
POST/decisionsquestions:writeCreate a decision
GET/decisions/{decision_id}questions:readRead a decision
PATCH/decisions/{decision_id}questions:writeEdit a decision
POST/decisions/{decision_id}/archivequestions:writeArchive a decision
POST/decisions/{decision_id}/unarchivequestions:writeUnarchive a decision
POST/decisions/{decision_id}/generatequestions:writeDraft more questions
PATCH/decisions/{decision_id}/drafts/{draft_id}questions:writeEdit a question draft
POST/decisions/{decision_id}/drafts/discardquestions:writeDiscard question drafts
POST/decisions/{decision_id}/drafts/keepquestions:writeKeep question drafts
POST/decisions/{decision_id}/forecastsforecasts:writeStart forecasts for a decision
POST/decisions/{decision_id}/forecasts/estimateforecasts:readPrice forecasts for a decision
POST/decisions/{decision_id}/nodesquestions:writePut a question on a decision
DELETE/decisions/{decision_id}/nodes/{node_id}questions:writeTake a question off a decision
POST/decisions/{decision_id}/linksquestions:writeLink two questions on a decision
PATCH/decisions/{decision_id}/links/{link_id}questions:writeEdit a failed conditional link
DELETE/decisions/{decision_id}/links/{link_id}questions:writeRemove a link
POST/decisions/{decision_id}/notesquestions:writeAdd a note to a decision
PATCH/decisions/{decision_id}/notes/{note_id}questions:writeEdit a decision note
DELETE/decisions/{decision_id}/notes/{note_id}questions:writeRemove a decision note
GET/decisions/{decision_id}/commentsquestions:readList the comments on a decision
POST/decisions/{decision_id}/commentsquestions:writeComment on a decision
PATCH/decisions/{decision_id}/comments/{comment_id}questions:writeEdit a comment
DELETE/decisions/{decision_id}/comments/{comment_id}questions:writeDelete a comment
GET/decisions/{decision_id}/conditionsquestions:readList the conditions on a decision
POST/decisions/{decision_id}/conditionsquestions:writeAdd a condition to a decision
PATCH/decisions/{decision_id}/conditions/{condition_id}questions:writeChange a condition or arm it again
DELETE/decisions/{decision_id}/conditions/{condition_id}questions:writeDelete a condition
GET/decisions/{decision_id}/summaryquestions:readRead a decision summary
POST/decisions/{decision_id}/summary/refreshquestions:writeWrite a new decision summary
PUT/decisions/{decision_id}/liveforecasts:writeTurn live mode on or off
PUT/decisions/{decision_id}/followquestions:writeFollow or unfollow a decision

Account

MethodPathScopeWhat it does
GET/creditsforecasts:readGet organization credits

Analytics cookies

Prolepsia uses analytics cookies to learn how people use the app. They link your visits together and can record how you move through pages, with all text and typed values hidden. Until you accept, Prolepsia only counts visits anonymously, without cookies. Read the privacy policy