Skip to content

Docs/Realtime Option Trades API

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.

Open product page →

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

  • WebSocket URL: wss://ws.optiondata.io

Request

Parameters

NameExampleRequiredDescription
tokencus_xxxxxxxxxxRequiredGet your API token here: optiondata.io
aggregation_modeAGGREGATED or RAWOptional, default value is AGGREGATED if leave blankDetermine how the option trades are aggregated by our algorithm. In AGGREGATED mode, the modified real-time feed combines option trades executed simultaneously for the same option symbol. In contrast, RAW mode keep the option trades in their original form without any modifications.
symbolsSPY,TSLA,SPX or AAPLOptional field, leave it blank means no filteringA 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 filteringThe 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.
sentimentBULLISH,BEARISH,NEUTRAL or BULLISHOptional field, leave it blank means no filteringThe 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_typeSTOCK,ETF,INDEX,ETN,REITOptional field, leave it blank means no filteringThe 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 filteringThe 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]
moneynessITM,OTM,ATMOptional field, leave it blank means no filteringThe 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 filteringThe 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_onlytrueOptional field, leave it blank means no filteringOnly trades on stocks with upcoming earnings. AGGREGATED MODE only
put_callCALL or PUTOptional field, leave it blank means no filteringFilter to identify whether this option trade is PUT or CALL
sideASK,AASK,BID,BBID,MIDOptional field, leave it blank means no filteringPrice 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 filteringIdentity 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 filteringThe 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 filteringImplied 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_typeAUTOOptional field, leave it blank means no filteringOPRA 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 filteringNumber 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_onlytrueOptional field, leave it blank means no filteringOnly opening trades (2 × size >= OI + daily volume). AGGREGATED MODE only
size[155,2000]Optional field, leave it blank means no filteringFilter 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 filteringThe 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 filteringThe 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_modetrueOptionalEnable 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

import WebSocket from 'ws';

const url = "wss://ws.optiondata.io?symbols=AAPL,SPX&token=cus_xxxx&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);
});
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_TOKEN"

# 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...")
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=cus_xxxx&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

NameTypeDescription
idstringunique id for this option trade record
symbolstringTicker Symbol (TSLA, MSFT, etc...)
datestringthe date the trade was executed, the date format is YYYY-MM-DD
timestringThe time the trade was created. Format is YYYY-MM-DD HH:MM:SS
put_callstringIndicates whether this trade is PUT or CALL
strikefloatStrike price of the option trade
expiration_datestringThe date on which the Option expires. The Option becomes invalid after this date and cannot be exercised ex: 2022-04-05
option_activity_typestringOPRA trade condition code (e.g., "AUTO", "SLAN"). AGGREGATED MODE only
underlying_typestringIndicates underlying is Common Stock, ETF, ETN, etc.
oiintegerOpen Interest. The total number of this options contract that are still open.
sizeintegerTotal order size (either of the 1 trade for the trade of SINGLE or REPEATED, or the sum of trade sizes for a AGGREGATED trade)
pricefloatLast price of a trade, or last price of last trade in a sweep.
underlying_pricefloatCurrent stock price of the underlying asset
bidfloatOption contract best bid
askfloatOption contract best ask
ivfloatImplied volatility (IV) is an estimate of the future volatility of the underlying stock based on options prices.
premiumfloatCost in dollar of the entire sweep or block option trade
sentimentstringBULLISH, BEARISH, or NEUTRAL The sentiment is estimated based on whether the trade was executed at the bid, ask, or spot price.
trade_countintegerNumber of trades involved in the sweep. In RAW mode, this is always 1.
expiry_daysintegerThe days left to expiration date.
sidestringPrice side: "AASK" (above ask), "ASK", "MID", "BID", "BBID" (below bid)
moneynessstring"ITM" (in the money), "ATM" (at the money), "OTM" (out of the money)
deltafloatDelta is a measure of the change in an option's price
gammafloatOption gamma
dexnumberDelta exposure -- equivalent shares the dealer must hedge
deinumberDelta exposure impact relative to daily stock volume
daily_volumeintegerDay volume for this option contract including this trade AGGREGATED MODE only
earning_datestringNext earning date for the underlying asset AGGREGATED MODE only
updated_timestampnumberUTC timestamp in milliseconds when this record was created
market_capnumberMarket capitalization in dollars (current share price × outstanding shares) AGGREGATED MODE only
option_symbolstringOption 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):

{
  "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):

{
  "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)

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

500 Internal Error

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

Heartbeat

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

{ "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. If the limit is exceeded, the oldest connection for that token is closed with WebSocket close code 1008 and reason "Too many concurrent connections for this token".

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 to see the original trades without any modification, please use RAW mode, the major differences between the two data feeds:

  • 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 no modifications to the trades, you can 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.

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: On average, We process around 20,000 records per minute during daily market open times. Due to the initial surge of data when the market opens, there may be delays during this period.

Market open delay chart

How to get started?

A: Please register at https://optiondata.io to get a 14-day free trial without a credit card required. Please reach out to us to help you set up the API connection.

What happens when the market closes?

A: When the market closes (5:00 PM ET), your WebSocket connection stays open. Data streaming stops, but heartbeat pings continue every 20 seconds. When the market reopens (9:30 AM ET, next trading day), data automatically resumes on the same connection -- no reconnection 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. For historical data analysis outside market hours, please consider using our Historical Option Data API.

What are the refund and trial policies for different APIs?

A: Real-time Option Data API:

  • 14-day free trial available
  • Historical SQL included; trial SQL responses are capped at 10 rows
  • Option Chain REST API included for trialing and active realtime users
  • 30-day money-back guarantee
  • No questions asked refund policy

Historical SQL and Option Chain:

  • Included with the realtime plan
  • Paid realtime subscriptions unlock larger historical SQL responses
  • Endpoint: POST https://www.optiondata.io/api/historical/sql
  • Option Chain endpoint: POST https://www.optiondata.io/api/option-chain

How do I request a refund?

A: For subscription purchases, simply send an email to support@optiondata.io using your login email address within 30 days of purchase. We will process your refund promptly and get back to you with confirmation.

The 30-day money-back guarantee applies to both real-time and historical API subscriptions. https://discord.com/invite/mE5pEDMdWs