Markets
GET /api/v1/markets/:id
Returns full details for a single market, including best bid/ask.
Auth: None
Rate limit: v1: 60 req / 60 s per IP
Cache: s-maxage=10, stale-while-revalidate=30
Path parameters
| Param | Type | Description |
|---|---|---|
id | UUID | Market identifier |
Response
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"title": "Will Reform Party win the most seats?",
"description": "Resolves YES if Reform Party gets the most Riigikogu seats in the 2026 election.",
"category": "estonian_politics",
"status": "open",
"eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"bestBid": 58,
"bestAsk": 61,
"lastTradePrice": 58,
"yesVolume": 124500,
"noVolume": 87300,
"closeTime": "2026-03-01T20:00:00.000Z",
"createdAt": "2025-11-15T09:00:00.000Z",
"resolvedOutcome": null,
"thumbnailId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"thumbnailUrl": "https://cdn.juhus.ee/thumbnails/reform-2026.webp"
}| Field | Type | Description |
|---|---|---|
id | UUID string | Market identifier |
title | string | Market title |
description | string | Market description |
category | string | Category slug |
status | string | open | paused | closed | resolved |
eventId | UUID string | null | Parent event, or null for standalone |
bestBid | integer | null | Highest open YES buy order price (1–99) |
bestAsk | integer | null | Lowest open YES sell order price (1–99), derived from best NO bid |
lastTradePrice | integer | null | Most recent matched price (1–99) |
yesVolume | integer | Total YES-side volume in EUR-cents |
noVolume | integer | Total NO-side volume in EUR-cents |
closeTime | ISO 8601 | null | Scheduled trading close time |
createdAt | ISO 8601 | Creation timestamp |
resolvedOutcome | "yes" | "no" | "custom" | null | Resolution outcome; null while unresolved |
thumbnailId | UUID string | null | Thumbnail image id; null if none set |
thumbnailUrl | string | null | Thumbnail image URL; null if none set |
Errors
| Status | Body | When |
|---|---|---|
400 | { "error": "Invalid id" } | id is not a valid UUID |
404 | { "error": "Market not found" } | No market with that id |
GET /api/v1/markets/:id/orderbook
Returns the aggregated order book: all open YES buy orders (bids) and all open YES sell orders (asks, derived from NO buys).
Auth: None
Rate limit: v1: 60 req / 60 s per IP
Cache: s-maxage=5, stale-while-revalidate=10
Path parameters
| Param | Type | Description |
|---|---|---|
id | UUID | Market identifier |
Response
{
"bids": [
{ "price": 58, "quantity": 120 },
{ "price": 55, "quantity": 340 },
{ "price": 50, "quantity": 500 }
],
"asks": [
{ "price": 61, "quantity": 80 },
{ "price": 65, "quantity": 200 }
]
}bids are open YES buy orders, sorted descending by price (highest first).
asks are open YES sell orders (stored as NO buys), sorted ascending by price (lowest first). The ask price is already converted to YES-price space: 100 - stored_no_price.
| Field | Type | Description |
|---|---|---|
bids | array | YES buy orders aggregated by price level |
asks | array | YES sell orders aggregated by price level |
bids[].price | integer | Price level (1–99) |
bids[].quantity | integer | Total unfilled shares at this level |
asks[].price | integer | Price level in YES space (1–99) |
asks[].quantity | integer | Total unfilled shares at this level |
Errors
| Status | Body | When |
|---|---|---|
400 | { "error": "Invalid id" } | id is not a valid UUID |
404 | { "error": "Market not found" } | No market exists with that id. An empty order book on an existing market returns 200 with empty bids and asks arrays. |
GET /api/v1/markets/:id/trades
Returns paginated trade history for a market, most recent first.
Auth: None
Rate limit: v1: 60 req / 60 s per IP
Cache: s-maxage=5, stale-while-revalidate=10
Path parameters
| Param | Type | Description |
|---|---|---|
id | UUID | Market identifier |
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Results per page (1–50) |
Response
{
"data": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"price": 58,
"quantity": 10,
"createdAt": "2026-01-15T14:32:00.000Z"
}
],
"meta": {
"total": 312,
"page": 1,
"limit": 20,
"hasMore": true
}
}| Field | Type | Description |
|---|---|---|
id | UUID string | Trade identifier |
price | integer | Matched price (1–99) |
quantity | integer | Number of shares traded |
createdAt | ISO 8601 | When the trade occurred |
Errors
| Status | Body | When |
|---|---|---|
400 | { "error": "Invalid id" } | id is not a valid UUID |
404 | { "error": "Market not found" } | No market with that id |
GET /api/v1/markets/:id/stream
Server-Sent Events stream. Sends an initial snapshot immediately on connect, then re-sends a full snapshot after every trade or order book change.
Auth: None
Rate limit: sse:market: 120 connections / 60 s per IP
The v1 path streams directly, public and unauthenticated, for server-side and API-key clients. From the browser, use the same-origin proxy at /api/stream/market/:id: EventSource can’t send custom headers, and the proxy avoids CORS.
A non-200 is returned as a real HTTP status before the stream opens: a malformed UUID returns 400, an unknown market returns 404.
Path parameters
| Param | Type | Description |
|---|---|---|
id | UUID | Market identifier |
Event frame shape
Each data: frame is a JSON snapshot:
{
"orderBook": {
"yesBids": [
{ "price": 58, "quantity": 120 },
{ "price": 55, "quantity": 340 }
],
"noBids": [
{ "price": 42, "quantity": 80 },
{ "price": 38, "quantity": 200 }
]
},
"recentTrades": [
{
"price": 58,
"quantity": 10,
"createdAt": "2026-01-15T14:32:00.000Z",
"takerName": "Alice",
"takerHandle": "alice",
"takerPrivate": false,
"side": "yes"
}
],
"tradersCount": 47
}orderBook.noBids are raw NO buy orders sorted descending by stored price. To convert a NO bid to a YES ask price: 100 - noBid.price.
| Field | Type | Description |
|---|---|---|
orderBook.yesBids | array | Open YES buy orders aggregated by price, sorted descending |
orderBook.noBids | array | Open NO buy orders aggregated by stored price, sorted descending |
recentTrades | array | Up to 50 most recent trades, newest first |
recentTrades[].price | integer | Matched price (1–99) |
recentTrades[].quantity | integer | Shares traded |
recentTrades[].createdAt | ISO 8601 | Trade time |
recentTrades[].takerName | string | null | Taker display name; null if private profile |
recentTrades[].takerHandle | string | null | Taker handle; null if private profile |
recentTrades[].takerPrivate | boolean | true if taker has a private profile |
recentTrades[].side | "yes" | "no" | null | Side the taker bought; null for legacy trades |
tradersCount | integer | Distinct traders who have ever traded this market |
Keep-alive
The server writes : ping comments every 25 seconds. Reconnect on any network error. The stream sends a fresh snapshot on each connection.
Errors
| Status | Body | When |
|---|---|---|
400 | { "error": "INVALID_ID" } | id is not a valid UUID |
404 | { "error": "Market not found" } | No market with that id |
429 | { "error": "Too many requests" } | sse:market rate limit exceeded |