# Realtime Option Trades API

> WebSocket API reference for OPRA-licensed U.S. equity option trades: filters, RAW vs AGGREGATED modes, fields, test mode, and connection limits.

The Realtime Option Trades API provides streaming access to option trade data.

- **WebSocket URL**: `wss://ws.optiondata.io`

## Request

### Parameters

| Name | Example | Required | Description |
| :--- | :--- | :--- | :--- |
  | `token` | `YOUR_API_KEY` | Required | Get your API key here: [optiondata.io](https://optiondata.io/) |
| `aggregation_mode` | `AGGREGATED` or `RAW` | Optional, default value is AGGREGATED if leave blank | Determine how OptionData groups trades. AGGREGATED combines prints executed simultaneously for the same option symbol. RAW emits one service record per received print without OptionData trade aggregation; it is not an OPRA-native audit feed or a completeness guarantee. |
| `symbols` | `SPY,TSLA,SPX` or `AAPL` | Optional field, leave it blank means no filtering | A list of symbols in upper case separated by comma. Using the * wildcard or don't provide it to subscribe to all symbols. |
| `premium` | `[100000,250000]` or `[100,null]` | Optional field, leave it blank means no filtering | The filter for minimum premium. The first value represents the minimum premium, while the second value indicates the maximum premium. If not provided, [0,null] will be the default value. |
| `sentiment` | `BULLISH,BEARISH,NEUTRAL` or `BULLISH` | Optional field, leave it blank means no filtering | The filter for sentiment includes potential values of BULLISH, BEARISH, and NEUTRAL. You can combine multiple selections by separating them with commas, such as NEUTRAL,BEARISH. |
| `underlying_type` | `STOCK,ETF,INDEX,ETN,REIT` | Optional field, leave it blank means no filtering | The filter for underlying_type includes potential values of STOCK, ETF, INDEX, ETN, and REIT. You can combine multiple selections by separating them with commas, such as INDEX,STOCK. |
| `delta` | `[-0.9,0.98]` | Optional field, leave it blank means no filtering | The filter for delta has a default range of [-1, 1]. The first value represents the minimum delta, while the second value indicates the maximum delta. The range must remain within [-1, 1] |
| `moneyness` | `ITM,OTM,ATM` | Optional field, leave it blank means no filtering | The filter for moneyness includes potential values of ITM, ATM, and OTM. You can combine multiple selections by separating them with commas, such as ITM,OTM. |
| `expiry_days` | `[15,20]` | Optional field, leave it blank means no filtering | The days remaining until the expiration date. The first number represents the minimum expiration days, with a default value of 0 if null is provided. The second number is the maximum expiration days; if null is provided, there is no limit. |
| `is_recent_earning_only` | `true` | Optional field, leave it blank means no filtering | Only trades on stocks with upcoming earnings. **AGGREGATED MODE only** |
| `put_call` | `CALL` or `PUT` | Optional field, leave it blank means no filtering | Filter to identify whether this option trade is PUT or CALL |
| `side` | `ASK,AASK,BID,BBID,MID` | Optional field, leave it blank means no filtering | Price side of the trade. Values: AASK (above ask), ASK, MID, BID, BBID (below bid). Comma-separated for multiple. |
| `strike` | `[15.5,20]` | Optional field, leave it blank means no filtering | Identity the range of the Strike price of the option trade. The first number represents the minimum strike price, with a default value of 0 if null is provided. The second number is the maximum strike price. if null is provided, there is no limit at the maximium strike price. default value is [0,null] with minimum strike price is $0, and no limit on the maximum strike price. |
| `oi` | `[155,2000]` | Optional field, leave it blank means no filtering | The total number of this options contract that are still open. The first number represents the minimum OI, with a default value of 0 if null is provided. The second number is the maximum OI. if null is provided, there is no limit at the maximium OI. default value is [0,null] with minimum OI is 0, and no limit on the maximum OI. |
| `iv` | `[0.1,1.1]` | Optional field, leave it blank means no filtering | Implied Volatility which reflects the market's expectations of the future volatility of the underlying asset's price. The first number represents the minimum IV, with a default value of 0 if null is provided. The second number is the maximum IV. if null is provided, there is no limit at the maximium IV. default value is [0,null] with minimum IV is 0, and no limit on the maximum IV. |
| `option_activity_type` | `AUTO` | Optional field, leave it blank means no filtering | OPRA trade condition code. Values: `AUTO` (AutoExecution), `SLAN` (SingleLegAuctionNonISO), `MLET` (MultiLegAutoEx), `MLAT` (MultiLegAuction), `ISOI` (IntermarketSweep), `MESL` (MultiLegAutoSingleLeg), `TLET` (StockOptionAutoEx), `SLFT` (SingleLegFloor), `MLFT` (MultiLegFloor), `CBMO` (MultiLegFloorPropProduct). **AGGREGATED MODE only** |
| `trade_count` | `[1,1]` or `[2,null]` | Optional field, leave it blank means no filtering | Number of trades involved in the sweep or trade. Use [1,1] to filter for block trades only, and [2,null] to filter for aggregated trades. In RAW mode, this is always 1. |
| `is_opening_only` | `true` | Optional field, leave it blank means no filtering | Only opening trades (2 × size >= OI + daily volume). **AGGREGATED MODE only** |
| `size` | `[155,2000]` | Optional field, leave it blank means no filtering | Filter for the total order size. The first number represents the minimum size, with a default value of 0 if null is provided. The second number is the maximum size if null is provided, there is no limit at the maximium size. default value is [0,null] with minimum size is 0, and no limit on the maximum size. |
| `dex` | `[100,500]` | Optional field, leave it blank means no filtering | The filter for Delta Exposure (DEX). The first value represents the minimum dex, while the second value indicates the maximum dex. If not provided, default is no filtering. |
| `dei` | `[0.1,0.5]` | Optional field, leave it blank means no filtering | The filter for Delta Impact (DEI). The first value represents the minimum dei, while the second value indicates the maximum dei. If not provided, default is no filtering. |
| `test_mode` | `true` | Optional | Enable test snapshot mode: server sends pre-loaded sample data and then closes the connection. No authentication required. Use to validate filters and client parsing before going live. |

### Sample Code

```typescript
import WebSocket from 'ws';

const url = "wss://ws.optiondata.io?symbols=AAPL,SPX&token=YOUR_API_KEY&premium=[300000,null]";

// Create a new WebSocket instance
const ws = new WebSocket(url);

// Event handler for connection open
ws.on('open', function open() {
  console.log('WebSocket connection opened');
});

// Event handler for incoming messages
ws.on('message', function incoming(data) {
  console.log('Received message:', data);
  // You can parse and handle the data here
  try {
    const receivedData = JSON.parse(data.toString());
    console.log('Parsed data:', receivedData);
    // Further processing of receivedData
  } catch (error) {
    console.error('Error parsing received data:', error);
  }
});

// Event handler for connection close
ws.on('close', function close() {
  console.log('WebSocket connection closed');
});

// Event handler for errors
ws.on('error', function error(err) {
  console.error('WebSocket error:', err);
});
```


```python
import asyncio
import websockets
import json
import datetime
from asyncio import Queue

# Set your WebSocket URL here
url = "wss://ws.optiondata.io?token=YOUR_API_KEY"

# If this runs in a managed notebook with an HTTP proxy, allow the WebSocket
# host through the proxy (for example: NO_PROXY=ws.optiondata.io,.optiondata.io).

# Create a global queue for demonstration
data_queue = Queue()

async def connect():
    """Connect to WebSocket and process messages."""
    while True:
        try:
            async with websockets.connect(url, ping_interval=10, ping_timeout=15) as websocket:
                print(f'[{datetime.datetime.utcnow()}]  WebSocket connection opened')

                try:
                    while True:
                        message = await websocket.recv()
                        print(f"[{datetime.datetime.utcnow()}] Message received: {message[:200]}{'...' if len(message) > 200 else ''}")
                        try:
                            received_data = json.loads(message)
                            option_symbol = received_data.get('option_symbol')

                            if received_data.get('daily_volume', 0) > 1000 and (
                                (received_data.get('price', 0) > 0.02 and option_symbol != "SPY")
                                or option_symbol == "SPY"
                            ):
                                if option_symbol:
                                    await data_queue.put(received_data)  # Push to queue
                                    print(f"[{datetime.datetime.utcnow()}] Queued data for option_symbol: {option_symbol}")

                        except json.JSONDecodeError as e:
                            print(f"[{datetime.datetime.utcnow()}]  Error parsing received data: {e}")

                except websockets.exceptions.ConnectionClosed as e:
                    print(f"[{datetime.datetime.utcnow()}] WebSocket connection closed: {e}, reconnecting immediately.")

        except websockets.exceptions.WebSocketException as e:
            print(f"[{datetime.datetime.utcnow()}] WebSocket error: {e}, reconnecting immediately.")
        except Exception as e:
            print(f"[{datetime.datetime.utcnow()}] Unexpected error: {e}, reconnecting immediately.")

        print(f"[{datetime.datetime.utcnow()}] Waiting 2 seconds before reconnecting...")
        await asyncio.sleep(2)  # Backoff before reconnecting to avoid hammering the server

if __name__ == "__main__":
    try:
        asyncio.run(connect())
    except KeyboardInterrupt:
        print("Shutting down...")
```


```go
package main

import (
	"fmt"
	"log"
	"os"
	"os/signal"

	"github.com/gorilla/websocket"
)

func main() {
	// Define the URL of the WebSocket server
	url := "wss://ws.optiondata.io?symbols=AAPL,SPX&token=YOUR_API_KEY&premium=[300000,null]"

	// Connect to the WebSocket server
	c, _, err := websocket.DefaultDialer.Dial(url, nil)
	if err != nil {
		log.Fatalf("Failed to connect to WebSocket server: %v", err)
	}
	defer c.Close()

	// Handle interrupt signals to terminate gracefully
	interrupt := make(chan os.Signal, 1)
	signal.Notify(interrupt, os.Interrupt)

	fmt.Println("WebSocket connection opened")

	done := make(chan struct{})

	// Goroutine for reading messages
	go func() {
		defer close(done)
		for {
			_, message, err := c.ReadMessage()
			if err != nil {
				log.Println("Error reading message:", err)
				return
			}
			fmt.Printf("Received message: %s\n", message)
			// Ideally, you would parse and handle the JSON message here
		}
	}()

	// Main loop for handling program exit
	for {
		select {
		case <-done:
			return
		case <-interrupt:
			log.Println("Interrupt received, closing WebSocket connection...")
			// To cleanly close the connection, send a close message and wait for the server to close the connection
			err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
			if err != nil {
				log.Println("Error during closing WebSocket connection:", err)
				return
			}
			select {
			case <-done:
			}
			return
		}
	}
}
```



## Response

### Schema

| Name | Type | Description |
| :--- | :--- | :--- |
| `id` | `string` | unique id for this option trade record |
| `symbol` | `string` | Ticker Symbol (`TSLA`, `MSFT`, etc...) |
| `date` | `string` | the date the trade was executed, the date format is `YYYY-MM-DD` |
| `time` | `string` | The time the trade was created. Format is `YYYY-MM-DD HH:MM:SS` |
| `put_call` | `string` | Indicates whether this trade is `PUT` or `CALL` |
| `strike` | `float` | Strike price of the option trade |
| `expiration_date` | `string` | The date on which the Option expires. The Option becomes invalid after this date and cannot be exercised ex: 2022-04-05 |
| `option_activity_type` | `string` | OPRA trade condition code (e.g., `"AUTO"`, `"SLAN"`). **AGGREGATED MODE only** |
| `underlying_type` | `string` | Indicates underlying is Common Stock, ETF, ETN, etc. |
| `oi` | `integer` | Open Interest. The total number of this options contract that are still open. |
| `size` | `integer` | Total order size (either of the 1 trade for the trade of SINGLE or REPEATED, or the sum of trade sizes for a AGGREGATED trade) |
| `price` | `float` | Last price of a trade, or last price of last trade in a sweep. |
| `underlying_price` | `float` | Current stock price of the underlying asset |
| `bid` | `float` | Option contract best bid |
| `ask` | `float` | Option contract best ask |
| `iv` | `float` | Implied volatility (IV) is an estimate of the future volatility of the underlying stock based on options prices. |
| `premium` | `float` | Cost in dollar of the entire sweep or block option trade |
| `sentiment` | `string` | BULLISH, BEARISH, or NEUTRAL The sentiment is estimated based on whether the trade was executed at the bid, ask, or spot price. |
| `trade_count` | `integer` | Number of trades involved in the sweep. In RAW mode, this is always 1. |
| `expiry_days` | `integer` | The days left to expiration date. |
| `side` | `string` | Price side: `"AASK"` (above ask), `"ASK"`, `"MID"`, `"BID"`, `"BBID"` (below bid) |
| `moneyness` | `string` | `"ITM"` (in the money), `"ATM"` (at the money), `"OTM"` (out of the money) |
| `delta` | `float` | Delta is a measure of the change in an option's price |
| `gamma` | `float` | Option gamma |
| `dex` | `number` | Delta exposure -- equivalent shares the dealer must hedge |
| `dei` | `number` | Delta exposure impact relative to daily stock volume |
| `daily_volume` | `integer` | Day volume for this option contract including this trade **AGGREGATED MODE only** |
| `earning_date` | `string` | Next earning date for the underlying asset **AGGREGATED MODE only** |
| `updated_timestamp` | `number` | UTC timestamp in milliseconds when this record was created |
| `market_cap` | `number` | Market capitalization in dollars (current share price × outstanding shares) **AGGREGATED MODE only** |
| `option_symbol` | `string` | Option Contract Symbol, for example: `TSLA240430P00508000`, this refers to a put option contract with a strike price of $508 and an expiration date of April 30, 2024. |

### Message format

When the WebSocket connects, the server sends a single **connection success** message. After that, each **trade record** is sent as an individual JSON message: the message body is the trade object itself (no `status` or `data` wrapper). Parse each incoming message as JSON; if it has a `status` field, treat it as control (e.g. `SUCCESS`); otherwise treat it as a trade record.

### Sample response

**When WebSocket connected (one-time):**

```json
{
  "status": "SUCCESS",
  "msg": "Connection established with id: 159b4576-70ec-476f-8a1d-b061fdca0fb8",
  "data": {
    "connection_id": "1707123456789-a1b2c3d4e5f6"
  }
}
```

**When streaming data is received (each message is a plain trade object):**

```json
{
  "id": "13k7t4e",
  "date": "2025-05-02",
  "time": "2025-05-02 09:53:51",
  "symbol": "TSLA",
  "put_call": "CALL",
  "strike": 280,
  "expiration_date": "2025-05-02",
  "size": 7,
  "price": 4.44,
  "premium": 3109,
  "bid": 4.38,
  "ask": 4.5,
  "underlying_price": 281.77,
  "side": "MID",
  "sentiment": "NEUTRAL",
  "moneyness": "ITM",
  "expiry_days": 0,
  "option_symbol": "TSLA250502C00280000",
  "trade_count": 2,
  "dex": 413,
  "dei": 0.00239,
  "iv": 0.588,
  "delta": 0.589,
  "gamma": 0.0449,
  "oi": 14410,
  "daily_volume": 8352,
  "underlying_type": "STOCK",
  "option_activity_type": "SLAN",
  "earning_date": "2025-07-21",
  "market_cap": 900485935542,
  "updated_timestamp": 1746194031262
}
```

### Response Error

**401 Unauthorized** (missing or invalid token, before WebSocket upgrade)

```json
{
  "status": "ERROR",
  "code": 401,
  "msg": "Unauthorized, please login to https://optiondata.io get a valid API subscription plan first"
}
```

**500 Internal Error**

```json
{
  "status": "ERROR",
  "code": 500,
  "msg": "Internal Error"
}
```

## Heartbeat

The server sends a ping message every **20 seconds** to keep the connection alive:

```json
{ "type": "ping", "ts": 1707123456789 }
```

Clients can optionally respond with a pong: `{ "type": "pong", "ts": 1707123456789 }`. If the client sends `{"type": "ping"}`, the server responds with `{"type": "pong", "ts": ...}`.

## Connection limits

Each API token is limited to **5 concurrent WebSocket connections**. When the limit is reached, a new WebSocket/SSE handshake is rejected with HTTP `429` and a `Retry-After` header; existing connections remain open. Respect the retry delay and use exponential backoff rather than opening another connection immediately.

## Test mode

Use `test_mode=true` to validate your integration without live market data. No token is required.

- **URL example:** `wss://ws.optiondata.io?test_mode=true&aggregation_mode=AGGREGATED&symbols=TSLA`
- After sending the connection success message, the server streams pre-loaded sample trade records (with your filters applied), then closes the connection with code `1000` and reason `"test_snapshot_complete"`.
- No heartbeat pings are sent in test mode. Connection limits are not enforced.

## FAQ

### What is an Aggregation Mode?

A: Real-time Option Trades in `AGGREGATED` mode consolidates trades with the same option symbol executed simultaneously (at the second level) into a single aggregate trade. This approach simplifies the identification of large trades that have been divided into multiple smaller transactions. If you want print-level service records without OptionData trade aggregation, use `RAW` mode. RAW is not an OPRA-native audit feed or a completeness guarantee. The major differences between the two modes are:

*   The `trade_count` for option trades in `RAW` mode is always 1. In `AGGREGATED` mode, the `trade_count` can be either 1 or greater than 1. If it is greater than 1, this indicates that the option trade record aggregates multiple original option trades with the same option symbol that was executed simultaneously.
*   The average daily volume for Real-time Option Trades in `AGGREGATED` mode is 4 million rows of records, while the average daily volume for `RAW` is around 7 million rows of records.

### How to choose between AGGREGATED mode and RAW mode?

A: Institutional players often divide large trades into smaller ones and execute them simultaneously. They do this either to hide their intentions or due to a lack of liquidity or market depth at the moment of execution. This is the background for why we introduced AGGREGATED mode, for the case that you need to identify significant block trades in real-time, you can select AGGREGATED mode, allowing our aggregation services to combine these trades. If you prefer print-level service records without OptionData trade aggregation, choose RAW mode instead. By default, the API uses AGGREGATED mode unless you specify a different value for the aggregation_mode field. Please note that this is an assumption that trades executed simultaneously are sent by the same trader/institution, otherwise, you can choose RAW mode instead.

### Which OPRA audit fields does RAW mode expose?

A: `RAW` means one service record per received print (`trade_count = 1`), without OptionData trade aggregation; it is not an OPRA-native audit feed. The portal payload does not expose the original OPRA trade condition, exchange, correction/cancellation status, or source sequence/message id. It exposes only the documented service fields, including a service-generated `id` and millisecond timestamps.

### Does RAW mode guarantee every trade and a complete, lossless session?

A: No `premium` or `size` filter is applied when those parameters are omitted, and RAW records are not aggregated. However, OptionData does not warrant that every source print is delivered or that a session is complete, uninterrupted, or lossless. The stream has no source sequence field with which a client can independently certify completeness.

### What connection, backlog, drop, and recovery telemetry is exposed?

A: Clients receive a connection-success control message, individual trade messages, heartbeat pings about every 20 seconds, transport close/errors, and HTTP `429` with `Retry-After` when the five-connection cap is exceeded. The API does not expose customer-facing backlog depth, drop counts, recovery counts, gap counters, or a complete-session marker. Track receive counts, time gaps, disconnects, and reconnects in your client.

### Can I retain raw WebSocket frames or derived files after access ends?

A: Under the current Terms, when access or the subscription terminates you must stop using the Service and delete copies of raw Data. Derived files are permitted only when they cannot be reverse engineered to recreate the original Data or a reasonable substitute. Self-serve Pro is for individual, non-professional use; evaluation or use for a company, business, or professional purpose requires a separate commercial license from [support@optiondata.io](mailto:support@optiondata.io).

### Can you give me some examples?

A: Let's say there are two records in real-time option trades with `RAW` mode like below:

```
// RAW MODE
date        time      symbol  strike  put_call expiration_date premium size trade_count
2025-02-01  09:32:02  TSLA    330     CALL     2025-02-15       1000    2    1
2025-02-01  09:32:02  TSLA    330     CALL     2025-02-15       2000    3    1
```

These two trades will be aggregated into one trade since they are executed at the same time with the same option symbol.

```
// AGGREGATED MODE
date        time      symbol  strike  put_call expiration_date premium size trade_count
2025-02-01  09:32:02  TSLA    330     CALL     2025-02-15       3000    5    2
```

For the example above, the size of trade in the `AGGREGATED` mode is equal to the sum of the size of the trades in the `RAW` mode(5 = 3 + 2), the trade size of trade in the `AGGREGATED` mode is equal to the sum of the trade_size in the `RAW` mode. For other data fields in `AGGREGATED` mode (eg: delta, iv, gamma, theta) is the average value of the related data field in the `RAW` mode.

### Is there a delay in the data?

A: Realtime WebSocket trades are **live** during market hours. We typically process around **20,000 records per minute**; the open may lag under surge. Historical SQL (separate product) enforces a minimum **15-minute** delay from each trade’s execution time.

![Market open delay chart](https://1362181483-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDJROLvMer7QV5Op2yce2%2Fuploads%2FwvhHAf9mIJHKc8vXbeeL%2Fimage.png?alt=media&token=6b84da6a-27de-4fa5-815e-9be33445d9de)

### What is the typical daily volume?

A: On the order of **10M+** prints/day. **AGGREGATED** mode averages about **4M** records/day; **RAW** mode about **7M** records/day.

### What are connection and rate limits?

A: Each API token allows up to **5 concurrent WebSocket connections**. A sixth handshake receives HTTP **429** with `Retry-After`; existing connections are not evicted. Heartbeats run about every **20 seconds**. HTTP APIs (Historical SQL, Option Chain, Market Structure) default to about **60 requests per 60 seconds** per key (HTTP **429** + `Retry-After`).

### How to get started? Free trial length / extension?

A: Register at [https://www.optiondata.io](https://www.optiondata.io), complete qualification with an invitation code, then explicitly activate the **14-day** no-card Pro trial when you are ready. Copy your API key and connect to `wss://ws.optiondata.io`. If you do not add a payment method, you are not charged and access does not continue as paid when the trial ends. Eligible trial users can receive **50% off the first year**, with next steps shown inside the authenticated portal. Trials are **not auto-extended**.

### What happens when the market closes?

A: During market closures, your authenticated live WebSocket connection stays open. Data streaming stops, but heartbeat pings continue every 20 seconds. When the feed resumes for the next applicable trading session, data resumes on the same connection; no reconnection is needed. If you connect outside market hours, authentication succeeds normally and you receive the connection success message, but no trade data is sent until the market opens. The portal offers separate Sample and Live modes. Sample mode returns a finite synthetic snapshot and closes. Live mode does not switch to sample based on the browser clock, weekends, or holidays. An open connection with no matching trades is a waiting state. For historical trade research, use the Historical SQL API.

### What does the Pro plan include? Refunds?

A: One Pro subscription covers **Realtime WebSocket**, **Historical SQL** (trial SQL capped at **10 rows**), **Option Chain REST**, and **Market Structure API** under one key. Eligible trial users can receive **50% off the first year**, with next steps shown inside the authenticated portal. **30-day** money-back guarantee; cancel anytime. Refunds: email [support@optiondata.io](mailto:support@optiondata.io) from your login address within 30 days of purchase.
