MetaCopier
Open appSupport
  • 📄Documentation
  • 💟Introduction
  • Features
    • 💼Basic features
    • 🌟Pro features
    • 🔀Signal sharing
    • 🏎️HFT support
    • 💢Specifications
  • ⛳Pending orders
  • Tutorials
    • 🚀Quick start guide
    • 📈Set up strategy
    • ➡️Connect TradingView
    • 💰Prop firms guide
    • 🖥️For developers
    • 🔍Regex
    • ⏳Cron expressions
    • 🏠My home IP
  • B2B
    • 👨‍💼Business-to-Business
    • 🏷️White label
  • MetaCopier
    • 🧑‍🤝‍🧑Affiliate Program
    • Frequently Asked Questions (FAQ)
    • ⛑️Troubleshooting
    • 💠App
    • 💲Billing
    • 💳Payment methods
    • 🤝Support
    • 🚧Feature request
    • 🚀Release notes
  • REST API
    • ✨SDK
      • Usage
        • C#
        • Java
        • Typescript
        • Python
        • Other
      • Generation
    • 💡API
      • Swagger
      • Readme.io
    • 🗒️Changes
  • Socket API
    • 💡API
    • 🗒️Changes
Powered by GitBook
On this page
  • 1. Overview
  • 2. Connection & Authentication
  • 3. Subscribe Flow
  • 4. Messages & Destinations
  • 5. Data Format (DTOs)
  • 6. Example Client Code
  • 7. Error Handling & Disconnects
  • 8. Connection Limits
  • Conclusion

Was this helpful?

  1. Socket API

API

PreviousChangesNextChanges

Last updated 26 days ago

Was this helpful?

The API is currently in beta, which means that breaking changes may be introduced

1. Overview

The WebSocket API provides real-time updates about various accounts (e.g., UpdateAccountInformationDTO, UpdateHistoryDTO, UpdateOpenPositionsDTO). Clients can subscribe to one or more accounts and automatically receive new data whenever it’s published.

Communication uses STOMP (Simple Text Oriented Messaging Protocol) over a standard WebSocket. You can use any STOMP client library that supports WebSockets (e.g., in Node.js or JavaScript).

Do not forget to add the 'Socket' feature to the account from which you want to receive real-time data. If the feature is not added, no updates will be sent for this account.

Note on Heartbeats In most cases, the STOMP client auto-configures heartbeats, ensuring the connection stays alive. However, if you notice the connection closes after around 20 seconds of inactivity, you may need to manually enable and tune the heartbeat settings (e.g., heart-beat: '20000,20000') so the client periodically sends a small frame to confirm that the connection is still valid.


2. Connection & Authentication

  • Endpoint: wss://api.metacopier.io/ws/api/v1

  • Authentication:

    • Each client must provide an api-key in the STOMP CONNECT headers.

    • Example STOMP connect header:

      "api-key": "YOUR_API_KEY"
    • If the api-key is missing or invalid, the connection will be rejected.


3. Subscribe Flow

After a successful STOMP CONNECT:

  1. SUBSCRIBE to a private queue destination such as:

    /user/queue/accounts/changes
    • This tells the server: “Send updates for my session to me at this address.”

  2. SEND a JSON body to "/app/subscribe" that indicates which accounts you want:

    • If you send an empty list, it means “subscribe me to all accounts that my api-key can access.”

    • If you send a non-empty list (["uuid1", "uuid2"]), you only receive updates for those specific accounts.

Example Subscription Request

{
  "accountIds": []
}

(Empty array => all accounts)

STOMP Frame

SEND
destination:/app/subscribe
content-type:application/json

{
  "accountIds": []
}
^@

4. Messages & Destinations

  • Receiving Updates:

    • You will receive messages on your private user queue (e.g. "/user/queue/accounts/changes").

    • The server sends a copy of each relevant update (based on your subscription) to only your session.

  • Message Types:

    1. UpdateAccountInformationDTO

    2. UpdateHistoryDTO (contains a list of PositionDTO)

    3. UpdateOpenPositionsDTO (contains a list of PositionDTO)

(See REST API for DTO format REST API)


5. Data Format (DTOs)

The server wraps each outgoing message in a MessageWrapper object with two fields:

  1. type – This is the simple name of the DTO class. For example:

    • "UpdateAccountInformationDTO"

    • "UpdateOpenPositionsDTO"

    • "UpdateHistoryDTO"

  2. data – The actual DTO (serialized as JSON).

Example JSON Messages

  1. UpdateAccountInformationDTO

    {
      "type": "UpdateAccountInformationDTO",
      "data": {
        "accountId": "11111111-1111-1111-1111-111111111111",
        "info": {
          "id": "11111111-1111-1111-1111-111111111111",
          "balance": 1000.0,
          "equity": 980.5,
          "leverage": 100,
          "profitThisMonth": 25.0,
          "currency": "USD",
          "connected": true,
          "fallbackMode": false,
          "compatibilityMode": false,
          "status": "active",
          "drawdown": 2.5,
          "riskLimitsStatus": [],
          "profitTargetsStatus": [],
          "isInvestorPassword": false,
          "brokeTimeOffsetToUtc": 2,
          "latencyInMs": 120,
          "openPositions": true,
          "pendingOrders": false,
          "positionMismatch": false,
          "proxy": {},
          "wrongCredentials": false,
          "pendingApprovals": [],
          "timestamp": "2025-02-13T12:00:00Z",
          "lastConnectedTimestamp": "2025-02-13T11:55:00Z",
          "lastCTraderTokenRefreshed": "2025-02-13T11:00:00Z",
          "allSymbolsInfoLoaded": true,
          "hftMode": false
        }
      }
    }
  2. UpdateHistoryDTO (history positions only for the last 24h)

    {
      "type": "UpdateHistoryDTO",
      "data": {
        "accountId": "22222222-2222-2222-2222-222222222222",
        "history": [
          {
            "id": "pos-123",
            "dealType": "...",
            "openTime": "2025-02-13T09:30:00Z",
            "closeTime": "2025-02-13T10:00:00Z",
            "openPrice": 1.2345
            // ...
          }
        ]
      }
    }
  3. UpdateOpenPositionsDTO

    {
      "type": "UpdateOpenPositionsDTO",
      "data": {
        "accountId": "33333333-3333-3333-3333-333333333333",
        "openPositions": [
          {
            "id": "pos-456",
            "symbol": "EURUSD",
            "openPrice": 1.1200,
            "closePrice": null,
            "volume": 0.1
            // ...
          }
        ]
      }
    }

Identifying the DTO

Since "type" is now the class simple name, you can simply check:

  • "UpdateAccountInformationDTO" => The "data" field is an UpdateAccountInformationDTO

  • "UpdateHistoryDTO" => The "data" field is an UpdateHistoryDTO

  • "UpdateOpenPositionsDTO" => The "data" field is an UpdateOpenPositionsDTO

For example (in JavaScript/Node):

if (message.type === 'UpdateAccountInformationDTO') {
  // parse message.data as UpdateAccountInformationDTO
} else if (message.type === 'UpdateHistoryDTO') {
  // parse message.data as UpdateHistoryDTO
} else if (message.type === 'UpdateOpenPositionsDTO') {
  // parse message.data as UpdateOpenPositionsDTO
}

This ensures you always know which DTO you’re handling without guessing based on field names.


6. Example Client Code

  • Node.js

  • Pyhton

  • Java - Springboot

  • C# (.NET)

  • Javascript - Browser

  • Typescript - Angular

Node.js

npm install @stomp/stompjs ws
const { Client } = require('@stomp/stompjs');
const WebSocket = require('ws');

const client = new Client({
  // Where to connect
  brokerURL: "wss://api.metacopier.io/ws/api/v1",
  // Provide your API key in the STOMP headers here
  connectHeaders: {
    // e.g. 'api-key': 'YOUR_API_KEY_HERE'
    'api-key': 'REPLACE_WITH_YOUR_API_KEY'
  },

  debug: (str) => console.log('[STOMP DEBUG]', str),
  reconnectDelay: 5000,

  // For Node.js, pass in the WebSocket factory
  webSocketFactory: () => new WebSocket('wss://api.metacopier.io/ws/api/v1')
});

client.onConnect = (frame) => {
  console.log('Connected via STOMP:', frame.headers);

  // 1) Subscribe to user-specific updates
  client.subscribe('/user/queue/accounts/changes', (message) => {
    console.log('RAW message =>', message.body);
    try {
      const data = JSON.parse(message.body);
      if (data.type === 'UpdateAccountInformationDTO') {
        console.log('Received UpdateAccountInformationDTO:', data.data);
      } else if (data.type === 'UpdateOpenPositionsDTO') {
        console.log('Received UpdateOpenPositionsDTO:', data.data);
      } else if (data.type === 'UpdateHistoryDTO') {
        console.log('Received UpdateHistoryDTO:', data.data);
      } else {
        console.log('Unknown payload type:', data);
      }
    } catch (err) {
      console.error('Failed to parse JSON', err);
    }
  });

  // 2) Send a subscription request to /app/subscribe
  //    If you want to subscribe to all accessible accounts, pass an empty array:
  const request = {
    accountIds: []
  };

  client.publish({
    destination: '/app/subscribe',
    body: JSON.stringify(request)
  });
};

client.onStompError = (frame) => {
  console.error('Broker error:', frame.headers['message'], frame.body);
};

client.activate();
  1. brokerURL: Points to the WebSocket/STOMP endpoint.

  2. connectHeaders: Replace 'REPLACE_WITH_YOUR_API_KEY' with your own API key.

  3. subscribe: We subscribe to "/user/queue/accounts/changes", which is a private user queue (messages are sent only to your session).

  4. publish: We send a JSON body to "/app/subscribe" specifying which account IDs we want ([] means all).

  5. message handling: We parse the JSON body. Because the server sends a MessageWrapper with a type field, we switch on data.type to see whether it’s an UpdateAccountInformationDTO, UpdateHistoryDTO, or UpdateOpenPositionsDTO.

That’s all you need to connect, authenticate, subscribe, and receive real-time data!

Python

pip3 install websocket-client stomper
import os
import json
import time
import threading
import websocket
import stomper

# Variables
METACOPIER_API_KEY = "MY_KEY"
WS_URL = "wss://api.metacopier.io/ws/api/v1"

def on_message(ws, message):
    # Use stomper to decode the incoming STOMP frame
    frame = stomper.unpack_frame(message)
    command = frame.get("cmd", "")
    
    if command == "CONNECTED":
        print("Connected via STOMP:", frame.get("headers", {}))
    elif command == "MESSAGE":
        body = frame.get("body", "")
        print("RAW message =>", body)
        try:
            data = json.loads(body)
            msg_type = data.get('type')
            if msg_type == 'UpdateAccountInformationDTO':
                print("Received UpdateAccountInformationDTO:", data.get('data'))
            elif msg_type == 'UpdateOpenPositionsDTO':
                print("Received UpdateOpenPositionsDTO:", data.get('data'))
            elif msg_type == 'UpdateHistoryDTO':
                print("Received UpdateHistoryDTO:", data.get('data'))
            else:
                print("Unknown payload type:", data)
        except Exception as err:
            print("Failed to parse JSON", err)
    elif command == "ERROR":
        # STOMP error frame
        headers = frame.get("headers", {})
        print("Broker error:", headers.get("message"), frame.get("body"))
    else:
        print("Unhandled STOMP frame:", command)

def on_error(ws, error):
    print("WebSocket error:", error)

def on_close(ws, close_status_code, close_msg):
    print("WebSocket closed")

def on_open(ws):
    def run():
        # Create STOMP connect frame
        connect_headers = {"api-key": METACOPIER_API_KEY}
        
        # Use stomper to create and send a proper STOMP 1.2 connect frame
        connect_frame = stomper.Frame()
        connect_frame.cmd = "CONNECT"
        connect_frame.headers = connect_headers
        connect_frame.headers["accept-version"] = "1.2"
        connect_frame.headers["heart-beat"] = "10000,10000"  # Added heartbeat for connection stability
        connect_frame.headers["host"] = "/"
        ws.send(connect_frame.pack())
        
        # Give time for the CONNECTED frame to arrive
        time.sleep(1)
        
        # Subscribe to user-specific updates
        subscribe_frame = stomper.subscribe("/user/queue/accounts/changes", "sub-0")
        ws.send(subscribe_frame)
        
        # Send a subscription request with an empty accountIds list
        request = {"accountIds": []}
        send_frame = stomper.send("/app/subscribe", json.dumps(request), content_type="application/json")
        ws.send(send_frame)
        
        # Keep the thread alive and implement heartbeat
        while True:
            time.sleep(10)  # Send heartbeat every 10 seconds
            try:
                ws.send(stomper.heartbeat())  # Send STOMP heartbeat
            except Exception as e:
                print("Error sending heartbeat:", e)
                break
    
    # Start the thread
    thread = threading.Thread(target=run)
    thread.daemon = True  # Make thread daemon so it exits when main thread exits
    thread.start()

if __name__ == "__main__":
    # Enable WebSocket trace logging (optional)
    websocket.enableTrace(True)
    
    while True:
        try:
            # Create and connect the WebSocket app
            ws_app = websocket.WebSocketApp(
                WS_URL,
                on_open=on_open,
                on_message=on_message,
                on_error=on_error,
                on_close=on_close,
            )
            
            # Run the WebSocket connection with ping_interval for connection monitoring
            ws_app.run_forever(ping_interval=30, ping_timeout=10)
            
            print("Connection lost, reconnecting in 5 seconds...")
            time.sleep(5)
        except KeyboardInterrupt:
            print("Program terminated by user")
            break
        except Exception as e:
            print(f"Error occurred: {e}, reconnecting in 5 seconds...")
            time.sleep(5)

Java - Springboot

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>5.3.26</version> <!-- or a matching Spring version of your choice -->
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-messaging</artifactId>
  <version>5.3.26</version>
</dependency>

<!-- For JSON handling (if not already included elsewhere) -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.14.2</version>
</dependency>
package com.example;

import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.simp.stomp.*;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;

import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;

public class StompClientWithJacksonExample {

    public static void main(String[] args) {
        // 1) Create the underlying WebSocket client
        WebSocketClient standardWebSocketClient = new StandardWebSocketClient();

        // 2) Create the STOMP client
        WebSocketStompClient stompClient = new WebSocketStompClient(standardWebSocketClient);

        // Use Jackson for JSON conversion (handles application/json)
        stompClient.setMessageConverter(new MappingJackson2MessageConverter());

        // 3) Prepare STOMP CONNECT headers (e.g. for your API key)
        StompHeaders connectHeaders = new StompHeaders();
        connectHeaders.set("api-key", "REPLACE_WITH_YOUR_API_KEY");

        // 4) (Optional) Set HTTP headers for the initial WebSocket handshake
        WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders();

        // 5) Create a custom session handler
        StompSessionHandler sessionHandler = new MyStompSessionHandler();

        // 6) Connect to the broker
        String url = "wss://api.metacopier.io/ws/api/v1";
        ListenableFuture<StompSession> future =
                stompClient.connect(url, handshakeHeaders, connectHeaders, sessionHandler);

        // 7) Optionally, block until connected (or handle asynchronously)
        try {
            StompSession session = future.get();
            System.out.println("Session established. ID => " + session.getSessionId());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 8) Keep the main thread alive so the subscription remains active
        try {
            while (true) {
                Thread.sleep(1000);
            }
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            System.err.println("Main thread interrupted, exiting.");
        }
    }

    /**
     * Our custom StompSessionHandler that subscribes to updates and sends a subscription request.
     */
    private static class MyStompSessionHandler extends StompSessionHandlerAdapter {

        @Override
        public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
            System.out.println("Connected via STOMP. Session => " + session.getSessionId());

            // 1) Subscribe to /user/queue/accounts/changes
            session.subscribe("/user/queue/accounts/changes", new StompFrameHandler() {
                @Override
                public Type getPayloadType(StompHeaders headers) {
                    // We expect JSON shaped like {"type":"SomeDTO", "data":{...}}
                    // We'll map it into MetaCopierResponseDto<Object> for demonstration
                    return MetaCopierResponseDto.class;
                }

                @Override
                public void handleFrame(StompHeaders headers, Object payload) {
                    // Jackson has converted the JSON payload into a MetaCopierResponseDto
                    if (!(payload instanceof MetaCopierResponseDto)) {
                        System.out.println("Received unknown payload: " + payload);
                        return;
                    }
                    MetaCopierResponseDto<?> dto = (MetaCopierResponseDto<?>) payload;
                    System.out.println("Received => " + dto);

                    // Check the "type" field to decide how to process the "data"
                    switch (dto.getType()) {
                        case "UpdateAccountInformationDTO":
                            System.out.println(" --> UpdateAccountInformation data: " + dto.getData());
                            break;
                        case "UpdateOpenPositionsDTO":
                            System.out.println(" --> UpdateOpenPositionsDTO data: " + dto.getData());
                            break;
                        case "UpdateHistoryDTO":
                            System.out.println(" --> UpdateHistoryDTO data: " + dto.getData());
                            break;
                        default:
                            System.out.println(" --> Unknown type: " + dto.getType());
                    }
                }
            });

            // 2) Send subscription request to /app/subscribe, labeled as JSON
            StompHeaders subscribeHeaders = new StompHeaders();
            subscribeHeaders.setDestination("/app/subscribe");
            subscribeHeaders.setContentType(MimeTypeUtils.APPLICATION_JSON);

            // Example request object that will be serialized to {"accountIds":[]}
            SubscriptionRequest request = new SubscriptionRequest();
            request.setAccountIds(Collections.emptyList());

            // Send the object; Jackson will convert it to JSON
            session.send(subscribeHeaders, request);
        }

        @Override
        public void handleException(StompSession session,
                                    StompCommand command,
                                    StompHeaders headers,
                                    byte[] payload,
                                    Throwable exception) {
            System.err.println("STOMP handleException => " + exception.getMessage());
        }

        @Override
        public void handleTransportError(StompSession session, Throwable exception) {
            System.err.println("STOMP handleTransportError => " + exception.getMessage());
        }
    }

    /**
     * Represents the JSON body we send to /app/subscribe,
     * e.g. {"accountIds": [...]}
     */
    private static class SubscriptionRequest {
        private List<Long> accountIds;

        public List<Long> getAccountIds() {
            return accountIds;
        }

        public void setAccountIds(List<Long> accountIds) {
            this.accountIds = accountIds;
        }
    }

    /**
     * Represents the general shape of responses from /user/queue/accounts/changes.
     * We expect messages like:
     *
     * {
     *   "type": "UpdateOpenPositionsDTO",
     *   "data": { ...some object... }
     * }
     */
    private static class MetaCopierResponseDto<T> {
        private String type;
        private T data;

        public String getType() {
            return type;
        }
        public void setType(String type) {
            this.type = type;
        }

        public T getData() {
            return data;
        }
        public void setData(T data) {
            this.data = data;
        }

        @Override
        public String toString() {
            return "MetaCopierResponseDto{" +
                    "type='" + type + '\'' +
                    ", data=" + data +
                    '}';
        }
    }
}

C# (.NET)

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace StompWebSocketClient
{
    class Program
    {
        private static ClientWebSocket _webSocket;
        private static bool _stompConnected;

        // Replace with your actual API key
        private const string ApiKey = "REPLACE_WITH_YOUR_API_KEY";

        static async Task Main(string[] args)
        {
            _webSocket = new ClientWebSocket();

            try
            {
                // 1) Connect via WebSocket to wss://api.metacopier.io/ws/api/v1
                var uri = new Uri("wss://api.metacopier.io/ws/api/v1");
                Console.WriteLine($"[WebSocket] Connecting to {uri} ...");
                await _webSocket.ConnectAsync(uri, CancellationToken.None);
                Console.WriteLine("[WebSocket] Connected!");

                // 2) Send STOMP CONNECT frame
                string connectFrame = $"CONNECT\n" +
                                      $"accept-version:1.2\n" +
                                      $"api-key:{ApiKey}\n" +
                                      $"\n\0"; // empty line, then \0 terminator
                await SendTextAsync(connectFrame);
                Console.WriteLine("[STOMP] Sent CONNECT frame");

                // 3) Start a background task to read incoming messages
                _ = Task.Run(() => ReceiveLoop());

                // 4) Keep the main thread alive
                //    We can do an infinite loop or wait on user input, etc.
                //    For demonstration, infinite loop + short sleep.
                while (true)
                {
                    await Task.Delay(1000);
                    if (_webSocket.State != WebSocketState.Open)
                    {
                        Console.WriteLine("[WebSocket] No longer open, exiting main loop...");
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[Main] Exception: {ex.Message}");
            }
            finally
            {
                if (_webSocket != null && _webSocket.State == WebSocketState.Open)
                {
                    await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
                }
                _webSocket.Dispose();
            }
        }

        /// <summary>
        /// Continuously reads frames from the WebSocket and processes STOMP frames.
        /// </summary>
        private static async Task ReceiveLoop()
        {
            var buffer = new byte[4096];
            var sb = new StringBuilder();

            while (_webSocket.State == WebSocketState.Open)
            {
                try
                {
                    var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        Console.WriteLine("[WebSocket] Received CLOSE frame from server");
                        await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
                        return;
                    }

                    // Accumulate the data into a StringBuilder
                    sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));

                    // If this is the end of the message, we try to parse STOMP frames
                    if (result.EndOfMessage)
                    {
                        string allData = sb.ToString();
                        sb.Clear();

                        // Split on '\0' to separate STOMP frames
                        var rawFrames = allData.Split('\0');
                        foreach (var frame in rawFrames)
                        {
                            string trimmed = frame.Trim();
                            if (!string.IsNullOrEmpty(trimmed))
                            {
                                HandleStompFrame(trimmed);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"[ReceiveLoop] Exception: {ex.Message}");
                    break;
                }
            }
        }

        /// <summary>
        /// Parses a STOMP frame (naively).
        /// </summary>
        private static void HandleStompFrame(string frame)
        {
            Console.WriteLine($"\n[STOMP] Incoming frame:\n{frame}\n");

            // The first line is the command
            var lines = frame.Split('\n', StringSplitOptions.RemoveEmptyEntries);
            if (lines.Length == 0) return;

            string command = lines[0].Trim().ToUpperInvariant();
            switch (command)
            {
                case "CONNECTED":
                    Console.WriteLine("[STOMP] CONNECTED => STOMP session established");
                    _stompConnected = true;
                    // Now we can SUBSCRIBE and SEND
                    SubscribeToChanges();
                    SendSubscribeRequest();
                    break;

                case "MESSAGE":
                    // A subscription update
                    Console.WriteLine("[STOMP] MESSAGE => subscription data");
                    break;

                case "ERROR":
                    // The broker responded with an error
                    Console.WriteLine("[STOMP] ERROR => " + frame);
                    break;

                default:
                    // Could be RECEIPT, or something else
                    Console.WriteLine("[STOMP] Command => " + command);
                    break;
            }
        }

        /// <summary>
        /// Send a SUBSCRIBE frame for /user/queue/accounts/changes
        /// </summary>
        private static async void SubscribeToChanges()
        {
            if (!_stompConnected) return;

            string subscribeFrame =
                "SUBSCRIBE\n" +
                "id:sub-0\n" +
                "destination:/user/queue/accounts/changes\n" +
                "\n\0";

            await SendTextAsync(subscribeFrame);
            Console.WriteLine("[STOMP] Sent SUBSCRIBE => /user/queue/accounts/changes");
        }

        /// <summary>
        /// Send a SEND frame to /app/subscribe with a JSON body: {"accountIds":[]}
        /// </summary>
        private static async void SendSubscribeRequest()
        {
            if (!_stompConnected) return;

            string jsonBody = "{\"accountIds\":[]}";
            string sendFrame =
                "SEND\n" +
                "destination:/app/subscribe\n" +
                "content-type:application/json\n" +
                "\n" +
                jsonBody +
                "\0";

            await SendTextAsync(sendFrame);
            Console.WriteLine("[STOMP] Sent SEND => /app/subscribe with JSON: " + jsonBody);
        }

        /// <summary>
        /// Helper method to send a text (STOMP) frame over the WebSocket.
        /// </summary>
        private static Task SendTextAsync(string text)
        {
            var bytes = Encoding.UTF8.GetBytes(text);
            return _webSocket.SendAsync(
                new ArraySegment<byte>(bytes),
                WebSocketMessageType.Text,
                true, // endOfMessage
                CancellationToken.None
            );
        }
    }
}

Javascript - Browser

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>STOMP over WebSocket Demo</title>
  </head>
  <body>
    <!-- Include the STOMP library via CDN -->
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@8.1.0/dist/stomp.min.js"></script>

    <script>
      // Once the script is loaded, we have the global StompJs object available

      // 1) Create a new STOMP Client
      const client = new StompJs.Client({
        // Where to connect
        brokerURL: 'wss://api.metacopier.io/ws/api/v1',

        // Provide your API key in the STOMP headers
        connectHeaders: {
          'api-key': 'REPLACE_WITH_YOUR_API_KEY'
        },

        // Enable console logging for debugging
        debug: (msg) => {
          console.log('[STOMP DEBUG]', msg);
        },

        // If the connection fails or drops, auto-reconnect after a delay (ms)
        reconnectDelay: 5000,

        // Called when the client is fully connected to the STOMP broker
        onConnect: (frame) => {
          console.log('[STOMP] Connected:', frame.headers);

          // 1) Subscribe to /user/queue/accounts/changes
          client.subscribe('/user/queue/accounts/changes', (message) => {
            console.log('[STOMP] RAW message =>', message.body);
            try {
              const data = JSON.parse(message.body);
              // Example checks, just like the Node.js snippet
              if (data.type === 'UpdateAccountInformationDTO') {
                console.log('[STOMP] Received UpdateAccountInformationDTO:', data.data);
              } else if (data.type === 'UpdateOpenPositionsDTO') {
                console.log('[STOMP] Received UpdateOpenPositionsDTO:', data.data);
              } else if (data.type === 'UpdateHistoryDTO') {
                console.log('[STOMP] Received UpdateHistoryDTO:', data.data);
              } else {
                console.log('[STOMP] Unknown payload type:', data);
              }
            } catch (err) {
              console.error('[STOMP] Failed to parse JSON', err);
            }
          });

          // 2) Send a subscription request to /app/subscribe
          //    If you want to subscribe to all accessible accounts, pass an empty array:
          const request = {
            accountIds: []
          };
          client.publish({
            destination: '/app/subscribe',
            body: JSON.stringify(request),
            // If you want to explicitly set content-type:
            // headers: { 'content-type': 'application/json' }
          });
        },

        // Called if the broker sends an ERROR frame
        onStompError: (frame) => {
          console.error('[STOMP] Broker error:', frame.headers['message'], frame.body);
        },

        // If the connection drops or fails
        onDisconnect: () => {
          console.warn('[STOMP] Disconnected from server');
        }
      });

      // 2) Activate (connect) the client
      client.activate();
    </script>
  </body>
</html>

Typescript - Angular

npm install --save @stomp/stompjs
// src/app/services/stomp.service.ts

import { Injectable } from '@angular/core';
import { Client, IMessage, StompSubscription } from '@stomp/stompjs';

// If you want to parse JSON or store messages in Observables, you can also import RxJS

@Injectable({
  providedIn: 'root'
})
export class StompService {
  private client: Client;
  private subscription: StompSubscription | null = null;

  constructor() {
    // 1) Create the STOMP Client
    this.client = new Client({
      // Where to connect
      brokerURL: 'wss://api.metacopier.io/ws/api/v1',

      // Provide your API key in the STOMP connect headers
      connectHeaders: {
        'api-key': 'REPLACE_WITH_YOUR_API_KEY'
      },

      // Enable console debugging
      debug: (msg: string) => {
        console.log('[STOMP DEBUG]', msg);
      },

      // Auto-reconnect delay
      reconnectDelay: 5000,
    });

    // 2) Set up callbacks
    this.client.onConnect = frame => {
      console.log('[STOMP] Connected:', frame.headers);
      this.onConnected();
    };

    this.client.onStompError = frame => {
      console.error('[STOMP] Broker error:', frame.headers['message'], frame.body);
    };
  }

  /**
   * Activate the STOMP client (actually opens the WebSocket connection).
   */
  public connect(): void {
    this.client.activate();
  }

  /**
   * Cleanly disconnect if needed (e.g., onDestroy of a component or app).
   */
  public disconnect(): void {
    if (this.client && this.client.active) {
      this.client.deactivate();
      console.log('[STOMP] Disconnected');
    }
  }

  /**
   * Called once the STOMP connection is fully established.
   * We can subscribe to our topic(s) and send any initial messages here.
   */
  private onConnected(): void {
    // 1) Subscribe to /user/queue/accounts/changes
    // The callback is triggered whenever a message arrives at this subscription.
    this.subscription = this.client.subscribe('/user/queue/accounts/changes', (message: IMessage) => {
      console.log('[STOMP] RAW message =>', message.body);

      // Attempt to parse JSON if needed
      try {
        const data = JSON.parse(message.body);
        // Check `data.type` or process however you like:
        if (data.type === 'UpdateAccountInformationDTO') {
          console.log('[STOMP] Received UpdateAccountInformationDTO:', data.data);
        } else if (data.type === 'UpdateOpenPositionsDTO') {
          console.log('[STOMP] Received UpdateOpenPositionsDTO:', data.data);
        } else if (data.type === 'UpdateHistoryDTO') {
          console.log('[STOMP] Received UpdateHistoryDTO:', data.data);
        } else {
          console.log('[STOMP] Unknown payload type:', data);
        }
      } catch (err) {
        console.error('[STOMP] Failed to parse JSON:', err);
      }
    });

    // 2) Send a subscription request to /app/subscribe
    //    If you want to subscribe to all accessible accounts, pass an empty array:
    const request = {
      accountIds: []
    };

    this.client.publish({
      destination: '/app/subscribe',
      body: JSON.stringify(request),
      // Optionally set content-type if needed:
      // headers: { 'content-type': 'application/json' }
    });
  }
}

7. Error Handling & Disconnects

  1. Invalid API Key: If your api-key is wrong or expired, the server will send a STOMP ERROR frame or close the socket. Check logs for the cause.

  2. Connection Loss: If the server or network goes down, your client might attempt to reconnect (reconnectDelay=5000 means it tries every 5 seconds).

  3. Disconnect: The client can call client.deactivate() (in @stomp/stompjs) or close the WebSocket to end the session.

  4. Server may forcibly close the connection if you exceed concurrency limits or do not have permission for certain accounts.


8. Connection Limits

  1. Global or Per-API-Key Limits: We may enforce a maximum number of concurrent WebSocket sessions per API key (or a global total). If you try to exceed these limits, the server will reject additional connections.

  2. Disconnect on Excess: If your API key opens more than the allowed sessions, the newest or oldest connection might be forcibly disconnected, or the server might send an ERROR frame.

  3. IP-Based Limits: We may also optionally limit the number of connections from a single IP address to prevent abuse.

  4. What This Means for You: If you encounter frequent disconnects or ERROR frames mentioning concurrency, verify how many clients are simultaneously connecting with your API key/IP.

  5. Contact Support: If you need higher concurrency or have special requirements, reach out to the support team.


Conclusion

This WebSocket STOMP API allows real-time account updates. Once connected (with a valid api-key), you send a subscription message specifying which account(s) you want, and you receive JSON-encoded DTOs for relevant updates.

If you have any further questions or need more details, please contact our support team. Enjoy building real-time solutions with the WebSocket API!

Here is a simplified Node.js example using and :

Here is a simplified Python example using and

💡
@stomp/stompjs
ws
websocket-client
stomper
@stomp/stompjs
Socket