# WebSocket API Reference

## WebSocket API Reference

### Introduction

The Outpoll WebSocket API provides real-time streaming data for order books, probability charts, order updates, balance changes, and trade feeds.

All WebSocket connections use the `wss://` protocol.

***

### Authentication

Some endpoints are **public** (no authentication required): Order Book, Chart, Activity Feed, Last Trades.

Private endpoints (Order Updates, Balance Updates, Order Feed) require API key authentication.

#### API Key (in-band)

```json
{
  "t": "AUTH",
  "d": {
    "apiKey": "<API_KEY>",
    "signature": "<SIGNATURE>",
    "timestamp": "<UNIX_EPOCH_SECONDS>"
  }
}
```

Signature is computed as:

```
message   = timestamp + "GET" + path
signature = base64url_no_padding(HMAC-SHA256(base64url_decode(api_secret), message))
```

Where `path` is the WebSocket endpoint path (e.g. `/order/ws`, `/balance/ws`). Timestamp drift tolerance is ±30 seconds.

#### Sign with API Key (Python)

```python
import hmac, hashlib, base64, time

api_key = "op_k_your_api_key"
api_secret = "your_api_secret"

def ws_auth_message(path: str) -> dict:
    timestamp = str(int(time.time()))
    message = timestamp + "GET" + path
    secret_padded = api_secret + "=" * (4 - len(api_secret) % 4) if len(api_secret) % 4 else api_secret
    secret_bytes = base64.urlsafe_b64decode(secret_padded)
    signature = base64.urlsafe_b64encode(
        hmac.new(secret_bytes, message.encode(), hashlib.sha256).digest()
    ).decode().rstrip("=")
    return {
        "t": "AUTH",
        "d": {
            "apiKey": api_key,
            "signature": signature,
            "timestamp": timestamp,
        },
    }
```

> All WebSocket examples below use the `websockets` library: `pip install websockets`

***

## Order Book

Real-time price updates for prediction market outcomes. No authentication required.

```
wss://order-book.outpoll.com/event/ws
```

All messages follow a common envelope:

```json
{
  "t": "<TYPE>",
  "e": "<EVENT_ID>",
  "d": [...],
  "r": "<ERROR_MESSAGE>"
}
```

| Field | Type   | Description                      |
| ----- | ------ | -------------------------------- |
| t     | string | Message type                     |
| e     | string | Event ID                         |
| d     | array  | Payload (list of outcome prices) |
| r     | string | Error message (only on errors)   |

**Message Types**

| Type                | Direction | Description               |
| ------------------- | --------- | ------------------------- |
| SUBSCRIBE           | Client    | Subscribe to an event     |
| SUBSCRIBED          | Server    | Subscription confirmed    |
| UNSUBSCRIBE         | Client    | Unsubscribe from an event |
| UNSUBSCRIBED        | Server    | Unsubscription confirmed  |
| REQUEST\_DATA       | Client    | Request current prices    |
| PRICES\_DATA        | Server    | Full price snapshot       |
| PRICES\_DATA\_DELTA | Server    | Incremental price update  |
| PING                | Server    | Heartbeat                 |
| ERROR               | Server    | Error message             |

***

### Subscribe

Subscribe to price updates for an event.

**Client -> Server**

```json
{
  "t": "SUBSCRIBE",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a"
}
```

**Server -> Client** (confirmation)

```json
{
  "t": "SUBSCRIBED",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a"
}
```

***

### Price Data

After subscribing, the server sends `PRICES_DATA` with the current state, and `PRICES_DATA_DELTA` on each change.

**Server -> Client**

```json
{
  "t": "PRICES_DATA",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "d": [
    {
      "o": "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76",
      "c": 0.55,
      "b": 0.54,
      "a": 0.56
    },
    {
      "o": "97b06d16-aeff-4dd4-b08b-1ba30a2b8895",
      "c": 0.45,
      "b": 0.44,
      "a": 0.46
    }
  ]
}
```

| Field | Type   | Description               |
| ----- | ------ | ------------------------- |
| o     | string | Outcome ID                |
| c     | number | Chance (last trade price) |
| b     | number | Best bid                  |
| a     | number | Best ask                  |

> `PRICES_DATA_DELTA` has the same format but only includes outcomes whose prices changed.

**Python**

```python
import asyncio, json, websockets

async def order_book():
    uri = "wss://order-book.outpoll.com/event/ws"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "t": "SUBSCRIBE",
            "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
        }))

        async for msg in ws:
            data = json.loads(msg)
            if data["t"] in ("PRICES_DATA", "PRICES_DATA_DELTA"):
                for p in data["d"]:
                    print(f"Outcome {p['o']}: chance={p['c']}, bid={p['b']}, ask={p['a']}")
            elif data["t"] == "PING":
                pass

asyncio.run(order_book())
```

***

## Chart (Chance History)

Real-time probability chart data and historical OHLC-style probability aggregations. No authentication required.

```
wss://chance-history-service.outpoll.com/ws
```

All messages follow a common envelope:

```json
{
  "t": "<TYPE>",
  "e": "<EVENT_ID>",
  "o": ["<OUTCOME_ID_1>", "<OUTCOME_ID_2>"],
  "d": { ... },
  "r": "<ERROR_MESSAGE>"
}
```

| Field | Type      | Description                      |
| ----- | --------- | -------------------------------- |
| t     | string    | Message type                     |
| e     | string    | Event ID (UUID)                  |
| o     | string\[] | Outcome IDs (UUIDs)              |
| d     | object    | Payload (varies by message type) |
| r     | string    | Error message (only on errors)   |

***

### Subscribe

Subscribe to real-time probability updates for specific outcomes.

**Client -> Server**

```json
{
  "t": "SUBSCRIBE",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": [
    "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76",
    "97b06d16-aeff-4dd4-b08b-1ba30a2b8895"
  ]
}
```

| Field | Type      | Required | Description         |
| ----- | --------- | -------- | ------------------- |
| t     | string    | Yes      | `SUBSCRIBE`         |
| e     | string    | Yes      | Event ID            |
| o     | string\[] | Yes      | List of outcome IDs |

**Server -> Client** (confirmation + initial data)

On subscribe, the server sends:

1. A `probability` message with the last known value for each outcome
2. A `success` confirmation

```json
{
  "t": "probability",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": ["b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"],
  "d": {
    "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76": {
      "t": 1712160000,
      "p": 0.65
    }
  }
}
```

```json
{
  "t": "success",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": ["b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"],
  "d": { "message": "subscribe successful" }
}
```

**Python**

```python
import asyncio, json, websockets

async def chart_stream():
    uri = "wss://chance-history-service.outpoll.com/ws"
    async with websockets.connect(uri) as ws:
        # Subscribe
        await ws.send(json.dumps({
            "t": "SUBSCRIBE",
            "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
            "o": [
                "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76",
                "97b06d16-aeff-4dd4-b08b-1ba30a2b8895",
            ],
        }))

        # Listen for probability updates
        async for msg in ws:
            data = json.loads(msg)
            if data["t"] == "probability":
                print(f"Probability: {data['d']}")
            elif data["t"] == "UPDATE_HISTORY":
                print(f"History point: {data['d']}")

asyncio.run(chart_stream())
```

***

### Unsubscribe

Stop receiving probability updates for specific outcomes.

**Client -> Server**

```json
{
  "t": "UNSUBSCRIBE",
  "o": [
    "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"
  ]
}
```

**Server -> Client**

```json
{
  "t": "success",
  "o": ["b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"],
  "d": { "message": "unsubscribe successful" }
}
```

**Python**

```python
# Inside an active WebSocket connection:
await ws.send(json.dumps({
    "t": "UNSUBSCRIBE",
    "o": ["b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"],
}))
```

***

### Probability Update

Pushed in real-time when the probability of a subscribed outcome changes.

**Server -> Client**

```json
{
  "t": "probability",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": ["b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"],
  "d": {
    "t": 1712160300,
    "p": 0.67
  }
}
```

| Field | Type   | Description                    |
| ----- | ------ | ------------------------------ |
| d.t   | number | Timestamp (Unix epoch seconds) |
| d.p   | number | Probability (0.0–1.0)          |

***

### History Update

Pushed when a new aggregated data point is available for a subscribed outcome.

**Server -> Client**

```json
{
  "t": "UPDATE_HISTORY",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": ["b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"],
  "d": {
    "t": 1712160300,
    "p": 0.67
  }
}
```

Same data format as probability updates.

***

### Request Historical Data

Request aggregated historical probability data for chart rendering.

**Client -> Server**

```json
{
  "t": "HISTORY",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": [
    "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76",
    "97b06d16-aeff-4dd4-b08b-1ba30a2b8895"
  ],
  "d": {
    "p": "1H",
    "f": "2026-01-01T00:00:00Z",
    "t": "2026-01-01T12:00:00Z"
  }
}
```

| Field | Type      | Required | Description                                             |
| ----- | --------- | -------- | ------------------------------------------------------- |
| t     | string    | Yes      | `HISTORY`                                               |
| e     | string    | Yes      | Event ID                                                |
| o     | string\[] | Yes      | Outcome IDs to fetch                                    |
| d.p   | string    | No       | Aggregation period (see table below). Omit for raw data |
| d.f   | string    | Yes      | Start time (ISO 8601)                                   |
| d.t   | string    | Yes      | End time (ISO 8601)                                     |

#### Aggregation Periods

| Code | Duration   |
| ---- | ---------- |
| 1s   | 1 second   |
| 5s   | 5 seconds  |
| 5M   | 5 minutes  |
| 10M  | 10 minutes |
| 30M  | 30 minutes |
| 1H   | 1 hour     |
| 3H   | 3 hours    |
| 4H   | 4 hours    |
| 6H   | 6 hours    |
| 1d   | 1 day      |
| 7d   | 1 week     |
| 1m   | 1 month    |

> You can also pass a custom number of seconds as a string (e.g. `"300"` for 5-minute buckets).

**Server -> Client**

```json
{
  "t": "HISTORY_RESPONSE",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": [
    "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76",
    "97b06d16-aeff-4dd4-b08b-1ba30a2b8895"
  ],
  "d": {
    "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76": [
      { "t": 1735689600, "p": 0.50 },
      { "t": 1735693200, "p": 0.52 },
      { "t": 1735696800, "p": 0.55 },
      { "t": 1735700400, "p": 0.53 }
    ],
    "97b06d16-aeff-4dd4-b08b-1ba30a2b8895": [
      { "t": 1735689600, "p": 0.30 },
      { "t": 1735693200, "p": 0.32 },
      { "t": 1735696800, "p": 0.28 }
    ]
  }
}
```

Response data is a map of `outcomeId -> array of data points`:

| Field | Type   | Description                     |
| ----- | ------ | ------------------------------- |
| t     | number | Period end (Unix epoch seconds) |
| p     | number | Close probability (0.0–1.0)     |

> The first point in each array is an **anchor** — the last known probability value at or before the requested `from` time. This allows charts to draw a continuous line from the left edge.

> If no data exists in the requested range, the server returns two points (at `from` and `to`) with the last known probability, creating a flat line.

**Python — Request Historical Data**

```python
import asyncio, json, websockets

async def get_history():
    uri = "wss://chance-history-service.outpoll.com/ws"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "t": "HISTORY",
            "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
            "o": ["b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"],
            "d": {
                "p": "1H",
                "f": "2026-01-01T00:00:00Z",
                "t": "2026-01-01T12:00:00Z",
            },
        }))

        async for msg in ws:
            data = json.loads(msg)
            if data["t"] == "HISTORY_RESPONSE":
                for outcome_id, points in data["d"].items():
                    print(f"Outcome {outcome_id}:")
                    for pt in points:
                        print(f"  {pt['t']} -> {pt['p']}")
                break

asyncio.run(get_history())
```

***

### Chart History REST Endpoint

The same historical data is also available via REST.

```
GET /api/events/{eventId}/outcomes/{outcomeId}/history
```

**Query Parameters**

| Parameter | Type   | Required | Description                         |
| --------- | ------ | -------- | ----------------------------------- |
| period    | string | Yes      | Aggregation period code (e.g. `1H`) |
| from      | string | Yes      | Start time (ISO 8601)               |
| to        | string | Yes      | End time (ISO 8601)                 |

**Example**

```
GET /api/events/54ccea1a-.../outcomes/b21f6fd8-.../history?period=1H&from=2026-01-01T00:00:00Z&to=2026-01-01T12:00:00Z
```

**Response** `200 OK`

```json
[
  { "t": 1735689600, "p": 0.50 },
  { "t": 1735693200, "p": 0.52 },
  { "t": 1735696800, "p": 0.55 }
]
```

**Python**

```python
import requests

event_id = "54ccea1a-16fd-469c-8018-84b375243e8a"
outcome_id = "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"

response = requests.get(
    f"https://chance-history-service.outpoll.com/api/events/{event_id}/outcomes/{outcome_id}/history",
    params={"period": "1H", "from": "2026-01-01T00:00:00Z", "to": "2026-01-01T12:00:00Z"},
)

for point in response.json():
    print(f"{point['t']} -> {point['p']}")
```

***

## Order Updates

Real-time notifications about your order executions and TP/SL status changes.

```
wss://order-service.outpoll.com/order/ws
```

> Requires authentication. Send an `AUTH` message after connecting.

***

### Authenticate

**Option 1 — JWT Token**

```json
{
  "t": "AUTH",
  "d": {
    "apiKey": "<API_KEY>",
    "signature": "<SIGNATURE>",
    "timestamp": "<UNIX_EPOCH_SECONDS>"
  }
}
```

Signature: `base64url_no_padding(HMAC-SHA256(secret, timestamp + "GET" + "/order/ws"))`

**Server -> Client** (success)

```json
{
  "t": "success",
  "d": "authorized",
  "r": null
}
```

**Server -> Client** (failure)

```json
{
  "t": "error",
  "d": {
    "error": "Invalid or missing credentials"
  }
}
```

***

### Order Update

Sent when an order is placed or fails.

**Server -> Client**

```json
{
  "t": "ORDER_UPDATE",
  "d": {
    "i": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "u": "user-id",
    "s": "SUCCESS"
  }
}
```

| Field | Type   | Description                    |
| ----- | ------ | ------------------------------ |
| d.i   | string | Order ID                       |
| d.u   | string | User ID                        |
| d.s   | string | Status — `SUCCESS` or `FAILED` |

***

### TP/SL Update

Sent when a take-profit or stop-loss order changes state.

**Server -> Client**

```json
{
  "t": "TPSL_UPDATE",
  "d": {
    "i": "order-id",
    "u": "user-id",
    "a": "asset-id",
    "q": 100.00,
    "tpp": 0.95,
    "slp": 0.10,
    "s": "ACTIVE"
  }
}
```

| Field | Type   | Description       |
| ----- | ------ | ----------------- |
| d.i   | string | Order ID          |
| d.u   | string | User ID           |
| d.a   | string | Asset ID          |
| d.q   | number | Quantity          |
| d.tpp | number | Take-profit price |
| d.slp | number | Stop-loss price   |
| d.s   | string | Status            |

***

### Server Ping

The server sends a heartbeat every 15 seconds. No response required.

**Server -> Client**

```json
{
  "t": "PING",
  "d": null,
  "r": null
}
```

**Python**

```python
import asyncio, json, websockets

async def order_updates():
    uri = "wss://order-service.outpoll.com/order/ws"
    async with websockets.connect(uri) as ws:
        # Authenticate
        await ws.send(json.dumps(ws_auth_message("/order/ws")))

        async for msg in ws:
            data = json.loads(msg)
            if data["t"] == "success":
                print("Authenticated")
            elif data["t"] == "ORDER_UPDATE":
                print(f"Order {data['d']['i']}: {data['d']['s']}")
            elif data["t"] == "TPSL_UPDATE":
                print(f"TP/SL {data['d']['i']}: {data['d']['s']}")
            elif data["t"] == "PING":
                pass  # heartbeat, ignore

asyncio.run(order_updates())
```

***

## Balance Updates

Real-time balance changes and transaction updates.

```
wss://wallet-mutator-view.outpoll.com/balance/ws
```

> Requires authentication. Send an `AUTH` message after connecting.

***

### Authenticate

**Option 1 — JWT Token**

```json
{
  "t": "AUTH",
  "d": {
    "apiKey": "<API_KEY>",
    "signature": "<SIGNATURE>",
    "timestamp": "<UNIX_EPOCH_SECONDS>"
  }
}
```

Signature: `base64url_no_padding(HMAC-SHA256(secret, timestamp + "GET" + "/balance/ws"))`

After successful authentication, the server sends your current balances wrapped in a success message:

**Server -> Client** (auth success + initial balances)

```json
{
  "t": "success",
  "d": {
    "l": [
      {
        "i": "078dcd98-928d-479f-8110-ff6d27e44de2",
        "b": 1250.50,
        "f": 1200.00
      },
      {
        "i": "e7c4fed5-e7f4-4ec4-9b55-0bb41b83d813",
        "b": 100,
        "f": 100
      }
    ]
  },
  "r": null
}
```

| Field    | Type   | Description              |
| -------- | ------ | ------------------------ |
| t        | string | Message type (`success`) |
| d.l      | array  | List of balances         |
| d.l\[].i | string | Asset ID                 |
| d.l\[].b | number | Balance                  |
| d.l\[].f | number | Free balance             |
| r        | null   | Error (null on success)  |

***

### Balance Update

Sent when any balance changes (trade, deposit, withdrawal).

**Server -> Client**

```json
{
  "u": "user-id",
  "a": "asset-id",
  "aop": 0.50,
  "c": 50.00,
  "b": 1200.50,
  "f": 1150.00
}
```

| Field | Type   | Description        |
| ----- | ------ | ------------------ |
| u     | string | User ID            |
| a     | string | Asset ID           |
| aop   | number | Average open price |
| c     | number | Cost               |
| b     | number | Balance            |
| f     | number | Free balance       |

**Python**

```python
import asyncio, json, websockets

async def balance_updates():
    uri = "wss://wallet-mutator-view.outpoll.com/balance/ws"
    async with websockets.connect(uri) as ws:
        # Authenticate
        await ws.send(json.dumps(ws_auth_message("/balance/ws")))

        async for msg in ws:
            data = json.loads(msg)
            if data.get("t") == "success":
                # Initial balances after auth
                for b in data["d"]["l"]:
                    print(f"Asset {b['i']}: balance={b['b']}, free={b['f']}")
            elif "a" in data:
                # Balance update
                print(f"Update — asset {data['a']}: balance={data['b']}, free={data['f']}")

asyncio.run(balance_updates())
```

***

## Trade & Activity Feed

The history service exposes three separate WebSocket endpoints.

***

### Order Feed

Real-time order status updates for the authenticated user.

```
wss://history-service.outpoll.com/order/ws
```

> Requires authentication. Send an `AUTH` message after connecting.

```json
{
  "t": "AUTH",
  "d": {
    "apiKey": "<API_KEY>",
    "signature": "<SIGNATURE>",
    "timestamp": "<UNIX_EPOCH_SECONDS>"
  }
}
```

Signature: `base64url_no_padding(HMAC-SHA256(secret, timestamp + "GET" + "/order/ws"))`

**Server -> Client** (success)

```json
{
  "t": "SUCCESS",
  "d": {
    "message": "AUTH successful"
  },
  "r": null
}
```

**Python**

```python
import asyncio, json, websockets

async def order_feed():
    uri = "wss://history-service.outpoll.com/order/ws"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps(ws_auth_message("/order/ws")))

        async for msg in ws:
            data = json.loads(msg)
            print(f"Order update: {data}")

asyncio.run(order_feed())
```

***

### Activity Feed

Real-time activity updates (trades, position changes) with subscribe/unsubscribe.

```
wss://history-service.outpoll.com/history/ws
```

All messages follow a common envelope:

```json
{
  "t": "<TYPE>",
  "e": "<EVENT_ID>",
  "o": "<OUTCOME_ID>",
  "d": { ... },
  "r": "<ERROR_MESSAGE>"
}
```

**Client -> Server** (subscribe)

```json
{
  "t": "SUBSCRIBE",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"
}
```

**Client -> Server** (unsubscribe)

```json
{
  "t": "UNSUBSCRIBE",
  "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
  "o": "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76"
}
```

**Python**

```python
import asyncio, json, websockets

async def activity_feed():
    uri = "wss://history-service.outpoll.com/history/ws"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "t": "SUBSCRIBE",
            "e": "54ccea1a-16fd-469c-8018-84b375243e8a",
            "o": "b21f6fd8-b9d1-4b9f-bb79-ef141e3dcb76",
        }))

        async for msg in ws:
            data = json.loads(msg)
            print(f"Activity: {data}")

asyncio.run(activity_feed())
```

***

### Last Trades Feed

Real-time broadcast of the latest trades across the platform. No authentication or subscription required.

```
wss://history-service.outpoll.com/last-trades/ws
```

**Python**

```python
import asyncio, json, websockets

async def last_trades():
    uri = "wss://history-service.outpoll.com/last-trades/ws"
    async with websockets.connect(uri) as ws:
        async for msg in ws:
            data = json.loads(msg)
            print(f"Trade: {data}")

asyncio.run(last_trades())
```

***

## Errors

WebSocket error messages follow this format:

```json
{
  "t": "error",
  "e": "event-id",
  "o": ["outcome-id"],
  "d": {
    "error": "Error description"
  }
}
```

Common errors:

| Error                          | Cause                           |
| ------------------------------ | ------------------------------- |
| Invalid or missing credentials | API key auth failed             |
| No outcome IDs provided        | Subscribe without outcome IDs   |
| Invalid time range             | `from` is after `to`            |
| Invalid period                 | Unrecognized aggregation period |
| Unknown message type           | Unsupported `t` value           |
