WebSocket — /v1/stream
Subscribe to live M1 bar updates delivered the moment each one-minute candle closes. Typical latency is under 100ms.
Connection
wss://api.pipdata.net/v1/stream
Authenticate via query parameter (standard WebSocket() does not support custom headers):
const ws = new WebSocket(
"wss://api.pipdata.net/v1/stream?api_key=fxd_your_key_here"
);
Message Flow
Client Server
│ │
│ ← { type: "connected" } │ Immediately on connect
│ │
│ → { action: "subscribe" } │ Subscribe to symbols
│ │
│ ← { type: "subscribed" } │ Subscription confirmed
│ │
│ ← { type: "bar" } │ New M1 bar (every minute)
│ ← { type: "bar" } │
│ ... │
│ │
│ → { action: "ping" } │ Keepalive
│ ← { type: "pong" } │
Server → Connected
Sent immediately after the connection is established:
{
"type": "connected",
"session_id": "s_a1b2c3d4",
"server_time": 1749600000,
"limits": {
"max_symbols": 200
}
}
max_symbols is the number of subscriptions this connection may hold. Two things
about it are easy to get wrong:
- It is per connection, not per feed. One connection can carry symbols from any mix of feeds you are granted. You do not need a separate socket per broker.
barandtickcount separately. Subscribing to the same symbol for both uses two of your allowance.
Timestamps on the stream
tick.t and bar.time are Unix seconds, and they are on the feed's broker
clock — the same convention as candle_time in the database and as REST
/v1/history. They are not real UTC for every feed.
Convert with the per-feed offset from /v1/status:
real_utc = t + clock_offset_seconds
| Feed | clock_offset_seconds | Meaning |
|---|---|---|
WMMARKETS:* | -10800 | broker runs 3 h ahead of UTC |
ISE:TADIL, ISE:NOTADIL | 347142601 | exchange terminal runs ~4018 days behind |
OANDA:LIVE, ALPARI:LIVE, LITEFINANCE:LIVE, AMARKETS:LIVE | 0 | already true UTC |
Client → Subscribe
{
"action": "subscribe",
"symbols": [
"WMMARKETS:ZERO:EURUSD",
"WMMARKETS:ZERO:GBPUSD",
"WMMARKETS:ZERO:XAUUSD"
],
"type": "bar"
}
| Field | Value | Description |
|---|---|---|
action | "subscribe" | Required |
symbols | string | Compound keys to subscribe to |
type | "bar" | Data type — only bar is currently supported |
Server → Subscribed
{
"type": "subscribed",
"symbols": ["WMMARKETS:ZERO:EURUSD", "WMMARKETS:ZERO:GBPUSD"],
"rejected": []
}
If a symbol is rejected (no feed access or symbol cap reached):
{
"type": "subscribed",
"symbols": ["WMMARKETS:ZERO:EURUSD"],
"rejected": ["WMMARKETS:ZERO:GBPUSD"]
}
Server → Bar
Delivered each time an M1 candle closes:
{
"type": "bar",
"symbol": "WMMARKETS:ZERO:EURUSD",
"bar": {
"time": 1749599940,
"open": 1.08215,
"high": 1.08241,
"low": 1.08198,
"close": 1.08228,
"volume": 145
}
}
| Field | Type | Description |
|---|---|---|
bar.time | integer | Bar open time — Unix epoch seconds, in the same clock as /v1/history for that feed (WM: broker UTC+3; OANDA: true UTC). See the table below. |
bar.open | float | Open price |
bar.high | float | High price |
bar.low | float | Low price |
bar.close | float | Close price |
bar.volume | integer | Tick volume |
Client → Unsubscribe
{
"action": "unsubscribe",
"symbols": ["WMMARKETS:ZERO:EURUSD"]
}
Client → Ping
Send every 30 seconds to keep the connection alive:
{ "action": "ping" }
Response:
{ "type": "pong", "t": 1749600000 }
Connection Errors
| HTTP status | Body code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | No API key supplied |
| 401 | KEY_INVALID | Key not found |
| 401 | KEY_INACTIVE | Key disabled |
| 401 | KEY_EXPIRED | Key past its expiry date |
| 403 | IP_NOT_ALLOWED | Client IP not in the key's allowlist |
| 429 | TOO_MANY_CONNECTIONS | Concurrent connection cap reached |
Handling 429 TOO_MANY_CONNECTIONS
A 429 upgrade carries headers telling you exactly where you stand, so you do not have to guess:
Retry-After: 5
X-Ws-Connections-Limit: 10
X-Ws-Connections-In-Use: 10
Closing a socket frees its slot immediately. A slot left behind by an ungraceful
disconnect (process killed, network drop) is reclaimed automatically within 90
seconds — so if in-use looks higher than the number of sockets you believe you have
open, wait out that window rather than reconnecting harder.
Refused upgrades are counted in your rate_limited usage metric.
Full JavaScript Client
class ForexWebSocket {
constructor(apiKey) {
this.apiKey = apiKey;
this.ws = null;
this.handlers = {};
this.pingTimer = null;
this.reconnectMs = 5000;
}
connect() {
this.ws = new WebSocket(
`wss://api.pipdata.net/v1/stream?api_key=${this.apiKey}`
);
this.ws.onopen = () => {
console.log("WebSocket connected");
this.reconnectMs = 5000; // reset backoff
this.pingTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ action: "ping" }));
}
}, 30_000);
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "connected":
console.log(`Session: ${msg.session_id} | Max symbols: ${msg.limits.max_symbols}`);
break;
case "subscribed":
console.log(`Subscribed to ${msg.symbols.length} symbols`);
if (msg.rejected.length) {
console.warn("Rejected:", msg.rejected);
}
break;
case "bar":
this.handlers[msg.symbol]?.(msg.bar);
break;
case "error":
console.error("Server error:", msg.code);
if (["KEY_INVALID", "KEY_INACTIVE", "KEY_EXPIRED"].includes(msg.code)) {
this.ws.close(); // don't reconnect on auth errors
}
break;
}
};
this.ws.onclose = () => {
clearInterval(this.pingTimer);
console.log(`Disconnected — reconnecting in ${this.reconnectMs / 1000}s`);
setTimeout(() => {
this.reconnectMs = Math.min(this.reconnectMs * 2, 60_000);
this.connect();
}, this.reconnectMs);
};
}
subscribe(symbols, onBar) {
symbols.forEach(sym => { this.handlers[sym] = onBar; });
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
action: "subscribe",
symbols,
type: "bar"
}));
}
}
disconnect() {
clearInterval(this.pingTimer);
this.ws?.close();
}
}
// Usage
const client = new ForexWebSocket("fxd_your_key_here");
client.connect();
client.subscribe(
["WMMARKETS:ZERO:EURUSD", "WMMARKETS:ZERO:XAUUSD"],
(bar) => {
const time = new Date(bar.time * 1000).toISOString();
console.log(`close: ${bar.close} at ${time}`);
}
);
Python Client (asyncio)
import asyncio
import json
import websockets
API_KEY = "fxd_your_key_here"
URI = f"wss://api.pipdata.net/v1/stream?api_key={API_KEY}"
SYMBOLS = [
"WMMARKETS:ZERO:EURUSD",
"WMMARKETS:ZERO:GBPUSD",
"WMMARKETS:ZERO:XAUUSD",
]
async def stream():
reconnect_delay = 5
while True:
try:
async with websockets.connect(URI, ping_interval=30) as ws:
print("Connected ✓")
reconnect_delay = 5
await ws.send(json.dumps({
"action": "subscribe",
"symbols": SYMBOLS,
"type": "bar"
}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "bar":
bar = msg["bar"]
sym = msg["symbol"].split(":")[-1]
print(f"{sym}: close={bar['close']:.5f}")
elif msg["type"] == "error":
print(f"Error: {msg['code']}")
if msg["code"] in ("KEY_INVALID", "KEY_INACTIVE"):
return # stop on auth errors
except websockets.exceptions.ConnectionClosed as e:
print(f"Disconnected ({e}) — retrying in {reconnect_delay}s")
await asyncio.sleep(reconnect_delay)
reconnect_delay = min(reconnect_delay * 2, 60)
asyncio.run(stream())
Limits
| Limit | Description |
|---|---|
| Simultaneous connections | Plan-dependent (typically 2–50) |
| Symbols per connection | Plan-dependent (typically 50–500) |
| Idle timeout | 60 seconds without any message |
| Recommendation | Send a ping every 30 seconds |