# API

{% hint style="info" %}
The API is currently in beta, which means that breaking changes may be introduced
{% endhint %}

### 1. Overview

The **WebSocket API** provides **real-time updates** for various trading account data, including **account information**, **historical activity**, and **open positions**. Clients can subscribe to one or more accounts and automatically receive updates as new data becomes available.

The API uses the **STOMP (Simple Text Oriented Messaging Protocol)** over a standard WebSocket connection. Any **STOMP-compliant client library** that supports WebSockets can be used, such as **`@stomp/stompjs`** in JavaScript or Node.js.

This API sends the complete dataset (history) from the last 24 hours with each update, and all DTOs are complete - no deltas are sent. This ensures that no information is lost, even in the event of a brief network interruption.

A **single WebSocket connection** is sufficient to receive data for **all accounts**. If new accounts are added, their data will be **automatically included** in the same connection **without the need to reconnect or resubscribe**.

{% hint style="warning" %}
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. [#socket](https://docs.metacopier.io/features/pro-features#socket "mention")
{% endhint %}

{% hint style="warning" %}
**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.
{% endhint %}

***

### 2. Connection & Authentication

* **Endpoint**: `wss://api.metacopier.io/ws/api/v1`
* **Authentication**:
  * Each client **must** provide an `api-key` in the **STOMP CONNECT** headers.
  * You can use two types of API keys:
    * **Project-level API key**: Provides access to all accounts within the project. Generated under **Projects** → **(Choose a project)** → **API Keys**.
    * **Account API key**: Provides access only to that specific account. Automatically generated when an account is created. Retrieve using the [getAccountApiKeys](https://api.metacopier.io/rest/api/documentation/swagger-ui/index.html#/Account%20API/getAccountApiKeys) endpoint.
  * 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**

```json
{
  "accountIds": []
}
```

*(Empty array => all accounts)*

**STOMP Frame**

```txt
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](https://docs.metacopier.io/rest-api "mention")*)*

***

### 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**

   ```json
   {
     "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)**

   ```json
   {
     "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**

   ```json
   {
     "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):

```js
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

Here is a simplified Node.js example using [@stomp/stompjs](https://www.npmjs.com/package/@stomp/stompjs) and [ws](https://www.npmjs.com/package/ws):

```bash
npm install @stomp/stompjs ws
```

```js
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

Here is a simplified Python example using [websocket-client](https://pypi.org/project/websocket-client/) and [stomper](https://pypi.org/project/stomper/)

```python
pip3 install websocket-client stomper
```

```python
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"

# Global variables for connection management
connection_active = False
heartbeat_thread = None

def on_message(ws, message):
    # Check if this is a heartbeat (just a newline character)
    if message == b'\n' or message == '\n':
        print("Heartbeat received from server")
        return
    
    # 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):
    global connection_active
    connection_active = False
    print(f"WebSocket closed with code: {close_status_code}, message: {close_msg}")

def on_open(ws):
    global connection_active, heartbeat_thread
    connection_active = True
    
    print("WebSocket connection opened")
    
    # 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"] = "20000,20000"  # 20 second heartbeat
    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)
    
    # Start heartbeat in a separate function
    def send_heartbeat():
        while connection_active:
            time.sleep(20)  # Send heartbeat every 20 seconds
            if connection_active:
                try:
                    # Send proper STOMP heartbeat (newline character)
                    ws.send("\n")
                    print("Heartbeat sent")
                except Exception as e:
                    print("Error sending heartbeat:", e)
                    break
    
    # Start the heartbeat thread
    heartbeat_thread = threading.Thread(target=send_heartbeat)
    heartbeat_thread.daemon = True
    heartbeat_thread.start()

if __name__ == "__main__":
    # Enable WebSocket trace logging (optional)
    websocket.enableTrace(True)
    
    reconnect_count = 0
    
    while True:
        try:
            # Reset connection state
            connection_active = False
            
            print(f"Attempting to connect... (attempt {reconnect_count + 1})")
            
            # 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)
            
            # If we reach here, connection was closed
            if connection_active:
                print("Connection lost unexpectedly, attempting to reconnect...")
                reconnect_count += 1
                print(f"Reconnecting in 5 seconds... (attempt {reconnect_count})")
                time.sleep(5)
            else:
                print("Connection closed normally")
                break
                
        except KeyboardInterrupt:
            print("Program terminated by user")
            connection_active = False
            break
        except Exception as e:
            print(f"Error occurred: {e}")
            reconnect_count += 1
            print(f"Reconnecting in 5 seconds... (attempt {reconnect_count})")
            time.sleep(5)
    
    print("WebSocket client terminated")

```

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>
```

```java
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)

```csharp
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

```html
<!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
```

```typescript
// 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!
