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.
  • bar and tick count 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
Feedclock_offset_secondsMeaning
WMMARKETS:*-10800broker runs 3 h ahead of UTC
ISE:TADIL, ISE:NOTADIL347142601exchange terminal runs ~4018 days behind
OANDA:LIVE, ALPARI:LIVE, LITEFINANCE:LIVE, AMARKETS:LIVE0already true UTC

Client → Subscribe

{
  "action": "subscribe",
  "symbols": [
    "WMMARKETS:ZERO:EURUSD",
    "WMMARKETS:ZERO:GBPUSD",
    "WMMARKETS:ZERO:XAUUSD"
  ],
  "type": "bar"
}
FieldValueDescription
action"subscribe"Required
symbolsstringCompound 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
  }
}
FieldTypeDescription
bar.timeintegerBar 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.openfloatOpen price
bar.highfloatHigh price
bar.lowfloatLow price
bar.closefloatClose price
bar.volumeintegerTick 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 statusBody codeMeaning
401UNAUTHORIZEDNo API key supplied
401KEY_INVALIDKey not found
401KEY_INACTIVEKey disabled
401KEY_EXPIREDKey past its expiry date
403IP_NOT_ALLOWEDClient IP not in the key's allowlist
429TOO_MANY_CONNECTIONSConcurrent 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

LimitDescription
Simultaneous connectionsPlan-dependent (typically 2–50)
Symbols per connectionPlan-dependent (typically 50–500)
Idle timeout60 seconds without any message
RecommendationSend a ping every 30 seconds