WebSocket Streaming
The Valley WebSocket API lets you subscribe to live GSE market data pushed directly to your client. Instead of polling REST endpoints every few seconds you open one persistent connection and receive updates the moment data changes.
Endpoint wss://api.usevalley.xyz/v1/ws
Authentication
Pass your API key in the Socket.IO handshake auth object. The connection is rejected immediately with a 401 error event if the key is missing or invalid.
1import { io } from "socket.io-client";
2
3const socket = io("wss://api.usevalley.xyz", {
4 path: "/v1/ws",
5 transports: ["websocket"],
6 auth: { apiKey: "live_your_key_here" },
7});Alternatively you may send it as a header (useful in server-to-server contexts):
1const socket = io("wss://api.usevalley.xyz", {
2 path: "/v1/ws",
3 transports: ["websocket"],
4 extraHeaders: { "x-api-key": "live_your_key_here" },
5});Subscribing to a channel
Emit a subscribe event with a channel string. The server will:
- Immediately emit one payload on that channel so your UI can render right away.
- Continue pushing updates on the same channel at the intervals listed below.
1socket.emit("subscribe", { channel: "market:snapshot" });
2
3socket.on("market:snapshot", (payload) => {
4 console.log(payload.data.gainers);
5});To stop receiving updates emit unsubscribe:
1socket.emit("unsubscribe", { channel: "market:snapshot" });Available channels
| Channel | Push interval | Description |
|---|---|---|
market:snapshot | 30 s | Top gainers, losers, most traded, breadth |
quote:<SYMBOL> | 20 s | Full price snapshot for one stock |
ticks:<SYMBOL> | 15 s | Intraday tick stream for one stock |
Symbols are case-insensitive — quote:MTNGH and quote:mtngh are equivalent.
Payload shape
Every push uses the same { data, meta } envelope as the REST API, plus a channel field in meta:
1{
2 "data": { ... },
3 "meta": {
4 "timestamp": "2026-03-03T11:00:00Z",
5 "version": "1.0",
6 "exchange": "GSE",
7 "channel": "quote:MTNGH"
8 }
9}Channel examples
market:snapshot
1{
2 "data": {
3 "gainers": [
4 { "symbol": "MTNGH", "display_price": 2.10, "change_pct": 2.44 }
5 ],
6 "losers": [
7 { "symbol": "EGH", "display_price": 7.02, "change_pct": -1.12 }
8 ],
9 "most_traded": [
10 { "symbol": "GCB", "total_trade_volume": 3200000 }
11 ],
12 "breadth": {
13 "advancing": 12,
14 "declining": 5,
15 "unchanged": 3,
16 "total": 20
17 }
18 },
19 "meta": { "timestamp": "2026-03-03T11:30:00Z", "version": "1.0", "exchange": "GSE", "channel": "market:snapshot" }
20}quote:<SYMBOL>
1{
2 "data": {
3 "latest": {
4 "symbol": "MTNGH",
5 "display_price": 2.10,
6 "bid_price": 2.09,
7 "ask_price": 2.11,
8 "open_price": 2.05,
9 "high_price": 2.12,
10 "low_price": 2.03,
11 "close_price": 2.10,
12 "total_trade_volume": 1250000
13 },
14 "metrics": {
15 "yearHigh": 2.45,
16 "yearLow": 1.72,
17 "avgVolume30d": 980000,
18 "previousClose": 2.05
19 },
20 "company": {
21 "company_name": "Scancom PLC (MTN Ghana)",
22 "sector": "Telecommunications"
23 }
24 },
25 "meta": { "timestamp": "2026-03-03T11:30:20Z", "version": "1.0", "exchange": "GSE", "channel": "quote:MTNGH" }
26}ticks:<SYMBOL>
1{
2 "data": [
3 {
4 "timestamp": "2026-03-03T09:30:00Z",
5 "last_trade_price": 2.05,
6 "bid_price": 2.04,
7 "ask_price": 2.06,
8 "volume": 12500
9 },
10 {
11 "timestamp": "2026-03-03T09:31:00Z",
12 "last_trade_price": 2.07,
13 "bid_price": 2.06,
14 "ask_price": 2.08,
15 "volume": 8200
16 }
17 ],
18 "meta": { "timestamp": "2026-03-03T11:30:15Z", "version": "1.0", "exchange": "GSE", "channel": "ticks:MTNGH" }
19}Complete example
1import { io } from "socket.io-client";
2
3const socket = io("wss://api.usevalley.xyz", {
4 path: "/v1/ws",
5 transports: ["websocket"],
6 auth: { apiKey: "live_your_key_here" },
7});
8
9socket.on("connect", () => {
10 console.log("Connected:", socket.id);
11
12 // Subscribe to live market breadth
13 socket.emit("subscribe", { channel: "market:snapshot" });
14
15 // Subscribe to MTN Ghana live quote
16 socket.emit("subscribe", { channel: "quote:MTNGH" });
17});
18
19socket.on("market:snapshot", ({ data }) => {
20 console.log("Advancing:", data.breadth.advancing);
21});
22
23socket.on("quote:MTNGH", ({ data }) => {
24 console.log("MTN price:", data.latest.display_price);
25});
26
27socket.on("error", (err) => {
28 console.error("WS error:", err.message);
29});
30
31socket.on("disconnect", (reason) => {
32 console.warn("Disconnected:", reason);
33});Error events
The server emits an error event (and then disconnects) for auth failures:
| Code | Message |
|---|---|
401 | Missing API key |
401 | Invalid or revoked API key |
Channel-level errors (bad payload, unknown channel) are emitted as a 400 error event without disconnecting.
Notes
- Only
WebSockettransport is supported — long-polling is disabled. - Rate limits are not enforced per-message on the WebSocket. A single authenticated connection counts toward your API key's connection quota.
- The WebSocket server runs on the same port as the REST API (
4000in development,443in production). - During off-market hours (outside 09:30–15:00 GMT), tick channels will push an empty array. Quote channels still push the last known prices.