api.outcome.sh

The whole API is one endpoint.

POST SQL, get JSON rows back. The full reference fits on this page: two endpoints, five tables, and an MCP server.

POST/v1/executeGET/v1/schemaPOST/v1/mcp
01getting started

First query

  1. 01Sign in at app.outcome.sh. Magic-link email, no password and no credit card.
  2. 02Create a key under API Keys in the dashboard. Keys are shown once.
  3. 03POST your SQL:
quickstart.sh
curl -X POST https://api.outcome.sh/v1/execute \
  -H "Authorization: Bearer $OUTCOME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT max(created_time) AS last_trade FROM trades"}'

Playground

The playground runs the same queries in the browser, with the live schema, charts, and an AI analyst. No key required.

MCP

Claude, Cursor, and other MCP clients can explore the schema and run queries through the MCP server.

02authentication

API keys

Every request authenticates with an API key in the Authorization header. Keys are created and revoked in the dashboard, belong to your team, and spend the team credit balance. The same key works for the execute endpoint and the MCP server.

header
Authorization: Bearer $OUTCOME_API_KEY
  • Each key is rate limited to 60 requests per second.
  • A leaked key can spend your credits. Revoke it from the API Keys page.
  • GET /v1/schema is public and needs no key.
03the endpoint

Execute a query

POST/v1/execute

Runs a single read-only SQL statement against the warehouse and returns the rows as a JSON array. SELECT, WITH, EXPLAIN, and SHOW are permitted; anything that writes, touches system tables, or reaches external data is rejected before it runs.

Request body

querystringrequired

A single read-only SQL statement. JSON responses are capped at 10,000 rows and 8 MiB, with a 1 MiB limit per row. Execution is limited to 60 minutes; available credits can impose a shorter limit. SQL SETTINGS, FORMAT, and INTO clauses are rejected.

finalbooleanoptional

When true, applies the ClickHouse FINAL modifier so markets and trades return merged, deduplicated rows instead of occasional pre-merge duplicates. Slower and scans more; use it only when exact dedup matters.

Allow at least 61 minutes for client request timeouts so the server can return a result or timeout error after the full query window.

Example

request.sh
curl -X POST https://api.outcome.sh/v1/execute \
  -H "Authorization: Bearer $OUTCOME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT ticker, yes_price FROM markets ORDER BY volume_24h DESC LIMIT 5"}'
200 OK
[
  { "ticker": "KXMENWORLDCUP-26-ES", "yes_price": 0.999 },
  { "ticker": "KXTECHLAYOFFS-26", "yes_price": 0.911 },
  { "ticker": "KXUSAALIENS-27", "yes_price": 0.074 }
]

Usage headers

Every successful response reports what the query cost:

X-Credits-Usedintegeralways

Credits charged for this query.

X-Duration-Msintegeralways

Wall-clock execution time in milliseconds.

X-Bytes-Readintegeralways

Bytes the warehouse scanned to answer the query.

X-Credits-Remainingintegerwhen charged

Team balance after the charge. Omitted when the query was not charged.

response headers
HTTP/2 200
X-Credits-Used: 3
X-Duration-Ms: 118
X-Bytes-Read: 204811520
X-Credits-Remaining: 49997
04large results

Streaming and exports

POST/v1/execute/streamPOST/v1/exports

Both endpoints accept the same query and optional final fields as /v1/execute and use the same authentication and query billing. They avoid the JSON endpoint’s 10,000-row and 8 MiB response limits. Individual rows still have a 1 MiB limit: return ordinary rows instead of collecting results into a large groupArray or JSON string. Query memory and time limits still apply.

Stream NDJSON

/v1/execute/stream sends one row record per line, followed by a completion record with the final row count, usage, and billing. Consume it incrementally. A 200 status alone does not mean the query succeeded: reject error records, missing or unsuccessful completion, row-count mismatches, and truncated transfers. Usage comes from the completion record, not response headers. Disconnecting cancels database consumption.

NDJSON response
{"type":"row","row":{"number":0}}
{"type":"complete","success":true,"row_count":1,"bytes_read":8,"duration_ms":12,"credits_used":1,"charged":true,"credits_remaining":99}

Create a downloadable export

POST /v1/exports runs the query and writes a file to private storage. Set format to csv for spreadsheets or ndjson for JSON Lines (the API default). An optional name uses your query title for the download filename, normalized with a UTC timestamp; new JSON Lines files use .jsonl. Keep the request open until it returns 201 with id, download_url, format, and completion. The file is published only after successful completion. JSON Lines includes the final completion record; CSV contains column headers and data rows, with completion and billing in the API response. Download directly from the signed URL; loading the file into a browser array or Blob defeats the memory benefit. When the playground has complete results for the current query, Download full CSV saves those results without rerunning or using more credits. Otherwise it explains the cost before running a new export, preserving any SQL LIMIT.

export.sh
curl --max-time 3660 https://api.outcome.sh/v1/exports \
  -H "Authorization: Bearer $OUTCOME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"SELECT number FROM numbers(20001)","format":"csv"}'

CSV preserves query-column order, quotes commas and newlines, represents null as an empty cell, and writes nested values as JSON text. Formula-like string cells are prefixed with an apostrophe for spreadsheets; use JSON Lines for exact original text. The table view keeps its 10,000-row cap; downloads bypass that cap.

POST/v1/exports/results

Save an existing JSON result array using its original response bytes as the body. Set ?format=csv or ?format=ndjson and an optional name. This stores the supplied data without running SQL or charging credits, and adds the file to Exports. The response uses the same completion contract with zero credits used. Uploads are limited to 10,000 rows, 8 MiB total, 1 MiB per row, and two minutes. CSV columns follow sorted JSON keys. The playground reuses only complete results matching the exact SQL and account; older results without that metadata cannot be reused safely. An upload failure never automatically starts a new query.

GET/v1/exports

List your team’s completed files, including older exports and files whose creation response was interrupted. Each page contains up to 100 exports with id, format, filename, size_bytes, created_at, and expires_at. Pass the returned cursor as ?cursor=... to continue in storage-key order. The Exports page under Workspace and in the profile menu uses this endpoint.

GET/v1/exports/{id}

Save the export ID. An authenticated GET returns a fresh download_url for a completed export owned by your team without rerunning or rebilling the query. Links last up to one hour and files are retained for seven days. Signed links grant access to the file; share them only with intended recipients.

If creation fails after returning X-Export-ID, check that ID before submitting another POST: the file may already be complete and the query billed. A lost connection before headers may leave the outcome unknown. Creating another export always starts a new query. A 404 lookup means no completed export is available to your team; a 503 can mean capacity is busy or exports are not configured. Cancellation requests stop ongoing work, but cannot undo a completed query or charge.

05introspection

The live schema

GET/v1/schema

Returns every queryable table with its columns and exact ClickHouse types. Public, no key required. Feed it to a code generator or an agent before it writes SQL.

request.sh
curl https://api.outcome.sh/v1/schema
200 OK
{
  "tables": [
    {
      "name": "markets",
      "columns": [
        { "name": "venue", "type": "LowCardinality(String)" },
        { "name": "ticker", "type": "String" },
        { "name": "yes_price", "type": "Float64" },
        ...
      ]
    },
    ...
  ]
}
06data reference

The tables

Kalshi is live today, more venues are on the way. The venue column tells rows apart, so queries keep working as venues are added.

Outcome is an analytical warehouse, not a trading feed: new markets, trades, and book updates land within five minutes of the venue. Build research and dashboards on it, not HFT.

Types below are simplified. For exact ClickHouse types, use GET /v1/schema.

markets

One row per market, updated continuously while it trades: title, status, current prices, volume, liquidity, and the rules text that defines resolution.

markets.sql
-- What does the market believe right now?
SELECT title, round(yes_price * 100, 1) AS chance
FROM markets
WHERE status = 'open'
ORDER BY volume_24h DESC
LIMIT 5
columns
  • venuetext
  • tickertext
  • event_tickertext
  • titletext
  • yes_sub_titletext
  • no_sub_titletext
  • statustext
  • resulttext
  • market_typetext
  • strike_typetext
  • yes_pricefloat
  • no_pricefloat
  • yes_bidtext
  • yes_asktext
  • no_bidtext
  • no_asktext
  • volumefloat
  • volume_24hfloat
  • liquiditytext
  • yes_liquidityfloat
  • no_liquidityfloat
  • notional_valuetext
  • rules_primarytext
  • rules_secondarytext
  • open_timetimestamp
  • created_timetimestamp
  • close_timetimestamp
  • expiration_timetimestamp
  • expected_expiration_timetimestamp
  • updated_timetimestamp
  • is_deleteduint8

trades

Every trade at tick level, from a market opening to its resolution. Prices are the YES and NO contract prices in dollars, count is the number of contracts.

trades.sql
-- Replay one market's price path
SELECT created_time, yes_price
FROM trades
WHERE ticker = 'KXMENWORLDCUP-26-ES'
ORDER BY created_time
columns
  • venuetext
  • trade_idtext
  • tickertext
  • yes_pricefloat
  • no_pricefloat
  • countfloat
  • taker_sidetext
  • taker_book_sidetext
  • taker_outcome_sidetext
  • created_timetimestamp
  • is_deleteduint8

orderbooks

The latest order book snapshot per market: best bids and asks on both sides, spreads, available liquidity, and the full depth levels as JSON.

orderbooks.sql
-- Tightest markets right now
SELECT ticker, yes_bid, yes_ask, spread_yes
FROM orderbooks
WHERE spread_yes IS NOT NULL
ORDER BY spread_yes ASC
LIMIT 10
columns
  • venuetext
  • tickertext
  • yes_bidfloat
  • yes_askfloat
  • no_bidfloat
  • no_askfloat
  • spread_yesfloat
  • spread_nofloat
  • yes_liquidityfloat
  • no_liquidityfloat
  • yes_levelstext
  • no_levelstext
  • updated_timetimestamp

historical_orderbooks

Every order book update, sequenced. The same shape as orderbooks plus a sequence_number, so you can reconstruct the book at any point in time.

historical_orderbooks.sql
-- Liquidity over time for one market
SELECT updated_time, yes_liquidity, no_liquidity
FROM historical_orderbooks
WHERE ticker = 'KXMENWORLDCUP-26-ES'
ORDER BY sequence_number
columns
  • venuetext
  • tickertext
  • yes_bidfloat
  • yes_askfloat
  • no_bidfloat
  • no_askfloat
  • spread_yesfloat
  • spread_nofloat
  • yes_liquidityfloat
  • no_liquidityfloat
  • yes_levelstext
  • no_levelstext
  • sequence_numberbigint
  • updated_timetimestamp

resolved_markets

The final result of every settled market, one row per resolution.

resolved_markets.sql
-- Resolution outcomes across all settled markets
SELECT result, count() AS markets
FROM resolved_markets
GROUP BY result
columns
  • venuetext
  • tickertext
  • resulttext
07model context protocol

The MCP server

POST/v1/mcp

A remote MCP server for Claude, Cursor, and any other MCP client: the agent discovers the schema and queries the market itself. Streamable HTTP (protocol 2025-06-18), authenticated with your API key, with the same read-only rules, rate limits, and billing as the execute endpoint. Nothing to install or host.

Connect a client

terminal
claude mcp add --transport http outcome https://api.outcome.sh/v1/mcp \
  --header "Authorization: Bearer YOUR_API_KEY"

Tools

list_tablesfree

List every public table with its columns and types. Takes no arguments.

describe_tablefree

Return the schema of a single table by name.

preview_tablebilled

Fetch a bounded sample of rows (up to 100) from one table, optionally restricted to specific columns. The server generates the SQL; no arbitrary queries.

run_querybilled

Execute a read-only SQL query with exactly the same rules, limits, and billing as POST /v1/execute. Returns rows plus row count, bytes read, credits used, and duration.

A typical session: list_tables once, a preview or two, then run_query. Billed tools report credits used in their results.

08billing

Credits and limits

1,000 credits equal one dollar. Only successful queries are charged: one base credit, plus one credit per 100 MB scanned, plus a time cost that rises steeply past a minute:

query durationapprox. time cost
1 second1 credit · $0.001
30 seconds11 credits · $0.011
60 seconds1,000 credits · $1.00
5 minutes5,000 credits · $5.00
10,000 rowsper result set
60 minutesper query
60 req/sper API key

Set spend limits and auto reload in Billing.

09errors

Errors

Errors return a JSON body with an error message and, where a machine needs to react, a stable code:

429 Too Many Requests
{
  "error": "team spend limit exceeded",
  "code": "team_spend_limit_exceeded"
}
400only SELECT/WITH/EXPLAIN/SHOW statements are permitted

The body is malformed, the query is empty, the SQL fails to parse, or the statement is not read-only.

401missing or invalid API key

No Authorization header, or the key is wrong or revoked.

402insufficient credit balance

The team balance cannot cover the query, or the query ran past its byte budget.

413result set exceeds the maximum of 10000 rows

The JSON result exceeds 10,000 rows or 8 MiB. Add a LIMIT, stream NDJSON, or create an export. Individual rows are limited to 1 MiB on every path.

429team spend limit exceeded

The key exceeded 60 requests per second, or the query would exceed the spend limit set in Billing.

503billing unavailable / query capacity exhausted

Billing is unavailable or query capacity is busy. Retry with backoff; check any returned export ID before creating another export.

504query exceeded the time limit

The query hit the 60 minute limit or a shorter limit based on available credits.

Start querying

Magic-link sign in, no credit card.

Get started

Questions or missing data? Academics and researchers get free credits. Write to support@outcome.sh.