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