WebSocket
Real-time data streaming via WebSocket connection.
Try It Live
Test the WebSocket API directly in your browser. Connect, send subscriptions, and see real-time responses.
wss://api.o2.app/v1/wsQuick Actions
Connection
URL: wss://api.testnet.o2.app/v1/ws
const ws = new WebSocket('wss://api.testnet.o2.app/v1/ws');
ws.onopen = () => {
console.log('Connected');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Disconnected');
};Connection Limits
| Parameter | Value | Description |
|---|---|---|
| Inactivity timeout | 60 seconds | Connection closes if no messages received |
| Max subscription topics | 10 | Maximum identities per subscription request |
| Write buffer size | 4 KiB | Server-side write buffer |
Heartbeat (Keep-Alive)
Send PING as plain text to keep the connection alive. Server responds with PONG.
// Send PING every 30 seconds to prevent timeout
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send('PING');
}
}, 30000);
ws.onmessage = (event) => {
if (event.data === 'PONG') {
console.log('Heartbeat OK');
return;
}
// Handle other messages...
};Subscription Actions
All subscription messages use JSON format with an action field.
subscribe_depth
Subscribe to full order book depth updates with real-time changes.
Request:
{
"action": "subscribe_depth",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f",
"precision": "10"
}| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "subscribe_depth" |
market_id | string | Yes | Market ID (hex, 0x-prefixed). See Get Markets List for available markets |
precision | string | Yes | Price aggregation level. Valid: "10", "100", "1000", "10000", "100000", "1000000", "10000000", "100000000", "1000000000" |
Initial Response (full state):
The first message after subscribing contains the complete current state of the order book in the view field.
{
"action": "subscribe_depth",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f",
"view": {
"precision": 10,
"buys": [
{ "price": "95000000000", "quantity": "1500000000" }
],
"sells": [
{ "price": "95100000000", "quantity": "2000000000" }
]
}
}Update Response (delta changes):
All subsequent messages contain only the changes since the last update in the changes field this is not a full state. Apply these deltas to your local order book. A quantity of "0" means the price level has been removed.
{
"action": "subscribe_depth_update",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f",
"changes": {
"buys": [
{ "price": "95000000000", "quantity": "1600000000" }
],
"sells": []
},
"onchain_timestamp": "1704067200",
"seen_timestamp": "1704067200123456789"
}subscribe_depth_view
Subscribe to a smoothed view of the order book at a specified frequency. Useful for visualizing the state of the order book in a rendered chart, but not recommended for trading since it does not provide a full picture of the market state.
Request:
{
"action": "subscribe_depth_view",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f",
"precision": "10",
"frequency": "500ms"
}| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "subscribe_depth_view" |
market_id | string | Yes | Market ID (hex, 0x-prefixed) |
precision | string | Yes | Price aggregation level |
frequency | string | Yes | Update frequency: "100ms", "500ms", "1s", "3s" |
Response:
{
"action": "subscribe_depth_view",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f",
"view": {
"precision": 10,
"buys": [
{ "price": "95000000000", "quantity": "1500000000" },
{ "price": "94900000000", "quantity": "3200000000" }
],
"sells": [
{ "price": "95100000000", "quantity": "2000000000" },
{ "price": "95200000000", "quantity": "1800000000" }
]
}
}subscribe_trades
Subscribe to trade executions for a market.
Request:
{
"action": "subscribe_trades",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f"
}| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "subscribe_trades" |
market_id | string | Yes | Market ID (hex, 0x-prefixed) |
Response:
{
"action": "subscribe_trades",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f",
"trades": [
{
"trade_id": "123456789",
"side": "Buy",
"quantity": "100000000",
"price": "95050000000",
"total": "9505000000000000000",
"timestamp": "1704067200123456789"
}
],
"onchain_timestamp": "1704067200",
"seen_timestamp": "1704067200123456789"
}| Response Field | Type | Description |
|---|---|---|
action | string | "subscribe_trades" |
market_id | string | Market ID |
trades | array | Array of trade objects |
trades[].trade_id | string | Unique trade identifier |
trades[].side | string | "Buy" or "Sell" (taker side) |
trades[].quantity | string | Trade quantity (base asset, raw units) |
trades[].price | string | Execution price (raw units) |
trades[].total | string | Total value (quantity * price) |
trades[].timestamp | string | Trade timestamp (nanoseconds since Unix epoch) |
onchain_timestamp | string | When the event occurred on-chain (seconds since Unix epoch) |
seen_timestamp | string | When the indexer processed the event (nanoseconds since Unix epoch) |
subscribe_orders
Subscribe to order updates for specific trading accounts.
Request:
{
"action": "subscribe_orders",
"identities": [
{ "ContractId": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "subscribe_orders" |
identities | Identity[] | Yes | Array of Identity objects (max 10) |
Response:
{
"action": "subscribe_orders",
"orders": [
{
"order_id": "0xabc123...",
"owner": { "ContractId": "0x1234..." },
"side": "Buy",
"order_type": "Limit",
"market_id": "0xc0c5c48...",
"quantity": "100000000",
"quantity_fill": "50000000",
"price": "95000000000",
"price_fill": "95000000000",
"timestamp": "1704067200123456789",
"close": false,
"partially_filled": true,
"cancel": false,
"history": [
{
"type": "created",
"status": "success",
"tx_id": "0xdef456..."
}
],
"fills": [
{
"order_id": "0xfff...",
"quantity": "50000000",
"price": "95000000000",
"timestamp": "1704067200123456789",
"fee": "100000"
}
]
}
],
"onchain_timestamp": "1704067200",
"seen_timestamp": "1704067200123456789"
}| Response Field | Type | Description |
|---|---|---|
order_id | string | Unique order identifier |
owner | Identity | Owner of the order |
side | string | "Buy" or "Sell" |
order_type | string | "Limit" or "Market" |
market_id | string | Market ID |
quantity | string | Total order quantity |
quantity_fill | string | Filled quantity |
price | string | Order price |
price_fill | string | Average fill price |
timestamp | string | Order creation timestamp |
close | boolean | Order is fully closed |
partially_filled | boolean | Order has partial fills |
cancel | boolean | Order was cancelled |
history | array | Transaction history |
fills | array | Individual fill events |
subscribe_balances
Subscribe to balance updates for specific trading accounts.
Request:
{
"action": "subscribe_balances",
"identities": [
{ "ContractId": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "subscribe_balances" |
identities | Identity[] | Yes | Array of Identity objects (max 10) |
Response:
{
"action": "subscribe_balances",
"balance": [
{
"identity": { "ContractId": "0x1234..." },
"asset_id": "0xabcd...",
"total_locked": "500000000",
"total_unlocked": "1500000000",
"trading_account_balance": "2000000000",
"order_books": {
"0xc0c5c48...": {
"locked": "500000000",
"unlocked": "0"
}
}
}
],
"onchain_timestamp": "1704067200",
"seen_timestamp": "1704067200123456789"
}| Response Field | Type | Description |
|---|---|---|
identity | Identity | Trading account identity |
asset_id | string | Asset ID |
total_locked | string | Total locked across all order books |
total_unlocked | string | Total unlocked across all order books |
trading_account_balance | string | Total balance in trading account |
order_books | object | Per-orderbook breakdown (market_id -> balances) |
subscribe_nonce
Subscribe to contract nonce updates. Useful for tracking transaction ordering.
Request:
{
"action": "subscribe_nonce",
"identities": [
{ "ContractId": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "subscribe_nonce" |
identities | Identity[] | Yes | Array of Identity objects (max 10) |
Response:
{
"action": "subscribe_nonce",
"contract_id": "0x1234...",
"nonce": "42",
"onchain_timestamp": "1704067200",
"seen_timestamp": "1704067200123456789"
}subscribe_nonce_v2
Subscribe to BOTH kinds of nonce state changes of a trading account: sequential counter increments and parallel nonce window updates. Mutually exclusive with subscribe_nonce on the same connection.
Request:
{
"action": "subscribe_nonce_v2",
"identities": [
{ "ContractId": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "subscribe_nonce_v2" |
identities | Identity[] | Yes | Array of Identity objects (max 10) |
Response (sequential nonce changed):
{
"action": "subscribe_nonce_v2",
"update": {
"sequential": {
"contract_id": "1234...",
"nonce": "42"
}
},
"onchain_timestamp": "1704067200",
"seen_timestamp": "1704067200123456789"
}Response (parallel nonce consumed — carries the full updated window of that nonce session):
{
"action": "subscribe_nonce_v2",
"update": {
"parallel": {
"contract_id": "1234...",
"nonce_session_id": 0,
"base": "0",
"slots": [
{ "word_position": "0", "bitmap": "15" },
{ "word_position": "1", "bitmap": "0" },
{ "word_position": "2", "bitmap": "0" },
{ "word_position": "3", "bitmap": "0" },
{ "word_position": "4", "bitmap": "0" },
{ "word_position": "5", "bitmap": "0" },
{ "word_position": "6", "bitmap": "0" },
{ "word_position": "7", "bitmap": "0" }
]
}
},
"onchain_timestamp": "1704067200",
"seen_timestamp": "1704067200123456789"
}Window updates are full snapshots (not deltas) and window state is monotone — base only advances and bits are only set — so a late subscriber can merge them safely with a window read from GET /v1/accounts/window.
Turbo Subscriptions
subscribe_depth, subscribe_depth_view, subscribe_trades and subscribe_orders, and their unsubscribes, accept "turbo": true to stream a market's Turbo order book. Depth also takes view ("executable" or "demand"). Turbo and canonical subscriptions are separate and can run together. See Turbo Order Books.
Unsubscribe Actions
To stop receiving updates, send the corresponding unsubscribe action.
unsubscribe_depth
{
"action": "unsubscribe_depth",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f"
}unsubscribe_depth_view
{
"action": "unsubscribe_depth_view",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f"
}unsubscribe_trades
{
"action": "unsubscribe_trades",
"market_id": "0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f"
}unsubscribe_orders
{
"action": "unsubscribe_orders"
}unsubscribe_balances
{
"action": "unsubscribe_balances",
"identities": [
{ "ContractId": "0x1234..." }
]
}unsubscribe_nonce_v2
{
"action": "unsubscribe_nonce_v2",
"identities": [
{ "ContractId": "0x1234..." }
]
}unsubscribe_nonce
{
"action": "unsubscribe_nonce",
"identities": [
{ "ContractId": "0x1234..." }
]
}Error Handling
Errors are returned as JSON with an error field.
Parse Error:
{
"error": "expected value at line 1 column 1"
}Validation Error:
{
"error": "Cannot subscribe to more than 10 topics at once"
}Invalid Frequency:
{
"error": "Invalid subscription frequency: 200ms"
}Common Errors
| Error | Cause | Solution |
|---|---|---|
Cannot subscribe to more than 10 topics at once | Too many identities in request | Reduce identities array to 10 or fewer |
Invalid subscription frequency | Invalid frequency value | Use 100ms, 500ms, 1s, or 3s |
Client is inactive | No messages for 60 seconds | Implement heartbeat with PING |
expected value at line X column Y | Malformed JSON | Check JSON syntax |
Timestamps
All responses include two timestamp fields:
| Field | Format | Description |
|---|---|---|
onchain_timestamp | Seconds since Unix epoch | When the event occurred on-chain (may be null) |
seen_timestamp | Nanoseconds since Unix epoch | When the indexer processed the event |
Complete Example
const ws = new WebSocket('wss://api.testnet.o2.app/v1/ws');
// Connection opened
ws.onopen = () => {
console.log('Connected to O2 WebSocket');
// Subscribe to depth view
ws.send(JSON.stringify({
action: 'subscribe_depth_view',
market_id: '0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f',
precision: '10',
frequency: '500ms'
}));
// Subscribe to trades
ws.send(JSON.stringify({
action: 'subscribe_trades',
market_id: '0xc0c5c48315d8cfff9f00777f81fdf04e9b4ef69e802a1a068549a8d6a06ee19f'
}));
// Start heartbeat
setInterval(() => ws.send('PING'), 30000);
};
// Handle messages
ws.onmessage = (event) => {
if (event.data === 'PONG') return;
const data = JSON.parse(event.data);
if (data.error) {
console.error('Error:', data.error);
return;
}
switch (data.action) {
case 'subscribe_depth_view':
console.log('Order book:', data.view);
break;
case 'subscribe_trades':
console.log('Trades:', data.trades);
break;
default:
console.log('Unknown action:', data.action);
}
};
// Handle errors and reconnection
ws.onclose = (event) => {
console.log('Disconnected, reconnecting in 5s...');
setTimeout(() => {
// Reconnect logic here
}, 5000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};Reconnection Strategy
Implement exponential backoff for reconnection:
class O2WebSocket {
constructor(url) {
this.url = url;
this.retryCount = 0;
this.maxRetry = 10;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.retryCount = 0;
// Resubscribe to topics here
};
this.ws.onclose = () => {
if (this.retryCount < this.maxRetry) {
const delay = Math.min(2000 + this.retryCount * 1500, 10000);
this.retryCount++;
setTimeout(() => this.connect(), delay);
}
};
}
}Backoff Schedule:
| Retry | Delay |
|---|---|
| 1 | 2.0s |
| 2 | 3.5s |
| 3 | 5.0s |
| 4 | 6.5s |
| 5 | 8.0s |
| 6+ | 10.0s (max) |