WebSocket API
Real-time data streams specification
The WebSocket API provides real-time updates for market data and user account information.
Note: All trading pairs are denominated against USDC (e.g., BTC-USDC, ETH-USDC). USDC is used as the collateral currency and all settlements are in USDC.
Connection
Endpoint
| Environment | URL |
|---|---|
| Local | ws://localhost:8080/ws |
| Production | wss://api.diffusal.xyz/ws |
Connection Lifecycle
CLIENT SERVER
│ │
├── WebSocket Connect ─────────────────►│
│ │
│◄── Connection Acknowledged ───────────┤
│ { type: "connected", timestamp } │
│ │
├── Subscribe to Channels ─────────────►│
│ │
│◄── Subscription Confirmed ────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ Continuous Real-time Updates│ │
│◄───│ (trades, orderbook, tickers)│────┤
│ └─────────────────────────────┘ │
│ │
├── Unsubscribe ───────────────────────►│
│ │
├── Close Connection ──────────────────►│
│ │Message Format
All messages use JSON format with a type field to identify the message type.
Client → Server
| Field | Type | Description |
|---|---|---|
type | string | Message type |
id | string | Request ID (for correlation) |
... | varies | Type-specific fields |
Server → Client
| Field | Type | Description |
|---|---|---|
type | string | Message type |
id | string | Request ID (for subscription confirmations) |
channel | string | Channel name (for snapshot and update types) |
data | object | Payload data (for snapshot and update types) |
timestamp | number | Server timestamp (milliseconds) |
Note: Data messages (snapshot and update) wrap the payload in a data field along with channel and timestamp.
Authentication
Private channels require authentication. Authenticate after connecting, before subscribing to private channels.
Obtaining the JWT Token
The SIWE authentication flow establishes a session via HTTP cookie. The JWT token for WebSocket authentication is obtained from this session:
| Environment | JWT Source |
|---|---|
| Browser | Automatically included with WebSocket connection via cookie; manual auth message optional |
| Non-browser | Extract from session cookie or Authorization header from prior SIWE request |
Extracting JWT from SIWE Session
For non-browser environments (Node.js, mobile apps, etc.), extract the JWT token from the SIWE verification response:
import { createSiweMessage } from "viem/siwe";
const API_URL = "https://api.diffusal.xyz";
// Step 1: Request nonce
const nonceRes = await fetch(
`${API_URL}/api/auth/siwe/nonce?walletAddress=${walletAddress}`
);
const { nonce } = await nonceRes.json();
// Step 2: Create and sign SIWE message
const message = createSiweMessage({
domain: "diffusal.xyz", // Use 'localhost' for local development
address: walletAddress,
statement: "Sign in to Diffusal",
uri: API_URL,
version: "1",
chainId: chainId,
nonce: nonce,
});
const signature = await walletClient.signMessage({
account: walletAddress,
message: message,
});
// Step 3: Verify and get session
const verifyRes = await fetch(`${API_URL}/api/auth/siwe/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, signature }),
credentials: "include",
});
// Step 4: Extract JWT from Set-Cookie header
const setCookie = verifyRes.headers.get("set-cookie");
const sessionToken = setCookie?.match(/session=([^;]+)/)?.[1];
if (!sessionToken) {
throw new Error("Failed to extract session token");
}
// Step 5: Use token in WebSocket auth message
const ws = new WebSocket("wss://api.diffusal.xyz/ws");
ws.onopen = () => {
ws.send(
JSON.stringify({
type: "auth",
token: sessionToken,
})
);
};Note: The session token is valid for 7 days. Store it securely and refresh before expiry.
Auth Message
Send after connecting for non-browser environments or explicit authentication:
| Field | Type | Description |
|---|---|---|
type | "auth" | Message type |
token | string | JWT token from SIWE session |
Auth Response
Success:
| Field | Type | Description |
|---|---|---|
type | "auth_success" | Auth succeeded |
address | string | Authenticated address |
Failure:
| Field | Type | Description |
|---|---|---|
type | "auth_error" | Auth failed |
error | string | Error message |
Subscription Management
Subscribe
| Field | Type | Description |
|---|---|---|
type | "subscribe" | Message type |
id | string | Request ID |
channels | array | Channel names to subscribe |
Unsubscribe
| Field | Type | Description |
|---|---|---|
type | "unsubscribe" | Message type |
id | string | Request ID |
channels | array | Channel names to unsubscribe |
Subscription Response
| Field | Type | Description |
|---|---|---|
type | "subscribed" / "unsubscribed" | Response type |
id | string | Request ID |
channels | array | Affected channels |
Public Channels
No authentication required.
orderbook:{symbol}
Real-time order book updates for a series.
Channel Name: orderbook:BTC-82000-C-1736409600
Snapshot Message (on subscribe):
{
"type": "snapshot",
"channel": "orderbook:BTC-82000-C-1736409600",
"data": {
"bids": [{ "price": "1000000000000000000", "size": "5000000000000000000" }],
"asks": [{ "price": "1100000000000000000", "size": "3000000000000000000" }]
},
"timestamp": 1706500000000
}Update Message:
{
"type": "update",
"channel": "orderbook:BTC-82000-C-1736409600",
"data": {
"bids": [{ "price": "1000000000000000000", "size": "6000000000000000000" }],
"asks": []
},
"timestamp": 1706500001000
}Level Format (in data.bids / data.asks):
| Field | Type | Description |
|---|---|---|
price | string | Price level (WAD) |
size | string | New size (0 = removed) |
trades:{symbol}
Real-time trade notifications for a series.
Channel Name: trades:BTC-82000-C-1736409600
Trade Message:
{
"type": "update",
"channel": "trades:BTC-82000-C-1736409600",
"data": {
"id": "0x...",
"price": "1050000000000000000",
"size": "1000000000000000000",
"side": "buy",
"timestamp": 1706500000
},
"timestamp": 1706500000000
}Trade Data Fields:
| Field | Type | Description |
|---|---|---|
id | string | Trade ID |
price | string | Execution price (WAD) |
size | string | Trade size (WAD) |
side | string | Taker side ("buy"/"sell") |
timestamp | number | Execution time (seconds) |
ticker:{symbol}
Real-time ticker updates (prices, Greeks, stats).
Channel Name: ticker:BTC-82000-C-1736409600
Ticker Message:
{
"type": "update",
"channel": "ticker:BTC-82000-C-1736409600",
"data": {
"markPrice": "1050000000000000000",
"indexPrice": "95000000000000000000000",
"bestBid": "1000000000000000000",
"bestAsk": "1100000000000000000",
"delta": "550000000000000000",
"gamma": "1000000000000000",
"vega": "50000000000000000000",
"theta": "-10000000000000000",
"rho": "5000000000000000",
"iv": "800000000000000000",
"volume24h": "1000000000000",
"trades24h": 42
},
"timestamp": 1706500000000
}Ticker Data Fields:
| Field | Type | Description |
|---|---|---|
markPrice | string | Mark price (WAD) |
indexPrice | string | Spot price (WAD) |
bestBid | string | Best bid (WAD or null) |
bestAsk | string | Best ask (WAD or null) |
delta | string | Delta (WAD) |
gamma | string | Gamma (WAD) |
vega | string | Vega (WAD) |
theta | string | Theta (WAD) |
rho | string | Rho (WAD) |
iv | string | Implied volatility |
volume24h | string | 24h volume (USDC) |
trades24h | number | 24h trade count |
oracle:{pairId}
Real-time oracle price updates.
Channel Name: oracle:0x... (pairId as bytes32)
Oracle Message:
{
"type": "update",
"channel": "oracle:0x...",
"data": {
"pairId": "0x...",
"spotPrice": "95000000000000000000000",
"volatility": "800000000000000000",
"timestamp": 1706500000
},
"timestamp": 1706500000000
}Oracle Data Fields:
| Field | Type | Description |
|---|---|---|
pairId | string | Pair identifier |
spotPrice | string | Spot price (WAD) |
volatility | string | IV (WAD) |
timestamp | number | Oracle timestamp (secs) |
markets
Market-wide updates (new series, settlements).
Channel Name: markets
Series Created:
{
"type": "update",
"channel": "markets",
"data": {
"event": "series_created",
"seriesId": "0x...",
"symbol": "BTC-82000-C-1736409600",
"pairId": "0x...",
"strike": "100000000000000000000000",
"expiry": 1735257600,
"isCall": true
},
"timestamp": 1706500000000
}Series Settled:
{
"type": "update",
"channel": "markets",
"data": {
"event": "series_settled",
"seriesId": "0x...",
"settlementPrice": "95000000000000000000000"
},
"timestamp": 1706500000000
}Private Channels (Auth Required)
These channels require authentication. Authenticate before subscribing - subscribing without authentication returns an auth_required error.
positions
Position updates across all portfolios.
Channel Name: positions
Position Update:
{
"type": "update",
"channel": "positions",
"data": {
"portfolioId": 1,
"seriesId": "0x...",
"symbol": "BTC-82000-C-1736409600",
"optionBalance": "1000000000000000000",
"premiumBalance": "50000000000000000000",
"markPrice": "1050000000000000000",
"unrealizedPnl": "25000000000000000000"
},
"timestamp": 1706500000000
}Position Data Fields:
| Field | Type | Description |
|---|---|---|
portfolioId | number | Portfolio ID |
seriesId | string | Series ID |
symbol | string | Series symbol |
optionBalance | string | New balance (WAD) |
premiumBalance | string | Premium balance |
markPrice | string | Current mark (WAD) |
unrealizedPnl | string | Position PnL (WAD) |
collateral
Collateral and margin updates.
Channel Name: collateral
Collateral Update:
{
"type": "update",
"channel": "collateral",
"data": {
"portfolioId": 1,
"deposit": "10000000000",
"equity": "10500000000",
"initialMargin": "2000000000",
"maintenanceMargin": "1600000000",
"maxWithdraw": "8500000000",
"isHealthy": true
},
"timestamp": 1706500000000
}Collateral Data Fields:
| Field | Type | Description |
|---|---|---|
portfolioId | number | Portfolio ID |
deposit | string | USDC deposit |
equity | string | Total equity |
initialMargin | string | IM requirement |
maintenanceMargin | string | MM threshold |
maxWithdraw | string | Max withdrawable |
isHealthy | boolean | Health status |
orders
Order status updates.
Channel Name: orders
Order Update:
{
"type": "update",
"channel": "orders",
"data": {
"orderId": "0x...",
"status": "partially_filled",
"filled": "500000000000000000",
"remaining": "500000000000000000"
},
"timestamp": 1706500000000
}Order Cancelled:
{
"type": "update",
"channel": "orders",
"data": {
"orderId": "0x...",
"status": "cancelled",
"reason": "user_cancelled"
},
"timestamp": 1706500000000
}Order Data Fields:
| Field | Type | Description |
|---|---|---|
orderId | string | Order ID |
status | string | New status |
filled | string | Filled amount |
remaining | string | Remaining amount |
reason | string | Cancellation reason (if cancelled) |
fills
Trade fill notifications for the user.
Channel Name: fills
Fill Message:
{
"type": "update",
"channel": "fills",
"data": {
"tradeId": "0x...",
"orderId": "0x...",
"seriesId": "0x...",
"symbol": "BTC-82000-C-1736409600",
"portfolioId": 1,
"side": "buy",
"price": "1050000000000000000",
"size": "1000000000000000000",
"fee": "1000000",
"timestamp": 1706500000
},
"timestamp": 1706500000000
}Fill Data Fields:
| Field | Type | Description |
|---|---|---|
tradeId | string | Trade ID |
orderId | string | Order ID (if limit order) |
seriesId | string | Series ID |
symbol | string | Series symbol |
portfolioId | number | Portfolio ID |
side | string | "buy" or "sell" |
price | string | Fill price (WAD) |
size | string | Fill size (WAD) |
fee | string | Fee amount (USDC) |
timestamp | number | Fill time (seconds) |
Heartbeat
The server sends periodic heartbeats to keep connections alive.
Heartbeat Interval: 15 seconds
Heartbeat Message (Server → Client):
| Field | Type | Description |
|---|---|---|
type | "heartbeat" | Heartbeat |
timestamp | number | Server time |
Pong Message (Client → Server):
| Field | Type | Description |
|---|---|---|
type | "pong" | Heartbeat reply |
Clients should respond with a pong message. Connections are closed after 30 seconds of inactivity (no messages or pongs received).
Message Ordering
Subscription Flow
When subscribing to a channel, messages arrive in this order:
- Subscribe request sent by client
- Snapshot sent by server (current state of the channel)
- Subscription confirmation (
subscribedmessage) - Update messages (subsequent changes)
Ordering Guarantees
| Message Type | Ordering Guarantee |
|---|---|
| Snapshots | Sent before subscription confirmation |
| Updates | Approximate chronological order; strict ordering not guaranteed |
| Heartbeats | 15 second intervals |
Note: Updates reflect state changes since the snapshot. In fast-moving markets, some updates may arrive out of order. Use the timestamp field for sequencing.
Error Handling
Error Message
| Field | Type | Description |
|---|---|---|
type | "error" | Error occurred |
id | string | Request ID (if applicable) |
code | string | Error code |
message | string | Error message |
Error Codes
| Code | Description |
|---|---|
invalid_message | Malformed message |
unknown_channel | Channel does not exist |
auth_required | Channel requires auth |
rate_limited | Too many requests |
internal_error | Server error |
Connection Limits
| Limit | Value |
|---|---|
| Max connections per IP | 10 |
| Max subscriptions per connection | 50 |
| Max message size | 16 KB |
| Heartbeat interval | 15 seconds |
| Idle timeout | 30 seconds |
| Message rate limit | 100/second |
Reconnection Strategy
WebSocket connections may disconnect due to network issues, server restarts, or the 30-second idle timeout. Implement automatic reconnection for production applications.
Recommended Implementation
class DiffusalWebSocket {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private baseDelay = 1000; // 1 second
private subscriptions = new Set<string>();
private token: string | null = null;
private url: string;
constructor(url: string) {
this.url = url;
}
connect(token?: string) {
this.token = token ?? this.token;
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("WebSocket connected");
this.reconnectAttempts = 0;
// Re-authenticate if token exists
if (this.token) {
this.ws?.send(
JSON.stringify({
type: "auth",
token: this.token,
})
);
}
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
// Re-subscribe after successful auth
if (msg.type === "auth_success" && this.subscriptions.size > 0) {
this.ws?.send(
JSON.stringify({
type: "subscribe",
id: "resub-" + Date.now(),
channels: Array.from(this.subscriptions),
})
);
}
// Respond to heartbeats
if (msg.type === "heartbeat") {
this.ws?.send(JSON.stringify({ type: "pong" }));
}
};
this.ws.onclose = (event) => {
if (event.code !== 1000) {
// Not a clean close
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
private scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error("Max reconnection attempts reached");
return;
}
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const delay = this.baseDelay * Math.pow(2, this.reconnectAttempts);
this.reconnectAttempts++;
console.log(
`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
);
setTimeout(() => this.connect(), delay);
}
subscribe(channels: string[]) {
channels.forEach((ch) => this.subscriptions.add(ch));
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(
JSON.stringify({
type: "subscribe",
id: "sub-" + Date.now(),
channels: channels,
})
);
}
}
unsubscribe(channels: string[]) {
channels.forEach((ch) => this.subscriptions.delete(ch));
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(
JSON.stringify({
type: "unsubscribe",
id: "unsub-" + Date.now(),
channels: channels,
})
);
}
}
close() {
this.ws?.close(1000); // Clean close
}
}
// Usage
const ws = new DiffusalWebSocket("wss://api.diffusal.xyz/ws");
ws.connect(sessionToken);
ws.subscribe(["orderbook:BTC-82000-C-1736409600", "positions"]);Reconnection Checklist
| Step | Action | Purpose |
|---|---|---|
| 1 | Use exponential backoff | Avoid overwhelming server |
| 2 | Re-authenticate | JWT may still be valid (7-day lifetime) |
| 3 | Re-subscribe to channels | Restore previous subscription state |
| 4 | Handle snapshots | Data may have changed during disconnect |
Handling Idle Timeout
The server closes connections after 30 seconds of inactivity. To prevent disconnection:
- Respond to
heartbeatmessages withpong - Heartbeats are sent every 15 seconds
- If you miss 2 heartbeats, the connection closes
Example Flow
Public Market Data
1. Connect to ws://localhost:8080/ws
2. Receive: { "type": "connected" }
3. Send: {
"type": "subscribe",
"id": "1",
"channels": ["orderbook:BTC-82000-C-1736409600", "trades:BTC-82000-C-1736409600"]
}
4. Receive: {
"type": "subscribed",
"id": "1",
"channels": ["orderbook:BTC-82000-C-1736409600", "trades:BTC-82000-C-1736409600"]
}
5. Receive: {
"type": "snapshot",
"channel": "orderbook:BTC-82000-C-1736409600",
"data": { "bids": [...], "asks": [...] }
}
6. Receive updates as they occur...Private Account Data
1. Connect to ws://localhost:8080/ws
2. Receive: { "type": "connected" }
3. Send: {
"type": "auth",
"token": "eyJhbGciOiJIUzI1NiIs..."
}
4. Receive: {
"type": "auth_success",
"address": "0x..."
}
5. Send: {
"type": "subscribe",
"id": "2",
"channels": ["positions", "fills", "collateral"]
}
6. Receive: { "type": "subscribed", "id": "2", "channels": [...] }
7. Receive position/fill/collateral updates as they occur...Related
- API Reference - REST endpoints
- Authentication - SIWE flow