{
  "openapi": "3.0.3",
  "info": {
    "title": "TradeRail MT5 API",
    "version": "1.0.0",
    "description": "The managed MT5 trading API. This file is generated from the same source as the reference at https://traderail.cloud/docs.html."
  },
  "servers": [
    {
      "url": "https://api.traderail.cloud"
    }
  ],
  "tags": [
    {
      "name": "Sessions & account",
      "description": "Open a session for an MT5 account, check that it is alive, and read its balance and equity."
    },
    {
      "name": "Market data",
      "description": "Live quotes, the symbols an account can trade, and historical bars and ticks."
    },
    {
      "name": "Trading",
      "description": "Place, modify, close and cancel orders, and list what is open right now."
    },
    {
      "name": "History & analytics",
      "description": "Closed trades, deals, pending-order history and performance statistics."
    },
    {
      "name": "Copy-trade & system",
      "description": "Act on many accounts with one call, and check the health of the service."
    },
    {
      "name": "Real-time",
      "description": "Push updates over WebSocket instead of polling."
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "ApiKey"
      },
      "bearer": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  },
  "paths": {
    "/v1/ConnectEx": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "connect-ex",
        "summary": "Connect an account",
        "description": "Start a session for an MT5 account, or confirm the one already running, and get the account id that every other call uses.\n\nCall ConnectEx with the account's login, password and trade-server name. If the account has no live session, the service signs it in and only answers once the account is connected to the broker's trade server. It waits up to about two minutes for that, and longer in total if other accounts are being started at the same moment, because starts are queued. If the session is already live and you send the same password, the call returns almost immediately and nothing is restarted. On success the body is the account id as plain text (acct-<login>), which you pass as ?id= on every other /v1 call. On failure the body is JSON { \"code\", \"message\" } with the HTTP status set by errorReplyStatusCode (405 unless you choose another), so branch on code, not on the status.\n\n- The success body is plain text such as acct-700100200, not JSON. Read it as a string and do not JSON-parse it.\n\n- Every failure from this call except the 401 uses the same HTTP status (405 unless you set errorReplyStatusCode), so always branch on the code field of the JSON body. Do not set errorReplyStatusCode to a 2xx value, or failures will look like successes.\n\n- All parameters go in the query string, so URL-encode the password and server name. Characters such as +, &, # and % otherwise change or cut off the value. Keep these URLs out of your own logs.\n\n- Before you can connect with a new password, the live session has to stop. GET /v1/Disconnect does not stop anything. Use POST /orchestrator/disconnect, which needs the Bearer token described under Authentication, or let the session reach its idleHours limit. The next ConnectEx then signs in fresh with the new password.\n\n- The password check on a live session only works once this service has verified a password for that account. If no password has been verified yet, a repeat ConnectEx reports the account as connected without testing the password you sent.\n\n- Once a password has been verified for an account, any other password is refused with CREDENTIALS_MISMATCH while the session is live. That includes switching between the master and the investor password.\n\n- A repeat ConnectEx for a live session is only checked when you send both password and server, and even then only the password is compared, not the server name. With either one left out, a live session simply returns its id.\n\n- A successful ConnectEx that includes password and server replaces the account's idle setting. If you leave out idleHours on such a call, the account goes back to the service default. An empty idleHours= counts as 0, which means never stop. Calls without password and server, and failed calls, leave the idle setting unchanged.\n\n- Only calls that read from or act on the account count as activity for idleHours, for example AccountSummary, OpenedOrders, GetQuote, history and order calls. CheckConnect, ConnectByToken, Reconnect and a repeat ConnectEx on a live session do not count. Once an idle session has stopped, call ConnectEx again with password and server to bring it back.\n\n- If several ConnectEx calls for the same login arrive while a start is in progress, they all get the result of that one start, whatever password each of them sent.\n\n- When many accounts start at once, the starts are queued, so set your HTTP client timeout well above two minutes for this call.\n\n- If the account's session is already restarting, for example during an automatic recovery, ConnectEx waits for it to come up instead of restarting it again.\n\n- After LOGIN_REJECTED, NOT_AUTHORIZED or CONNECT_TIMEOUT the failed attempt is cleaned up, so a retry with corrected details starts fresh.\n\n- Call ConnectEx once and then reuse the id. Call it again when ConnectByToken returns INVALID_TOKEN or a data call returns 502 saying the account is not connected.",
        "parameters": [
          {
            "name": "user",
            "in": "query",
            "required": true,
            "description": "The MT5 account login (account number).",
            "schema": {
              "type": "integer"
            },
            "example": 700100200
          },
          {
            "name": "password",
            "in": "query",
            "required": true,
            "description": "The account's MT5 password (master or investor). Used to sign the account in, and to check the password when the account already has a live session.",
            "schema": {
              "type": "string"
            },
            "example": "S3cret-Pass!"
          },
          {
            "name": "server",
            "in": "query",
            "required": true,
            "description": "The broker's trade-server name exactly as MT5 shows it.",
            "schema": {
              "type": "string"
            },
            "example": "PUPrime-Demo"
          },
          {
            "name": "symbol",
            "in": "query",
            "required": false,
            "description": "Optional symbol used while a new session starts. Usually you leave it out and the service picks a suitable symbol for the broker. A value with other characters, or longer than 64 characters, is silently ignored and the broker default is used. If you pass a well-formed symbol the broker does not offer, the session cannot come up and the call fails.",
            "schema": {
              "type": "string"
            },
            "example": "EURUSD"
          },
          {
            "name": "idleHours",
            "in": "query",
            "required": false,
            "description": "Stops the session automatically after this many hours with no account activity. Fractions are allowed: 0.5 means 30 minutes. 0 means never stop. Leave it out to use the service default. A negative or non-numeric value is ignored and the service default applies. Default: service default.",
            "schema": {
              "type": "number"
            },
            "example": 3
          },
          {
            "name": "errorReplyStatusCode",
            "in": "query",
            "required": false,
            "description": "The HTTP status used for this call's failure responses (invalid login, not connected, and every sign-in failure). It does not change the 401 for a missing or invalid ApiKey. 0 or a non-numeric value falls back to 405. The JSON body still carries the specific failure code.",
            "schema": {
              "type": "integer",
              "default": 405
            },
            "example": 409
          }
        ],
        "responses": {
          "200": {
            "description": "The account id as plain text, not JSON. Pass it as ?id= on later /v1 calls.",
            "content": {
              "text/plain; charset=utf-8": {
                "example": "acct-700100200"
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, wrong or belongs to a deactivated key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "405": {
            "description": "`INVALID_REQUEST` — user is missing, is not a whole number, or is not positive. Message: \"invalid user (login)\".\n\n`NOT_CONNECTED` — password or server was left out and the account has no live session. Without both, ConnectEx only confirms an existing session and never starts one.\n\n`CREDENTIALS_MISMATCH` — A session for this login is live, or is already starting, and a different password has already been verified for it. The running session is left exactly as it was and nothing is restarted. The message explains how to change the password.\n\n`LOGIN_REJECTED` — The broker refused the sign-in because the login, password or server name is wrong. This usually comes back within seconds, and the message includes the broker's rejection line.\n\n`NOT_AUTHORIZED` — The account started but never reached the broker's trade server within the wait window (about 75 seconds). Either the login, password or server name is wrong, or the broker cannot be reached right now.\n\n`CONNECT_TIMEOUT` — The session did not come up before the connect timeout (about two minutes; the message gives the exact number of seconds). Check the login, password and server name, then retry.\n\n`CONNECT_FAILED` — The session could not be started for some other reason. Retry later."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/ConnectByToken": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "connect-by-token",
        "summary": "Check a session id",
        "description": "Report whether the account behind an id has a live session, as a JSON code.\n\nReturns { \"code\": \"OK\" } when the account behind ?id= has a live session, and { \"code\": \"INVALID_TOKEN\", \"message\": \"token not found or expired\" } when it does not. Both answers come back as HTTP 200, so read the code field. Use it to check a stored id before you reuse it. On INVALID_TOKEN, call ConnectEx again with the password and server. Despite its name, this call does not connect anything.\n\n- Both outcomes are HTTP 200, so check the code field rather than the status.\n\n- A missing or malformed id returns INVALID_TOKEN, not an error status.\n\n- This call does not count as account activity, so polling it will not stop a session with an idleHours limit from being stopped.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account id returned by ConnectEx (acct-<login>), or the bare login.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"code\": \"OK\" } when the session is live. Otherwise { \"code\": \"INVALID_TOKEN\", \"message\": \"token not found or expired\" }, also with HTTP 200.",
            "content": {
              "application/json; charset=utf-8": {
                "example": {
                  "code": "OK"
                }
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, wrong or belongs to a deactivated key."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/Reconnect": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "reconnect",
        "summary": "Confirm a session",
        "description": "Confirm that an account's session is live and get its id back. Nothing is restarted or reconnected.\n\nReturns { \"code\": \"OK\", \"terminalInstanceGuid\": \"acct-<login>\" } when the account behind ?id= has a live session. When it has none, it returns HTTP 405 with { \"code\": \"NOT_CONNECTED\", \"message\" }. Despite its name, this call never starts, restarts or reconnects a session. Use ConnectEx for that. terminalInstanceGuid is the same acct-<login> account id that ConnectEx returns.\n\n- The failure status is always 405 here. errorReplyStatusCode is not supported on this endpoint.\n\n- Reconnect does not recover a dropped session. After a NOT_CONNECTED answer, call ConnectEx with the password and server.\n\n- This call does not count as account activity for idleHours.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account id returned by ConnectEx (acct-<login>), or the bare login.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          }
        ],
        "responses": {
          "200": {
            "description": "The session is live. terminalInstanceGuid is the account id.",
            "content": {
              "application/json; charset=utf-8": {
                "example": {
                  "code": "OK",
                  "terminalInstanceGuid": "acct-700100200"
                }
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, wrong or belongs to a deactivated key."
          },
          "405": {
            "description": "`NOT_CONNECTED` — The account has no live session, or ?id= is missing or malformed. Body: {\"code\":\"NOT_CONNECTED\",\"message\":\"account not connected to bridge\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/CheckConnect": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "check-connect",
        "summary": "Check connection",
        "description": "The lightest session check: plain-text ok when the session is up, otherwise HTTP 403.\n\nReturns HTTP 200 with the plain-text body ok when the account behind ?id= has an active session, and HTTP 403 with an empty body when it does not. It reads no account data and makes no round trip to the broker, so it answers immediately and is safe to call often. It tells you whether the session exists, not whether the account can trade right now, so use a data call such as AccountSummary when you need to confirm that. When you get 403, start a session with ConnectEx.\n\n- A 403 here means there is no active session, not an authentication problem. A bad API key gets 401.\n\n- The success body is plain text, not JSON.\n\n- An ok answer does not confirm that the account's connection to the broker's trading server is up, so a session can report ok here while trading calls still fail.\n\n- This call does not count as account activity, so polling it will not keep a session with an idleHours limit from being stopped for inactivity.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account id returned by ConnectEx (acct-<login>), or the bare login. Anything that does not resolve to a positive whole-number login is treated as having no session.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          }
        ],
        "responses": {
          "200": {
            "description": "The account has an active session.",
            "content": {
              "text/plain; charset=utf-8": {
                "example": "ok"
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing or wrong, or the key belongs to an account that is not active. The body is JSON: {\"message\": \"ApiKey required\", \"error\": \"Unauthorized\", \"statusCode\": 401}."
          },
          "403": {
            "description": "`(empty body)` — The account has no active session, or ?id= is missing or malformed. The response body is empty."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/Disconnect": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "disconnect",
        "summary": "Disconnect (no-op)",
        "description": "Kept for client compatibility. It always answers OK and does not stop or change any session.\n\nGET /v1/Disconnect always returns { \"code\": \"OK\" } and has no effect. The session keeps running, its positions stay open, and the same id keeps working. It exists so that clients which call Disconnect at the end of a flow keep working. To actually stop a session, call POST /orchestrator/disconnect, which needs the Bearer token described under Authentication, or have it stop automatically with ConnectEx's idleHours.\n\n- All query parameters, including id, are ignored.\n\n- Calling it does not clear the password the session was started with. A later ConnectEx with a different password is still refused with CREDENTIALS_MISMATCH.\n\n- Do not use it to free up an account or close positions. Nothing is stopped and nothing is closed.",
        "responses": {
          "200": {
            "description": "Always returned. Nothing was changed.",
            "content": {
              "application/json; charset=utf-8": {
                "example": {
                  "code": "OK"
                }
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, wrong or belongs to a deactivated key."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/Ping": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "ping",
        "summary": "Ping",
        "description": "Check that the API is reachable and that your API key is accepted.\n\nReturns { \"code\": \"OK\" } for any request that carries a valid ApiKey header. It uses no account, so it is the simplest way to confirm connectivity and your key before you start a session. A missing or invalid key returns 401.\n\n- Ping tells you nothing about any account's session. Use CheckConnect or ConnectByToken for that.\n\n- Ping needs an API key. For a probe without credentials, use GET /healthz.",
        "responses": {
          "200": {
            "description": "The API is reachable and the key is valid.",
            "content": {
              "application/json; charset=utf-8": {
                "example": {
                  "code": "OK"
                }
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, wrong or belongs to a deactivated key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/AccountSummary": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "account-summary",
        "summary": "Get account summary",
        "description": "Live balance, equity, margin and floating profit/loss for a connected account.\n\nReads the account's current figures from its live session. All money values are in the account currency, given in currency. Some fields appear under two names for client compatibility: company and accountCompanyName both hold the broker's company name, and server and accountServer both hold the trade-server name. The account needs a live session; without one the call returns 502.\n\n- A 502 saying the account is not connected means the session has ended, for example after an idle stop. Call ConnectEx again with the password and server.\n\n- company and accountCompanyName are the same value, and so are server and accountServer. Read whichever name your client expects.\n\n- leverage is the right-hand side of the ratio as an integer, so 500 means 1:500.\n\n- marginLevel is equity divided by margin, as a percentage. It is 0 when no margin is in use.\n\n- profit is the floating profit or loss of open positions, and equity already includes it.\n\n- Numbers carry up to 8 decimal places, so round them for display.\n\n- Each call counts as account activity, which resets the idleHours clock.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account id returned by ConnectEx (acct-<login>), or the bare login.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          }
        ],
        "responses": {
          "200": {
            "description": "Account figures. balance, equity, margin, freeMargin, marginLevel and profit are numbers. leverage and login are integers. currency, company, accountCompanyName, server and accountServer are strings.",
            "content": {
              "application/json; charset=utf-8": {
                "example": {
                  "balance": 10000,
                  "equity": 10152.4,
                  "margin": 216,
                  "freeMargin": 9936.4,
                  "marginLevel": 4700.18518519,
                  "leverage": 500,
                  "profit": 152.4,
                  "currency": "USD",
                  "accountCompanyName": "Example Markets Ltd",
                  "company": "Example Markets Ltd",
                  "accountServer": "ExampleMarkets-Demo",
                  "server": "ExampleMarkets-Demo",
                  "login": 700100200
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — ?id= is missing or is not acct-<login> or a positive login. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, wrong or belongs to a deactivated key."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account 700100200 is not connected\"), did not answer within 5 seconds, or the read failed. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/AccountDetails": {
      "get": {
        "tags": [
          "Sessions & account"
        ],
        "operationId": "account-details",
        "summary": "Get server time-zone details",
        "description": "Returns the server time-zone fields some clients read at start-up. They are always zero.\n\nReturns { \"serverTimeZone\": 0, \"serverTimeZoneInMinutes\": 0, \"utcOffsetMinutes\": 0 } on every request. The values are fixed and are not read from the broker or the account. No id is needed, and one is ignored if sent. The endpoint exists so that clients which look up a server time-zone offset during start-up keep working.\n\n- The values are the same for every broker, so do not use this endpoint to find out a broker's server time zone.\n\n- It does not need a live session and succeeds even when the account is not connected.",
        "responses": {
          "200": {
            "description": "Fixed time-zone fields. All three are always 0.",
            "content": {
              "application/json; charset=utf-8": {
                "example": {
                  "serverTimeZone": 0,
                  "serverTimeZoneInMinutes": 0,
                  "utcOffsetMinutes": 0
                }
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, wrong or belongs to a deactivated key."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/GetQuote": {
      "get": {
        "tags": [
          "Market data"
        ],
        "operationId": "get-quote",
        "summary": "Get a quote",
        "description": "Returns the current bid, ask and last price for one symbol on a connected account.\n\nReturns the latest bid, ask and last price the broker has published for one symbol on a connected account. You can send a base name such as EURUSD or XAUUSD, and it is mapped to this broker's exact symbol name for you (see Symbol names). If no price has arrived yet for the symbol, which is typical the first time a symbol is requested in a session, the API waits briefly and retries once before it answers. The response holds prices only. It has no timestamp, spread or symbol name, so work out the spread as ask - bid and use TickHistory when you need timestamped prices. Quoting a symbol also adds it to the account's active (Market Watch) set, which matters for Symbols on very large brokers.\n\n- A missing or empty symbol is not rejected up front. Instead of a 400, the call returns 502 with the message \"symbol is required\".\n\n- Treat a bid or ask of 0 as \"no price available\" and never as a real price. You can still get 0 after the automatic retry if the broker has not published a price for that symbol.\n\n- When the market is closed, the quote is the last price the broker published. The response has no timestamp, so this endpoint alone cannot tell a stale quote from a live one.\n\n- last is the price of the most recent trade. It can be 0 for symbols where the broker publishes only bid and ask, which is typical of forex and CFDs.\n\n- The resolved broker symbol name is not returned. Use SymbolList, or TickHistory (which returns it), to see the exact name that was used.\n\n- By default each price read gets about 5 seconds. The first symbol-taking call on an account can take longer, because the account's symbol list is loaded once for name resolution and then cached for up to 10 minutes.",
        "parameters": [
          {
            "name": "ApiKey",
            "in": "header",
            "required": true,
            "description": "Your API key.",
            "schema": {
              "type": "string"
            },
            "example": "YOUR_API_KEY"
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "symbol",
            "in": "query",
            "required": true,
            "description": "Symbol to quote. A base name like EURUSD is resolved to the broker's exact name (for example EURUSD.s). An exact broker name is used as is. Matching is case-sensitive.",
            "schema": {
              "type": "string"
            },
            "example": "EURUSD"
          }
        ],
        "responses": {
          "200": {
            "description": "Latest prices for the symbol: bid, ask and last, all numbers. No other fields are returned.",
            "content": {
              "application/json": {
                "example": {
                  "bid": 1.08412,
                  "ask": 1.08425,
                  "last": 0
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — ?id= is missing or is not a valid account id (message \"account id (?id=) required\")."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, the key is unknown, or the key is no longer active."
          },
          "502": {
            "description": "`ERROR` — One of these: the symbol is missing or empty (message \"symbol is required\"), the broker has no such symbol (\"unknown symbol: ...\"), the account is not connected, or no price could be read within the time limit."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/SymbolList": {
      "get": {
        "tags": [
          "Market data"
        ],
        "operationId": "symbol-list",
        "summary": "List symbol names",
        "description": "Returns the exact name of every symbol the broker offers on the account.\n\nReturns a flat JSON array with the name of every symbol the broker makes available to this account. Each name is spelled exactly as the broker spells it, including any suffix such as \".s\". Use it to build a symbol picker, or to find a broker's exact spelling before calling GetQuote, PriceHistoryEx or OrderSendTask. The list covers every symbol the broker offers, not only the account's active ones. On some brokers it runs to many thousands of entries. Fetch it once per account and cache it.\n\n- The endpoint has no filter, search or paging parameters. Every call returns the whole list.\n\n- Names come back in the broker's own order and are not guaranteed to be sorted. Sort them client-side if you display them.\n\n- The list can include symbols the account can see but is not allowed to trade. Use tradeMode from Symbols to check a symbol before you offer it for trading.\n\n- On brokers with a very large catalogue, Symbols returns details only for the account's active symbols. On those brokers, not every name from SymbolList has an entry in Symbols.\n\n- A large list can take a while. The read has up to 60 seconds once it starts before it fails with 502. Requests for the same account run one at a time, so the read may first wait behind earlier calls, and later calls wait while it runs.\n\n- SymbolList does no name resolution. The names it returns are exactly the ones the broker uses.",
        "parameters": [
          {
            "name": "ApiKey",
            "in": "header",
            "required": true,
            "description": "Your API key.",
            "schema": {
              "type": "string"
            },
            "example": "YOUR_API_KEY"
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          }
        ],
        "responses": {
          "200": {
            "description": "Array of exact broker symbol names (strings).",
            "content": {
              "application/json": {
                "example": [
                  "EURUSD.s",
                  "GBPUSD.s",
                  "USDJPY.s",
                  "XAUUSD.s",
                  "BTCUSD"
                ]
              }
            }
          },
          "400": {
            "description": "`ERROR` — ?id= is missing or is not a valid account id. The body is { \"code\": \"ERROR\", \"message\": \"account id (?id=) required\" }."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, the key is unknown, or the key is no longer active. The body is { \"statusCode\": 401, \"message\": \"ApiKey required\", \"error\": \"Unauthorized\" }, so \"Unauthorized\" is in the error field and there is no code field."
          },
          "502": {
            "description": "`ERROR` — The account is not connected (message \"account <login> is not connected\"), the account rejected the request, or the list could not be read within 60 seconds. The body is { \"code\": \"ERROR\", \"message\": \"...\" }."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/Symbols": {
      "get": {
        "tags": [
          "Market data"
        ],
        "operationId": "symbols",
        "summary": "Get symbol specifications",
        "description": "Returns trading specifications for the account's symbols as an object keyed by exact broker symbol name.\n\nReturns an object whose keys are exact broker symbol names and whose values are each symbol's trading specification. That covers price precision (digits, point, tick size), the base, profit and margin currencies, whether and how the symbol can be traded (tradeMode), and the minimum, maximum and step for order volume. Use it to round prices to the symbol's tick size, and lot sizes to volumeStep, before calling OrderSendTask. On brokers that list more than 4,000 symbols, only the account's active (Market Watch) symbols are included so the call stays responsive. Any symbol you have quoted, charted or traded through the API becomes active, so if one is missing, call GetQuote for it and then fetch Symbols again. The response can be large, so cache it per account.\n\n- The description and path fields are present for compatibility, but they are currently always empty strings.\n\n- tickSize and trade_tick_size always hold the same value. Note the mixed naming: trade_tick_size is the only field that is not camelCase.\n\n- tradeMode is the MT5 symbol trade mode: 0 = trading disabled, 1 = long only, 2 = short only, 3 = close only, 4 = full trading.\n\n- Contract size and tick value are not part of this response.\n\n- There is no parameter for requesting a single symbol. Every call returns the full map.\n\n- On brokers that list more than 4,000 symbols, a symbol you have never quoted, charted or traded will be missing from the map even though SymbolList shows it.\n\n- The call has up to 60 seconds before it fails with 502, and other requests for the same account wait while it runs.",
        "parameters": [
          {
            "name": "ApiKey",
            "in": "header",
            "required": true,
            "description": "Your API key.",
            "schema": {
              "type": "string"
            },
            "example": "YOUR_API_KEY"
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          }
        ],
        "responses": {
          "200": {
            "description": "Object keyed by exact broker symbol name. Each value has these fields: symbol, description, path, digits, point, tickSize, trade_tick_size, currencyBase, currencyProfit, currencyMargin, tradeMode, volumeMin, volumeMax, volumeStep.",
            "content": {
              "application/json": {
                "example": {
                  "EURUSD.s": {
                    "symbol": "EURUSD.s",
                    "description": "",
                    "path": "",
                    "digits": 5,
                    "point": 0.00001,
                    "tickSize": 0.00001,
                    "trade_tick_size": 0.00001,
                    "currencyBase": "EUR",
                    "currencyProfit": "USD",
                    "currencyMargin": "EUR",
                    "tradeMode": 4,
                    "volumeMin": 0.01,
                    "volumeMax": 100,
                    "volumeStep": 0.01
                  },
                  "XAUUSD.s": {
                    "symbol": "XAUUSD.s",
                    "description": "",
                    "path": "",
                    "digits": 2,
                    "point": 0.01,
                    "tickSize": 0.01,
                    "trade_tick_size": 0.01,
                    "currencyBase": "XAU",
                    "currencyProfit": "USD",
                    "currencyMargin": "XAU",
                    "tradeMode": 4,
                    "volumeMin": 0.01,
                    "volumeMax": 50,
                    "volumeStep": 0.01
                  }
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — ?id= is missing or is not a valid account id (message \"account id (?id=) required\")."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, the key is unknown, or the key is no longer active."
          },
          "502": {
            "description": "`ERROR` — The account is not connected, or the specifications could not be read within 60 seconds."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/PriceHistoryEx": {
      "get": {
        "tags": [
          "Market data"
        ],
        "operationId": "price-history-ex",
        "summary": "Get price history (OHLC bars)",
        "description": "Returns the most recent OHLC bars for a symbol at a chosen timeframe, oldest first.\n\nReturns up to 5,000 of the most recent OHLC bars for one symbol, which is the data behind a price chart. You give the timeframe in minutes, and it is snapped to the nearest standard MT5 timeframe: 1, 5, 15, 30, 60, 240, 1440, 10080 or 43200 minutes, that is M1 to MN1. Bars are ordered oldest to newest, and the window always ends at the current bar. While the market is open, that last bar is still forming, so its high, low and close keep moving until it closes. You cannot ask for a specific date range; to see further back, request more bars or a longer timeframe. Bar times are in the broker's server time and have no time-zone suffix.\n\n- timeFrame must be a number of minutes. A name such as H1 or D1 is not understood, and you silently get 30-minute bars.\n\n- An empty numBars= parameter is read as 0 and returns a single bar. Leave the parameter out entirely to get the default of 100.\n\n- volume is the bar's tick volume, meaning the number of price updates in the bar. It is not traded lots or notional.\n\n- Bar times look like ISO 8601 but carry no zone. They use the broker's server clock, which on many brokers runs ahead of UTC, so do not append Z or treat them as UTC.\n\n- You may get fewer bars than numBars, or an empty array, if the broker holds less history for that symbol and timeframe.\n\n- The first request for a symbol and timeframe that has not been used recently may be slow, return fewer bars than asked, or fail with 502 while history is still loading. If that happens, retry after a short wait.\n\n- The history read itself is allowed 30 seconds. The first call for an account can take longer in total, because the symbol name is resolved first.\n\n- The response does not repeat the symbol or timeframe that was used, so keep track of what you asked for.",
        "parameters": [
          {
            "name": "ApiKey",
            "in": "header",
            "required": true,
            "description": "Your API key.",
            "schema": {
              "type": "string"
            },
            "example": "YOUR_API_KEY"
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "symbol",
            "in": "query",
            "required": true,
            "description": "Symbol to chart. Surrounding whitespace is trimmed. A base name like EURUSD is resolved to the broker's exact name (for example a suffixed variant). An exact broker name is used as is.",
            "schema": {
              "type": "string"
            },
            "example": "EURUSD"
          },
          {
            "name": "timeFrame",
            "in": "query",
            "required": false,
            "description": "Bar size in minutes. Supported values are 1 (M1), 5 (M5), 15 (M15), 30 (M30), 60 (H1), 240 (H4), 1440 (D1), 10080 (W1) and 43200 (MN1). Decimals are rounded down first. Any other positive number then snaps to the nearest supported value, and an exact tie goes to the shorter timeframe (45 gives M30, 10 gives M5). Zero, negative, empty or non-numeric values fall back to 30, and that includes names such as \"H1\".",
            "schema": {
              "type": "integer",
              "default": 30
            },
            "example": 30
          },
          {
            "name": "timeFrameMinutes",
            "in": "query",
            "required": false,
            "description": "Alias for timeFrame, with the same meaning and snapping. It is used only when timeFrame is not sent at all. An empty timeFrame= still counts as sent and gives 30-minute bars.",
            "schema": {
              "type": "integer"
            },
            "example": 15
          },
          {
            "name": "numBars",
            "in": "query",
            "required": false,
            "description": "How many of the most recent bars to return. Decimals are rounded down. Values below 1 become 1, values above 5000 become 5000, and a non-numeric value falls back to 100.",
            "schema": {
              "type": "integer",
              "default": 100
            },
            "example": 200
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Accepted for compatibility but ignored. The newest numBars bars are always returned.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Array of bars, oldest first. Each bar has time (broker server time, ISO 8601 with no zone), open, high, low, close and volume (tick volume). While the market is open, the last bar is the one still forming. The array can be empty if no bars are available.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "time": "2026-10-07T14:00:00",
                    "open": 1.08391,
                    "high": 1.08447,
                    "low": 1.08372,
                    "close": 1.08436,
                    "volume": 1843
                  },
                  {
                    "time": "2026-10-07T14:30:00",
                    "open": 1.08436,
                    "high": 1.08458,
                    "low": 1.08401,
                    "close": 1.08415,
                    "volume": 1527
                  },
                  {
                    "time": "2026-10-07T15:00:00",
                    "open": 1.08415,
                    "high": 1.08429,
                    "low": 1.08398,
                    "close": 1.08412,
                    "volume": 412
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`ERROR` — ?id= is missing or is not a valid account id (message \"account id (?id=) required\").\n\n`ERROR` — symbol is missing, empty or only whitespace (message \"symbol required\")."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, the key is unknown, or the key is no longer active (message \"ApiKey required\")."
          },
          "502": {
            "description": "`ERROR` — The price history could not be read. Typical causes: the broker has no such symbol (\"unknown symbol: ...\"), the account is not connected (\"account ... is not connected\"), the history read failed (\"CopyRates failed for ...\"), or the read did not finish within 30 seconds."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/TickHistory": {
      "get": {
        "tags": [
          "Market data"
        ],
        "operationId": "tick-history",
        "summary": "Get tick history",
        "description": "Returns every raw tick for a symbol within a time window of up to 6 hours.\n\nReturns the individual price updates (ticks) for one symbol between from and to. Each tick has bid, ask, last, volume and a time to the millisecond. That is finer than any bar timeframe, which makes ticks useful for spread analysis, back-testing or replaying price action. By default the window is the 15 minutes ending at to, and to defaults to now. A window can be at most 6 hours long: a longer request is cut to the 6 hours ending at to, and the response sets truncated: true. To cover a longer period, page backwards. The response also returns the window actually used (in epoch seconds) and the broker's exact symbol name.\n\n- Tick times use the broker's server clock, which on many brokers runs ahead of UTC. PriceHistoryEx bar times use the same clock, and the from/to window is matched against it too. The default to is the current UTC time. On a broker N hours ahead of UTC, the default window therefore returns ticks from about N hours ago and none of the latest ones. To target a period precisely, pass from and to as the broker's server wall-clock time with no zone suffix, in the same form PriceHistoryEx returns.\n\n- When truncated is true, you received only the last 6 hours of the window you asked for. To fetch the rest, repeat the call with the same from and with to set to the from value in the response. If you leave from out, it falls back to 15 minutes before the new to. Use time_msc to drop any tick that appears in both pages at their shared boundary.\n\n- A from or to value that cannot be parsed is not rejected. It is silently replaced by its default, so check the from and to in the response. Epoch values must be whole numbers: a decimal such as 1791373500.5 counts as unparseable.\n\n- Ticks are not aggregated, so a busy symbol returns a very large response even inside the 6-hour limit. Keep windows as short as you can. Reading the ticks has a 30-second limit, and past it the call fails with 502. The first call for an account can take longer overall, because the symbol name is resolved first.\n\n- flags is a bitmask of what changed in the tick: 2 = bid, 4 = ask, 8 = last, 16 = volume, 32 = buy trade, 64 = sell trade.\n\n- time is in epoch seconds. time_msc is the same instant in epoch milliseconds; use it to order or de-duplicate ticks.\n\n- last and volume relate to trades and can be 0 on symbols where the broker publishes only bid and ask.",
        "parameters": [
          {
            "name": "ApiKey",
            "in": "header",
            "required": true,
            "description": "Your API key.",
            "schema": {
              "type": "string"
            },
            "example": "YOUR_API_KEY"
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read. Pass either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "symbol",
            "in": "query",
            "required": true,
            "description": "Symbol to read. A base name like EURUSD is resolved to the broker's exact name (for example EURUSD.s). An exact broker name is used as is. A name the broker does not recognise is passed through unchanged and the call fails with 502.",
            "schema": {
              "type": "string"
            },
            "example": "EURUSD"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window. Accepts epoch seconds, epoch milliseconds or an ISO 8601 date/time. A date/time without a zone is read as UTC. If from is more than 6 hours before to, it is moved forward to 6 hours before to. Default: 15 minutes before to.",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-07T11:45:00"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window. Accepts the same formats as from. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-07T12:00:00"
          }
        ],
        "responses": {
          "200": {
            "description": "An object with these fields: symbol, the broker's exact symbol name; ticks, an array ordered oldest first, where each tick has time (epoch seconds), time_msc (epoch milliseconds), bid, ask, last, volume and flags; from and to, the window actually used, in epoch seconds; and truncated, which is true when the requested window was cut to 6 hours.",
            "content": {
              "application/json": {
                "example": {
                  "symbol": "EURUSD.s",
                  "ticks": [
                    {
                      "time": 1791373512,
                      "time_msc": 1791373512184,
                      "bid": 1.08412,
                      "ask": 1.08425,
                      "last": 0,
                      "volume": 0,
                      "flags": 6
                    },
                    {
                      "time": 1791373512,
                      "time_msc": 1791373512731,
                      "bid": 1.08413,
                      "ask": 1.08425,
                      "last": 0,
                      "volume": 0,
                      "flags": 2
                    },
                    {
                      "time": 1791373514,
                      "time_msc": 1791373514055,
                      "bid": 1.08413,
                      "ask": 1.08426,
                      "last": 0,
                      "volume": 0,
                      "flags": 4
                    }
                  ],
                  "from": 1791373500,
                  "to": 1791374400,
                  "truncated": false
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — ?id= is missing or is not a valid account id (message \"account id (?id=) required\").\n\n`ERROR` — symbol is missing or empty (message \"symbol is required\")."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, the key is unknown, or the key is no longer active. The body is { statusCode: 401, message: \"ApiKey required\", error: \"Unauthorized\" }."
          },
          "502": {
            "description": "`ERROR` — The ticks could not be read. The message gives the reason: the broker has no such symbol (\"unknown symbol: ...\"), the account is not connected or its connection dropped during the call, the broker could not supply ticks for the symbol, to fell before 1970, or the read did not finish within 30 seconds."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OpenedOrders": {
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "opened-orders",
        "summary": "List open positions and pending orders",
        "description": "Returns every open position and every working pending order on the account in one call.\n\nUse this to see everything currently live on the account: filled positions with their floating profit, and pending (limit/stop) orders that have not triggered yet. Positions come back in positionInfos and pending orders in openedOrders. Both arrays are empty, not missing, when there is nothing open. Several fields are deliberately duplicated under two names (for example lots/volume, openPrice/priceOpen, stopLoss/sl, expertId/magic) so you can read whichever naming your client prefers; the values are identical. Call this to reconcile state after a trade call came back with error.errorCode TIMEOUT: the outcome of that request is unknown, so check here whether the order or position exists before you retry. NOT_CONNECTED usually means nothing was sent, but if the connection dropped while the request was in flight the outcome is unknown as well, so checking here is the safe move.\n\n- openTime and timeSetup are plain Unix timestamps in seconds (numbers, not ISO strings), stamped by the broker's trade server. Many brokers run that clock ahead of UTC, so do not treat them as exact UTC.\n\n- symbol is the broker's own symbol name, which may carry a suffix (for example XAUUSD.s) even if you placed the order using the base name XAUUSD.\n\n- Pending orders do not include their expiration time in this response.\n\n- Positions carry no commission field; profit is the floating P/L and swap is reported separately.\n\n- A position's ticket is the value to pass as ticket to OrderModifyTask, OrderCloseTask and OrderCloseByTask. A pending order's ticket is the value to pass to OrderModifyTask, OrderCancelTask or OrderCloseTask.\n\n- If an entry in openedOrders is not one of the six pending types, orderType and type fall back to an upper-case raw value such as BUY, SELL or OTHER, so do not hard-fail on an unexpected value.\n\n- Unlike the trade endpoints, this read endpoint reports an unreachable account with HTTP 502, not with an HTTP 200 error body.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read. Either the session token returned by ConnectEx (\"acct-<login>\") or the bare MT5 login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          }
        ],
        "responses": {
          "200": {
            "description": "An object with two arrays. positionInfos[]: ticket, positionTicket and identifier (all the position ticket); symbol; orderType and type (\"Buy\" or \"Sell\"); lots and volume (open volume in lots); openPrice and priceOpen; priceCurrent and closePrice (the current price the position would close at); openTime and timeSetup (Unix seconds); stopLoss and sl, takeProfit and tp (0 when not set); swap; profit (floating P/L in account currency, swap reported separately); expertId and magic; comment. openedOrders[]: ticket; symbol; orderType and type (\"BuyLimit\", \"SellLimit\", \"BuyStop\", \"SellStop\", \"BuyStopLimit\" or \"SellStopLimit\"); lots, volume and volumeCurrent (all the remaining volume); openPrice and priceOpen (the order price); stopLoss and sl, takeProfit and tp; stopLimitPrice (0 unless a stop-limit order); timeSetup (Unix seconds); expertId and magic; comment.",
            "content": {
              "application/json": {
                "example": {
                  "positionInfos": [
                    {
                      "ticket": 812345678,
                      "positionTicket": 812345678,
                      "identifier": 812345678,
                      "symbol": "EURUSD",
                      "orderType": "Buy",
                      "type": "Buy",
                      "lots": 0.1,
                      "volume": 0.1,
                      "openPrice": 1.0825,
                      "priceOpen": 1.0825,
                      "priceCurrent": 1.08312,
                      "closePrice": 1.08312,
                      "openTime": 1791363600,
                      "timeSetup": 1791363600,
                      "stopLoss": 1.0795,
                      "sl": 1.0795,
                      "takeProfit": 1.0875,
                      "tp": 1.0875,
                      "swap": -0.12,
                      "profit": 6.2,
                      "expertId": 8100003,
                      "magic": 8100003,
                      "comment": "breakout-1"
                    },
                    {
                      "ticket": 812345702,
                      "positionTicket": 812345702,
                      "identifier": 812345702,
                      "symbol": "XAUUSD",
                      "orderType": "Sell",
                      "type": "Sell",
                      "lots": 0.05,
                      "volume": 0.05,
                      "openPrice": 2401.35,
                      "priceOpen": 2401.35,
                      "priceCurrent": 2398.9,
                      "closePrice": 2398.9,
                      "openTime": 1791365400,
                      "timeSetup": 1791365400,
                      "stopLoss": 0,
                      "sl": 0,
                      "takeProfit": 2385,
                      "tp": 2385,
                      "swap": 0,
                      "profit": 12.25,
                      "expertId": 0,
                      "magic": 0,
                      "comment": ""
                    }
                  ],
                  "openedOrders": [
                    {
                      "ticket": 812345730,
                      "symbol": "EURUSD",
                      "orderType": "BuyLimit",
                      "type": "BuyLimit",
                      "lots": 0.2,
                      "volume": 0.2,
                      "volumeCurrent": 0.2,
                      "openPrice": 1.079,
                      "priceOpen": 1.079,
                      "stopLoss": 1.075,
                      "sl": 1.075,
                      "takeProfit": 1.088,
                      "tp": 1.088,
                      "stopLimitPrice": 0,
                      "timeSetup": 1791367200,
                      "expertId": 0,
                      "magic": 0,
                      "comment": "dip-buy"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing or is neither \"acct-<login>\" nor a positive integer login. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing or is not a valid key. This body has no code field; \"Unauthorized\" is its error field. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The positions or orders could not be read: the account is not connected (message \"account <login> is not connected\"), it did not answer in time, or it reported a failure. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OrderSendTask": {
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "order-send",
        "summary": "Place an order",
        "description": "Places a market order or a pending limit, stop or stop-limit order on the account.\n\nSet operation to Buy or Sell for a market order, which is filled at the current ask (Buy) or bid (Sell); any price you pass is ignored for market orders. Set it to a pending type (BuyLimit, SellLimit, BuyStop, SellStop, BuyStopLimit, SellStopLimit) to place a working order, which then requires a positive price. Every trade outcome, including a broker rejection, comes back as HTTP 200, so branch on the body's code field (\"OK\" or \"ERROR\"); only a missing or invalid id (400) or a bad API key (401) changes the HTTP status. Pass a clientOrderId to make retries safe: a repeat of the same id within 5 minutes returns the first result instead of opening a second position. If you get errorCode TIMEOUT or NOT_CONNECTED, the order may or may not be live; check OpenedOrders before sending again.\n\n- A broker rejection is reported as HTTP 200 with body.code \"ERROR\"; only a bad id (400) or bad API key (401) changes the HTTP status.\n\n- error.errorCode is always a string (\"10019\", \"-2\", \"TIMEOUT\"); the numeric form is in error.mqlErrorTradeIntCode, which is omitted entirely for non-numeric codes such as TIMEOUT, NOT_CONNECTED and UNSUPPORTED.\n\n- The error message is the trade result text (for example \"not enough money\"), optionally followed by \": \" and the broker's own comment; return codes without a known text read \"trade error <retcode>\", which shows \"trade error 0\" when the broker returned no return code at all. Treat the message as human-readable only and branch on errorCode.\n\n- A clientOrderId is remembered for 5 minutes from when the first request started. A repeat that arrives while the first request is still running waits for it and receives exactly the same result, whatever that turns out to be; no new order is sent.\n\n- A definite failure (any errorCode other than TIMEOUT, including broker rejections, -2, -3, -100 and NOT_CONNECTED) releases the clientOrderId as soon as it is known, so a later retry with the same id sends a fresh attempt.\n\n- After a TIMEOUT, repeats of the same clientOrderId keep returning that TIMEOUT for the rest of the 5 minutes, even if the order was actually filled; reconcile with OpenedOrders rather than waiting for the replay to change.\n\n- NOT_CONNECTED also releases the clientOrderId, even when the connection dropped mid-request and the order may have been placed, so check OpenedOrders before retrying in that case.\n\n- The de-duplication key is the account plus the clientOrderId only. Reusing an id with different parameters within 5 minutes returns the earlier order's result and does not place the new order.\n\n- clientOrderId is not sent to the broker and does not appear on the order, in OpenedOrders or in history. Use comment or expertID if you need to tag orders.\n\n- De-duplication is not guaranteed to survive a service restart or redeploy, so do not rely on it beyond short-term retries.\n\n- stoploss and takeprofit are absolute prices, not distances in points.\n\n- A non-numeric value for stoploss, takeprofit, slippage, stopLimitPrice or expertID is silently ignored rather than rejected, so a typo can place an order without its stop-loss. A non-numeric price on a pending order is dropped too, which then fails with -2.\n\n- For a stop-limit order, price is the stop (activation) price and stopLimitPrice is the price of the limit order placed after activation.\n\n- expirationType is case-sensitive: \"today\" or \"specified\" in lower case fall through to good-till-cancelled unless expiration is also set.\n\n- expiration is passed to the broker unchanged, and brokers evaluate it against their trade-server clock, which often runs ahead of UTC.\n\n- The fill policy (fill-or-kill, immediate-or-cancel or return) is chosen automatically from what the symbol supports; you cannot set it.\n\n- If expertID is omitted, the order is stamped with magic number 0.\n\n- On a live (real-money) account, a request that passes the id and operation checks returns errorCode -100 unless live trading has been enabled for the service; no order is sent.\n\n- Because this is a GET request, make sure no proxy, prefetcher or HTTP retry layer replays it automatically; send a clientOrderId on every order.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to trade on: \"acct-<login>\" or the bare MT5 login (a positive integer).",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "operation",
            "in": "query",
            "required": true,
            "description": "Order type. Matching is case-insensitive, leading/trailing spaces and underscores are ignored, and a legacy \"Tmt5OrderType\" prefix is stripped, so Buy, BUY, buy_limit, BUY_STOP_LIMIT and Tmt5OrderTypeSellStop are all accepted. Market: Buy, Sell. Pending: BuyLimit, SellLimit, BuyStop, SellStop, BuyStopLimit, SellStopLimit. Anything else (including spaces or hyphens inside the name, such as \"Buy Limit\" or \"buy-limit\") returns errorCode UNSUPPORTED.",
            "schema": {
              "type": "string",
              "enum": [
                "Buy",
                "Sell",
                "BuyLimit",
                "SellLimit",
                "BuyStop",
                "SellStop",
                "BuyStopLimit",
                "SellStopLimit"
              ]
            },
            "example": "Buy"
          },
          {
            "name": "symbol",
            "in": "query",
            "required": true,
            "description": "Symbol to trade. You can send the base name (EURUSD, XAUUSD); it is matched to the broker's own name, including any suffix such as XAUUSD.s. An exact broker name is used as-is.",
            "schema": {
              "type": "string"
            },
            "example": "EURUSD"
          },
          {
            "name": "volume",
            "in": "query",
            "required": true,
            "description": "Order volume in lots. Must be greater than 0; the broker enforces the symbol's minimum, maximum and step.",
            "schema": {
              "type": "number"
            },
            "example": 0.1
          },
          {
            "name": "price",
            "in": "query",
            "required": false,
            "description": "Pending orders only, and required for them (must be > 0). For limit and stop orders it is the order price. For stop-limit orders it is the stop (activation) price. Ignored for market orders.",
            "schema": {
              "type": "number"
            },
            "example": 1.079
          },
          {
            "name": "stopLimitPrice",
            "in": "query",
            "required": false,
            "description": "Stop-limit orders only: the price of the limit order that is placed once price reaches the stop price given in price. Ignored for market orders.",
            "schema": {
              "type": "number"
            },
            "example": 1.0785
          },
          {
            "name": "stoplimit",
            "in": "query",
            "required": false,
            "description": "Alias of stopLimitPrice. If both are sent, stopLimitPrice wins.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "stoploss",
            "in": "query",
            "required": false,
            "description": "Stop-loss as an absolute price (not points). Omit or send 0 for no stop-loss. Applies to market and pending orders.",
            "schema": {
              "type": "number"
            },
            "example": 1.0795
          },
          {
            "name": "takeprofit",
            "in": "query",
            "required": false,
            "description": "Take-profit as an absolute price (not points). Omit or send 0 for no take-profit. Applies to market and pending orders.",
            "schema": {
              "type": "number"
            },
            "example": 1.0875
          },
          {
            "name": "slippage",
            "in": "query",
            "required": false,
            "description": "Maximum price deviation for a market order, in points (the symbol's smallest price increment). Not used for pending orders.",
            "schema": {
              "type": "integer",
              "default": 10
            },
            "example": 20
          },
          {
            "name": "expertID",
            "in": "query",
            "required": false,
            "description": "Magic number stamped on the order and on the resulting position. Shown as expertId/magic in OpenedOrders, so you can tag and later find your strategy's trades.",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "example": 8100003
          },
          {
            "name": "expertId",
            "in": "query",
            "required": false,
            "description": "Alias of expertID. Used only when expertID is absent.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "magic",
            "in": "query",
            "required": false,
            "description": "Alias of expertID. Used only when expertID and expertId are absent.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "comment",
            "in": "query",
            "required": false,
            "description": "Free-text comment stored on the order and shown in OpenedOrders. Empty values are not sent.",
            "schema": {
              "type": "string"
            },
            "example": "breakout-1"
          },
          {
            "name": "expirationType",
            "in": "query",
            "required": false,
            "description": "Pending orders only. Case-sensitive. \"Today\" or \"Day\": the order expires at the end of the trading day (any expiration you also send is ignored). \"Specified\" or \"SpecifiedDay\" together with a valid expiration: the order expires at that time; without one it is good-till-cancelled. Any other value, including \"GTC\" or no value, means good-till-cancelled, unless a valid expiration is also given, in which case the order expires at that time.",
            "schema": {
              "type": "string",
              "enum": [
                "GTC",
                "Today",
                "Day",
                "Specified",
                "SpecifiedDay"
              ],
              "default": "GTC"
            },
            "example": "Specified"
          },
          {
            "name": "expiration",
            "in": "query",
            "required": false,
            "description": "Pending orders only. When the order should expire: Unix seconds, Unix milliseconds (any all-digit value above 100000000000 is treated as milliseconds), or an ISO-8601 datetime. A datetime with no zone, such as 2026-10-08T12:00:00, is read as UTC. An unparseable value is ignored.",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-08T12:00:00"
          },
          {
            "name": "clientOrderId",
            "in": "query",
            "required": false,
            "description": "Your idempotency key, scoped to this account. While the first request with this id is running, and for 5 minutes after it started, any repeat returns that first result without sending another order. Leading and trailing spaces are trimmed; an empty value disables de-duplication.",
            "schema": {
              "type": "string"
            },
            "example": "strat7-20261007-0001"
          },
          {
            "name": "clientOrderID",
            "in": "query",
            "required": false,
            "description": "Alias of clientOrderId. Used only when clientOrderId is absent.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "clientId",
            "in": "query",
            "required": false,
            "description": "Alias of clientOrderId. Used only when clientOrderId and clientOrderID are absent.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success: { ticket, order, deal, volume, price, retcode, code: \"OK\", comment }. ticket is the order ticket (or the deal ticket if no order ticket was returned). order and deal are the order and deal tickets the broker reported; a filled market order carries a deal ticket, a newly placed pending order has deal 0. For a filled market order, volume and price are the executed volume and price. retcode is the broker's trade return code (10008 placed, 10009 done, 10010 partially done). comment is the broker's comment on the operation, not your order comment. Failure, still HTTP 200: { code: \"ERROR\", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }, where mqlErrorTradeIntCode is present only for numeric codes. Example broker rejection: {\"code\":\"ERROR\",\"message\":\"not enough money: No money\",\"error\":{\"errorCode\":\"10019\",\"errorMessage\":\"not enough money: No money\",\"mqlErrorTradeIntCode\":10019,\"mqlErrorTradeCode\":\"10019\"}}. Example bad operation: {\"code\":\"ERROR\",\"message\":\"unsupported operation: Teleport\",\"error\":{\"errorCode\":\"UNSUPPORTED\",\"errorMessage\":\"unsupported operation: Teleport\",\"mqlErrorTradeCode\":\"UNSUPPORTED\"}}.\n\n`10004-10046` — The broker refused the trade. body.code is \"ERROR\"; error.errorCode is the trade return code as a string (e.g. \"10019\" not enough money, \"10014\" invalid volume, \"10016\" invalid stops, \"10018\" market closed) and error.mqlErrorTradeIntCode is the same number. If the broker returned no trade return code, errorCode is instead the trading platform's numeric error code. Nothing was placed.\n\n`UNSUPPORTED` — operation is missing or not one of the accepted values. Nothing was sent.\n\n`-2` — Invalid parameters, for example volume missing or not greater than 0, symbol missing, or a pending order without a positive price (message \"pending order requires a positive price\").\n\n`-3` — The symbol does not exist on this account's broker (message \"unknown symbol: <symbol>\").\n\n`-100` — The account is a real-money (live) account and trading on live accounts is not enabled for the service. Nothing was sent.\n\n`TIMEOUT` — The service did not learn the outcome in time. The order may be live at the broker; check OpenedOrders before retrying.\n\n`NOT_CONNECTED` — The account is not connected, or its connection dropped while the order was in flight. In the second case the order may have reached the broker.\n\n`ERR` — An unclassified failure with no specific code.",
            "content": {
              "application/json": {
                "example": {
                  "ticket": 812345678,
                  "order": 812345678,
                  "deal": 812345690,
                  "volume": 0.1,
                  "price": 1.08318,
                  "retcode": 10009,
                  "code": "OK",
                  "comment": "Request executed"
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing or invalid. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing or not a valid key. Body: {\"statusCode\":401,\"message\":\"ApiKey required\",\"error\":\"Unauthorized\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OrderModifyTask": {
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "order-modify",
        "summary": "Modify a position or pending order",
        "description": "Changes the stop-loss and take-profit of an open position, or the price, stop-loss, take-profit or stop-limit price of a pending order.\n\nPass the ticket of an open position or a working pending order. For a position, only stoploss and takeprofit can change. For a pending order, price, stoploss, takeprofit and stoplimit can change, and its expiration and time-in-force are kept. A field you omit keeps its current value, and sending 0 clears a stop-loss or take-profit. Trade outcomes, including rejections, timeouts and a disconnected account, return HTTP 200 with body.code \"OK\" or \"ERROR\". Only a missing or invalid id (400) or a bad ApiKey (401) uses a non-200 status.\n\n- Do not treat ticket > 0 as the success test for a modify; check body.code === \"OK\" (or a retcode of 10008, 10009 or 10010).\n\n- On failure body.code is always \"ERROR\"; the specific reason is in error.errorCode, which is a string even for numeric codes such as \"10016\" or \"-8\".\n\n- Omitting stoploss keeps the existing stop-loss, but stoploss=0 removes it; the same applies to takeprofit.\n\n- The stop-limit parameter here is named stoplimit only; the stopLimitPrice spelling accepted by OrderSendTask is ignored on this endpoint.\n\n- Volume, expiration and order type cannot be changed here; cancel and re-place the order instead.\n\n- A non-numeric or empty stoploss, takeprofit, price or stoplimit is silently ignored, so that field keeps its current value.\n\n- A fractional ticket is truncated to a whole number before lookup, so ticket=1.5 looks up ticket 1.\n\n- Stop-loss and take-profit are absolute prices, not distances in points.\n\n- On a live (real-money) account without live trading enabled, every call returns error code \"-100\", whatever the other parameters are.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account: \"acct-<login>\" or the bare MT5 login.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "ticket",
            "in": "query",
            "required": true,
            "description": "Ticket of the open position or pending order to change, as listed in OpenedOrders. Positions are matched first.",
            "schema": {
              "type": "integer"
            },
            "example": 812345678
          },
          {
            "name": "stoploss",
            "in": "query",
            "required": false,
            "description": "New stop-loss price. Omit to keep the current one; send 0 to remove it.",
            "schema": {
              "type": "number"
            },
            "example": 1.081
          },
          {
            "name": "takeprofit",
            "in": "query",
            "required": false,
            "description": "New take-profit price. Omit to keep the current one; send 0 to remove it.",
            "schema": {
              "type": "number"
            },
            "example": 1.09
          },
          {
            "name": "price",
            "in": "query",
            "required": false,
            "description": "Pending orders only: new order price (the stop price for a stop-limit order). Omit to keep the current one. Ignored for positions.",
            "schema": {
              "type": "number"
            },
            "example": 1.0785
          },
          {
            "name": "stoplimit",
            "in": "query",
            "required": false,
            "description": "Stop-limit orders only: new limit price. Omit to keep the current one. Ignored for positions.",
            "schema": {
              "type": "number"
            },
            "example": 1.078
          }
        ],
        "responses": {
          "200": {
            "description": "Same trade result shape as OrderSendTask: { ticket, order, deal, volume, price, retcode, code: \"OK\", comment } on success. On failure the body is { code: \"ERROR\", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }. errorCode is a string, and mqlErrorTradeIntCode is present only when the code is numeric. For a modify, deal, volume and price are normally 0, and ticket (order, or else deal, or else 0) can be 0 too.\n\n`10004-10046` — body.code \"ERROR\", error.errorCode is the trade server retcode: the broker refused the change, for example \"10016\" for invalid stops or \"10025\" when the new values equal the current ones. Nothing changed.\n\n`4-digit platform error` — body.code \"ERROR\": the request was refused before it reached the trade server, so no retcode exists. error.errorCode is the platform's runtime error number (for example \"4756\"). Nothing changed.\n\n`-2` — body.code \"ERROR\", error.errorCode \"-2\": ticket is missing, non-numeric, or 0 or less (message \"ticket is required\").\n\n`-8` — body.code \"ERROR\", error.errorCode \"-8\": no open position or pending order has this ticket (message \"ticket not found: <ticket>\").\n\n`-100` — body.code \"ERROR\", error.errorCode \"-100\": the account is a live (real-money) account and live trading is not enabled for the service.\n\n`TIMEOUT` — body.code \"ERROR\", error.errorCode \"TIMEOUT\": the outcome was not learned in time, and the change may or may not have been applied. Read OpenedOrders to see the current values.\n\n`NOT_CONNECTED` — body.code \"ERROR\", error.errorCode \"NOT_CONNECTED\": the account is not connected, or the connection dropped mid-request.",
            "content": {
              "application/json": {
                "example": {
                  "ticket": 0,
                  "order": 0,
                  "deal": 0,
                  "volume": 0,
                  "price": 0,
                  "retcode": 10009,
                  "code": "OK",
                  "comment": "Request executed"
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing or is neither \"acct-<login>\" nor a positive login. Body: { code: \"ERROR\", message: \"account id (?id=) required\" }."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing or not a valid key. Body: { statusCode: 401, message: \"ApiKey required\", error: \"Unauthorized\" }, with no code field."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OrderCloseTask": {
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "order-close",
        "summary": "Close a position",
        "description": "Closes an open position in full or in part at market, or cancels a pending order if you pass a pending-order ticket.\n\nPass a position ticket to close it at the current market price: a Buy position closes at the bid and a Sell position at the ask. Add lots to close only part of the position; leave it out to close all of it. If the ticket belongs to a pending order instead of a position, that order is cancelled, so this one endpoint can close or cancel anything that is open. Once the request is authenticated and carries a valid id, the reply is always HTTP 200: body.code is \"OK\" on success or \"ERROR\" on failure, and the reason is in body.error.errorCode.\n\n- A lots value of 0, a negative value or more than the open volume is not an error: it closes the whole position, so double-check the value before sending a partial close.\n\n- lots is sent to the broker as given (only capped at the open volume). If the broker does not accept that volume, the call fails with a broker retcode such as 10014 and nothing is closed.\n\n- If you pass a pending-order ticket, that order is cancelled and lots and slippage are ignored.\n\n- OrderCloseTask has no clientOrderId de-duplication. After a TIMEOUT, read OpenedOrders before calling again. A retry of a partial close closes another lots of the same position, and a retry of a full close that already went through fails with -8.\n\n- Use positionInfos[].ticket from OpenedOrders as the ticket for a position (the same value is also returned as positionTicket and identifier). For a pending order, use openedOrders[].ticket.\n\n- A failure's code is in body.error.errorCode (a string). body.code is just \"ERROR\" for every failure.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account: \"acct-<login>\" or the bare MT5 login.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "ticket",
            "in": "query",
            "required": true,
            "description": "Position ticket to close (OpenedOrders positionInfos[].ticket), or pending-order ticket to cancel (OpenedOrders openedOrders[].ticket).",
            "schema": {
              "type": "integer"
            },
            "example": 812345678
          },
          {
            "name": "lots",
            "in": "query",
            "required": false,
            "description": "Volume to close, in lots, for a partial close. Leave it out, or send 0, a negative value or more than the open volume, to close the whole position. Ignored when cancelling a pending order.",
            "schema": {
              "type": "number"
            },
            "example": 0.05
          },
          {
            "name": "slippage",
            "in": "query",
            "required": false,
            "description": "Maximum price deviation allowed for the closing trade, in points. Ignored when cancelling a pending order.",
            "schema": {
              "type": "integer",
              "default": 10
            },
            "example": 20
          }
        ],
        "responses": {
          "200": {
            "description": "Same trade-result shape as OrderSendTask. For a close, ticket is the closing order ticket (or the deal ticket if no order ticket was returned), deal is the closing deal, and volume and price are the volume closed and the price it closed at. A pending-order cancellation uses the same shape, but no deal takes place. retcode is 10009 (done), 10010 (partially done) or 10008 (placed) on success. On failure the body is { code: \"ERROR\", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }, still with HTTP 200.\n\n`<broker retcode>` — The broker refused the close or cancel. Examples: \"10018\" market closed, \"10036\" position already closed, \"10014\" invalid volume. The code is in error.errorCode, and as a number in error.mqlErrorTradeIntCode. If the broker gives no retcode, a platform error number is returned instead. Nothing was closed.\n\n`-2` — ticket is missing, not a number, zero or negative (message \"ticket is required\").\n\n`-3` — The position's symbol could not be selected for trading (message \"unknown symbol: <symbol>\").\n\n`-8` — No open position or pending order has this ticket (message \"position or order not found: <ticket>\").\n\n`-100` — The account is a live (real-money) account and live trading is not enabled for the service.\n\n`TIMEOUT` — The outcome was not learned in time. The position may already be closed, so check OpenedOrders before retrying.\n\n`NOT_CONNECTED` — The account is not connected, or the connection dropped mid-request.",
            "content": {
              "application/json": {
                "example": {
                  "ticket": 812345801,
                  "order": 812345801,
                  "deal": 812345812,
                  "volume": 0.1,
                  "price": 1.08305,
                  "retcode": 10009,
                  "code": "OK",
                  "comment": "Request executed"
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing or invalid (message \"account id (?id=) required\")."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing or not a valid key (message \"ApiKey required\")."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OrderCancelTask": {
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "order-cancel",
        "summary": "Cancel a pending order",
        "description": "Cancels (deletes) a working pending order by its ticket.\n\nUse this to remove a limit, stop or stop-limit order that has not triggered yet. It only accepts tickets of pending orders that are still working. To close an open position, use OrderCloseTask. If the order has already triggered, expired or been cancelled, it is no longer a working pending order and the call fails with error.errorCode \"-8\". The HTTP status is 200 whether the cancel succeeds or fails, so check body.code: \"OK\" means the order was removed, and \"ERROR\" means it was not.\n\n- A position ticket is not accepted here and returns errorCode \"-8\". Use OrderCloseTask for positions.\n\n- An order can trigger between your read of OpenedOrders and this call. If you then get \"-8\", the order has probably become a position, so read OpenedOrders again.\n\n- A decimal ticket is truncated to a whole number rather than rejected, so always pass the integer ticket exactly as OpenedOrders returned it.\n\n- Failures still return HTTP 200, so always branch on body.code rather than on the HTTP status.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account: \"acct-<login>\" or the bare MT5 login.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "ticket",
            "in": "query",
            "required": true,
            "description": "Ticket of the pending order to cancel, from OpenedOrders.openedOrders[].ticket.",
            "schema": {
              "type": "integer"
            },
            "example": 812345730
          }
        ],
        "responses": {
          "200": {
            "description": "On success, the same trade result shape as OrderSendTask: { ticket, order, deal, volume, price, retcode, code: \"OK\", comment }. ticket equals order, and deal is 0 for a cancel. On failure: { code: \"ERROR\", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }. errorCode and mqlErrorTradeCode are strings (for example \"-8\" or \"TIMEOUT\"). mqlErrorTradeIntCode is the same code as a number and is present only when the code is numeric.\n\n`10004-10046` — The broker refused to cancel the order. errorCode is usually the broker's trade return code; if the broker gave none, it is the platform's error number instead.\n\n`-2` — ticket is missing, non-numeric, zero or negative (message \"ticket is required\").\n\n`-8` — No working pending order has this ticket. This includes an open position's ticket and an order that already triggered, expired or was cancelled (message \"pending order not found: <ticket>\").\n\n`-100` — The account is a live (real-money) account and live trading is not enabled for the service.\n\n`TIMEOUT` — The outcome was not learned in time, so the order may or may not have been cancelled. Check OpenedOrders before retrying.\n\n`NOT_CONNECTED` — The account is not connected, or the connection dropped mid-request.",
            "content": {
              "application/json": {
                "example": {
                  "ticket": 812345730,
                  "order": 812345730,
                  "deal": 0,
                  "volume": 0,
                  "price": 0,
                  "retcode": 10009,
                  "code": "OK",
                  "comment": "Request executed"
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing or invalid (message \"account id (?id=) required\")."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing or not a valid key. The body is { statusCode: 401, message: \"ApiKey required\", error: \"Unauthorized\" }."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OrderCloseByTask": {
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "order-close-by",
        "summary": "Close a position by an opposite one",
        "description": "Closes a position against an opposite position on the same symbol (MT5 Close By), on hedging accounts only.\n\nOn a hedging account you can hold a Buy and a Sell on the same symbol at the same time. Close By settles one against the other in a single trade operation instead of closing each separately. Pass the position to close as ticket and the opposite position as closeByTicket (or its alias ticketBy). The call always answers HTTP 200 once the account id is valid, with body.code \"OK\" on success or \"ERROR\" on failure. The specific failure reason is in body.error.errorCode.\n\n- Close By only works on hedging accounts; on a netting account the request is rejected and the rejection code is returned in error.errorCode.\n\n- Both tickets must be open positions on the same symbol in opposite directions; pending-order tickets are not accepted.\n\n- The service does not check the two positions first; any mismatch comes back as the broker's rejection code.\n\n- A failed Close By still returns HTTP 200, so always check body.code and do not rely on the HTTP status.\n\n- On a live (real-money) account without live trading enabled, the call returns -100 before the tickets are checked.\n\n- A TIMEOUT does not mean the Close By failed; check OpenedOrders before sending it again.\n\n- Decimal ticket values are truncated to whole numbers, so always send the exact integer ticket.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account: \"acct-<login>\" or the bare MT5 login.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "ticket",
            "in": "query",
            "required": true,
            "description": "Ticket of the position to close.",
            "schema": {
              "type": "integer"
            },
            "example": 812345678
          },
          {
            "name": "closeByTicket",
            "in": "query",
            "required": true,
            "description": "Ticket of the opposite-direction position on the same symbol used to close it. You may send ticketBy instead; one of the two is required.",
            "schema": {
              "type": "integer"
            },
            "example": 812345755
          },
          {
            "name": "ticketBy",
            "in": "query",
            "required": false,
            "description": "Alias of closeByTicket. Used only when closeByTicket is not sent at all; an empty closeByTicket= is not replaced by it.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Same trade result shape as OrderSendTask. On success: { ticket, order, deal, volume, price, retcode, code: \"OK\", comment }, where ticket is order (or deal when order is 0). On failure, still HTTP 200: { code: \"ERROR\", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }. Here errorCode and mqlErrorTradeCode are the failure code as a string. mqlErrorTradeIntCode is the same code as a number, and is present only when the code is numeric. Example failure: {\"code\":\"ERROR\",\"message\":\"invalid request\",\"error\":{\"errorCode\":\"10013\",\"errorMessage\":\"invalid request\",\"mqlErrorTradeIntCode\":10013,\"mqlErrorTradeCode\":\"10013\"}}\n\n`<broker retcode>` — The broker or trading platform refused the Close By, for example on a netting account or when the two positions are not opposite positions on the same symbol. body.code is \"ERROR\", and error.errorCode holds the MT5 trade retcode, or the platform error code when no retcode was given.\n\n`-2` — ticket, or closeByTicket/ticketBy, is missing, non-numeric, zero or negative (body.code \"ERROR\", error.errorCode \"-2\").\n\n`-100` — The account is a live (real-money) account and trading on live accounts is not enabled for the service (body.code \"ERROR\", error.errorCode \"-100\").\n\n`TIMEOUT` — The outcome was not learned in time (body.code \"ERROR\", error.errorCode \"TIMEOUT\"). The Close By may still have executed, so check OpenedOrders before retrying.\n\n`NOT_CONNECTED` — The account is not connected, or its connection dropped mid-request (body.code \"ERROR\", error.errorCode \"NOT_CONNECTED\").\n\n`ERR` — The request could not be delivered to the account (body.code \"ERROR\", error.errorCode \"ERR\").",
            "content": {
              "application/json": {
                "example": {
                  "ticket": 812345820,
                  "order": 812345820,
                  "deal": 812345831,
                  "volume": 0.1,
                  "price": 1.0825,
                  "retcode": 10009,
                  "code": "OK",
                  "comment": "Request executed"
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing or invalid; the body is { \"code\": \"ERROR\", \"message\": \"account id (?id=) required\" }."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing or not a valid key; the body is { \"message\": \"ApiKey required\", \"error\": \"Unauthorized\", \"statusCode\": 401 }."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OrderHistory": {
      "get": {
        "tags": [
          "History & analytics"
        ],
        "operationId": "order-history",
        "summary": "List closed trades",
        "description": "Returns up to 1,000 closed trades from a time window, newest close first.\n\nReturns the account's closed trades within a time window, defaulting to the last 90 days. You get one row per position, newest close first. Each row merges the position's opening and closing deals into one trade, with volume-weighted open and close prices, open and close times, closed volume, and net profit after swap and commission. A trade is in the window when its last closing deal is inside it, so a position opened before `from` but closed inside the window is included. The response holds at most the 1,000 most recent trades. Use OrderHistoryPagination if a window can hold more than that or you need a different sort order.\n\n- orderHistory and orders always hold the same rows, so read whichever key your code expects.\n\n- The response stops at the 1,000 most recent trades and does not tell you when older trades were left out. Use OrderHistoryPagination for complete exports.\n\n- sort, ascending, pageNumber and ordersPerPage are ignored here. Rows always come newest close first.\n\n- ticket, positionTicket and orderTicket all hold the position id, not an order ticket. Pass it to HistoryDealsByPositionId to see the individual deals and their real order tickets.\n\n- stopLoss and takeProfit are always 0 because the history does not keep them, and state is always \"filled\".\n\n- profit is net of swap and commission and grossProfit is before them. Both are in the account's deposit currency and rounded to 2 decimal places.\n\n- openPrice and closePrice are volume-weighted averages and are not rounded, so they can carry floating-point digits. Round them to the symbol's digits before you display them.\n\n- Volumes (lots, volume, closeLots) are rounded to 2 decimal places.\n\n- The search starts two days before from, so a trade opened earlier than that comes back without its opening leg. openPrice is then 0, openTime is the time of the first closing deal, the opening commission is missing from profit, and orderType shows the side of the closing deal, which is the opposite of the trade's direction. Move from back to cover the open if you need these fields.\n\n- A position closed in several parts is a single row: lots is the total volume closed so far, closePrice is the volume-weighted average exit and closeTime is the last exit. A position that is still partly open already appears with the volume closed so far.\n\n- magic, magicNumber and expertId are the first non-zero magic number found on the position's deals, or 0.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, inclusive. A date-time without a zone suffix is read as UTC. If the value cannot be parsed it is ignored and the default is used. Default: 90 days before now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01T00:00:00"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, inclusive. If you leave it out, trades stamped up to two days ahead of the current UTC time are still included, so a trade that just closed on a broker clock running ahead of UTC is not cut off. A date with no time (2026-09-30) means midnight at the start of that day. If you send a value that cannot be parsed, the window ends at the current time without the two-day allowance. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-30T23:59:59"
          }
        ],
        "responses": {
          "200": {
            "description": "An object with two keys, orderHistory and orders, that hold the same array of closed-trade rows. ticket, positionTicket and orderTicket all hold the position id. orderType and type are \"Buy\" or \"Sell\". lots, volume and closeLots are the total closed volume. openPrice/priceOpen and closePrice/priceClose/priceCurrent are volume-weighted average entry and exit prices. openTime/positionOpenTime is the first entry and closeTime/positionCloseTime the last exit, both written as YYYY-MM-DDTHH:mm:ss with no zone. profit is net (gross + swap + commission) and grossProfit is before swap and commission. magic, magicNumber and expertId hold the position's magic number. accountLogin is the numeric login. stopLoss and takeProfit are always 0, and state is always \"filled\".",
            "content": {
              "application/json": {
                "example": {
                  "orderHistory": [
                    {
                      "ticket": 512340871,
                      "positionTicket": 512340871,
                      "orderTicket": 512340871,
                      "symbol": "EURUSD",
                      "orderType": "Buy",
                      "type": "Buy",
                      "lots": 0.1,
                      "volume": 0.1,
                      "closeLots": 0.1,
                      "openPrice": 1.08425,
                      "priceOpen": 1.08425,
                      "closePrice": 1.08712,
                      "priceClose": 1.08712,
                      "priceCurrent": 1.08712,
                      "openTime": "2026-09-14T09:31:07",
                      "positionOpenTime": "2026-09-14T09:31:07",
                      "closeTime": "2026-09-15T14:02:45",
                      "positionCloseTime": "2026-09-15T14:02:45",
                      "stopLoss": 0,
                      "takeProfit": 0,
                      "profit": 27.58,
                      "grossProfit": 28.7,
                      "swap": -0.42,
                      "commission": -0.7,
                      "magic": 0,
                      "magicNumber": 0,
                      "expertId": 0,
                      "accountLogin": 700100200,
                      "state": "filled"
                    }
                  ],
                  "orders": [
                    {
                      "ticket": 512340871,
                      "positionTicket": 512340871,
                      "orderTicket": 512340871,
                      "symbol": "EURUSD",
                      "orderType": "Buy",
                      "type": "Buy",
                      "lots": 0.1,
                      "volume": 0.1,
                      "closeLots": 0.1,
                      "openPrice": 1.08425,
                      "priceOpen": 1.08425,
                      "closePrice": 1.08712,
                      "priceClose": 1.08712,
                      "priceCurrent": 1.08712,
                      "openTime": "2026-09-14T09:31:07",
                      "positionOpenTime": "2026-09-14T09:31:07",
                      "closeTime": "2026-09-15T14:02:45",
                      "positionCloseTime": "2026-09-15T14:02:45",
                      "stopLoss": 0,
                      "takeProfit": 0,
                      "profit": 27.58,
                      "grossProfit": 28.7,
                      "swap": -0.42,
                      "commission": -0.7,
                      "magic": 0,
                      "magicNumber": 0,
                      "expertId": 0,
                      "accountLogin": 700100200,
                      "state": "filled"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, or is not a valid, active key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account <login> is not connected\"), did not answer within 30 seconds, or reported a failure. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/OrderHistoryPagination": {
      "get": {
        "tags": [
          "History & analytics"
        ],
        "operationId": "order-history-pagination",
        "summary": "Page through closed trades",
        "description": "Returns closed trades from a time window one page at a time, sorted by close or open time.\n\nReturns the same closed-trade rows as OrderHistory, split into zero-based pages. You choose the page size and whether to sort by close or open time, ascending or descending. There is no 1,000-row cap, so use this endpoint to export a whole window. The response gives the number of pages but no total row count. Each page request rebuilds the window from scratch, so pin both from and to when you need pages that stay consistent while trades are still closing.\n\n- A window with no trades returns pagesCount 1 and an empty orders array, not 0 pages.\n\n- Asking for a page past the last one returns an empty orders array, not an error.\n\n- There is no total-count field. The total is at most pagesCount × itemsPerPage, and you know the exact count once you have read the last page.\n\n- sort is case-sensitive: closetime or CLOSETIME silently sorts by open time.\n\n- ascending only recognises the word true. Values such as 1 or yes give newest-first order.\n\n- If you send ordersPerPage with an empty value (ordersPerPage=), it is read as 0 and clamped to a page size of 1. Either give a number or leave the parameter out.\n\n- Each page is rebuilt from the account history when you request it. If a trade closes between two page requests, rows can shift between pages unless both from and to are fixed.\n\n- openTime and closeTime are the broker's server time with no zone suffix, and from/to are compared directly against them, so give your window boundaries on that same clock rather than converting them to true UTC.\n\n- Do not send a to value you are unsure parses. An unparseable to behaves like now but drops the two-day allowance, so trades closed in the last few hours can disappear.\n\n- A position that is only partly closed already appears as a row, with lots, volume and closeLots giving the volume closed so far. All closing fills of a position are merged into one row stamped with the latest close time.\n\n- Rows carry the same caveats as OrderHistory: stopLoss and takeProfit are always 0, and prices are unrounded volume-weighted averages.\n\n- If a trade's opening fill is more than two days before from, the row comes back without its opening leg: openPrice is 0, orderType is inverted, and openTime shows the first closing fill's time.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, inclusive, matched against each trade's close time. The comparison uses the trade timestamps as they appear in the rows (the broker's server time, shown without a zone suffix), so a date-time without a zone suffix is compared as written. If the value is empty or cannot be parsed, the default is used. Default: 90 days before now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01T00:00:00"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, inclusive, matched against each trade's close time on the same clock as from. If you leave it out, trades stamped up to two days ahead of the current UTC time are still included. If you send a value that cannot be parsed, it is treated as now without that two-day allowance. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-30T23:59:59"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort key. Only the exact, case-sensitive value CloseTime sorts by close time. Any other value sorts by open time.",
            "schema": {
              "type": "string",
              "enum": [
                "CloseTime",
                "OpenTime"
              ],
              "default": "CloseTime"
            },
            "example": "CloseTime"
          },
          {
            "name": "ascending",
            "in": "query",
            "required": false,
            "description": "Pass true (any letter case) for oldest first. Any other value, or leaving it out, gives newest first.",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": false
          },
          {
            "name": "pageNumber",
            "in": "query",
            "required": false,
            "description": "Zero-based page index. Fractions are rounded down, negative values are treated as 0, and a non-numeric value gives page 0.",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "example": 0
          },
          {
            "name": "ordersPerPage",
            "in": "query",
            "required": false,
            "description": "Page size. Fractions are rounded down, values are clamped to the range 1-1000, and a non-numeric value gives the default of 100.",
            "schema": {
              "type": "integer",
              "default": 100
            },
            "example": 100
          }
        ],
        "responses": {
          "200": {
            "description": "pagesCount is the number of pages for this page size and is at least 1. pageNumber is the page served. itemsPerPage is the page size actually used after clamping. orders and orderHistory hold the same array of closed-trade rows (see OrderHistory for the row fields). The example shows the second page (pageNumber=1) of a three-trade window with ordersPerPage=2.",
            "content": {
              "application/json": {
                "example": {
                  "pagesCount": 2,
                  "pageNumber": 1,
                  "itemsPerPage": 2,
                  "orders": [
                    {
                      "ticket": 512339120,
                      "positionTicket": 512339120,
                      "orderTicket": 512339120,
                      "symbol": "XAUUSD",
                      "orderType": "Sell",
                      "type": "Sell",
                      "lots": 0.05,
                      "volume": 0.05,
                      "closeLots": 0.05,
                      "openPrice": 2412.37,
                      "priceOpen": 2412.37,
                      "closePrice": 2398.91,
                      "priceClose": 2398.91,
                      "priceCurrent": 2398.91,
                      "openTime": "2026-09-15T15:20:11",
                      "positionOpenTime": "2026-09-15T15:20:11",
                      "closeTime": "2026-09-16T10:05:33",
                      "positionCloseTime": "2026-09-16T10:05:33",
                      "stopLoss": 0,
                      "takeProfit": 0,
                      "profit": 66.8,
                      "grossProfit": 67.3,
                      "swap": 0,
                      "commission": -0.5,
                      "magic": 0,
                      "magicNumber": 0,
                      "expertId": 0,
                      "accountLogin": 700100200,
                      "state": "filled"
                    }
                  ],
                  "orderHistory": [
                    {
                      "ticket": 512339120,
                      "positionTicket": 512339120,
                      "orderTicket": 512339120,
                      "symbol": "XAUUSD",
                      "orderType": "Sell",
                      "type": "Sell",
                      "lots": 0.05,
                      "volume": 0.05,
                      "closeLots": 0.05,
                      "openPrice": 2412.37,
                      "priceOpen": 2412.37,
                      "closePrice": 2398.91,
                      "priceClose": 2398.91,
                      "priceCurrent": 2398.91,
                      "openTime": "2026-09-15T15:20:11",
                      "positionOpenTime": "2026-09-15T15:20:11",
                      "closeTime": "2026-09-16T10:05:33",
                      "positionCloseTime": "2026-09-16T10:05:33",
                      "stopLoss": 0,
                      "takeProfit": 0,
                      "profit": 66.8,
                      "grossProfit": 67.3,
                      "swap": 0,
                      "commission": -0.5,
                      "magic": 0,
                      "magicNumber": 0,
                      "expertId": 0,
                      "accountLogin": 700100200,
                      "state": "filled"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, or is not a valid, active key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account <login> is not connected\"), its history did not arrive within 30 seconds, or the account reported a failure. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/TradeStats": {
      "get": {
        "tags": [
          "History & analytics"
        ],
        "operationId": "trade-stats",
        "summary": "Get trading statistics",
        "description": "Summarises the closed trades in a time window: counts, volume, profit totals, averages and the best and worst trade.\n\nCalculates performance figures from the account's closed trades in a time window (default: the last 90 days). The trades are built and selected exactly as in OrderHistory, but all of them are used, not only the 1,000 most recent. Every money figure is net of swap and commission, in the account's deposit currency, and rounded to 2 decimal places. Use it for dashboards and account summaries. For win rate or similar ratios, divide the counts it returns yourself.\n\n- Despite their names, longsWon.all and shortsWon.all count every Buy and every Sell trade, winners and losers alike.\n\n- Despite the name averageUsd, no currency conversion is done: both averages are in the account's deposit currency.\n\n- averageLost.averageUsd is a positive number (the size of the average loss), while worstTrade.profit keeps its sign.\n\n- A trade whose net profit is exactly 0 counts toward trades but toward neither wonTrades nor lostTrades.\n\n- With no trades in the window, bestTrade and worstTrade are {\"profit\": 0, \"date\": null}. If every trade lost, bestTrade is the smallest loss and is negative.\n\n- Trade times are recorded on the trading server's clock, which is often a few hours ahead of UTC. bestTrade.date and worstTrade.date are that server time, written without a zone suffix. from, to and the rolling periods are compared against those times, so trades near a window edge can fall on a different side than a pure-UTC calculation would put them.\n\n- dayProfit, weekProfit and monthProfit are rolling 24-hour, 7-day and 30-day periods ending now, not calendar periods, and only count trades already inside the window. If to is in the past, or from is less than 30 days ago, they do not cover the full period.\n\n- There are no win-rate, profit-factor or drawdown fields. For win rate, use profitability.wonTrades / trades.\n\n- If a trade was opened roughly two days or more before from, its opening fill can be missing from the calculation. The trade is then counted on the opposite side in longsWon and shortsWon, and its opening commission is left out of the totals. Start the window earlier if that matters.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, inclusive, matched against each trade's close time. A date-time without a zone suffix is read as UTC. If the value cannot be parsed it is ignored and the default is used. Default: 90 days before now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01T00:00:00"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, inclusive. If you leave it out, trades stamped up to two days ahead of the current UTC time are still included. If you send a value that cannot be parsed, now is used, but without that two-day allowance. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-30T23:59:59"
          }
        ],
        "responses": {
          "200": {
            "description": "trades: number of closed trades in the window. lots: their total closed volume. summary.totalProfit: sum of net profit. summary.dayProfit, weekProfit and monthProfit: net profit of the window's trades that closed within the last 24 hours, 7 days and 30 days. profitability.wonTrades and lostTrades: trades with net profit above and below zero. averageWin.averageUsd: mean net profit of winning trades. averageLost.averageUsd: mean net loss of losing trades, as a positive number. bestTrade and worstTrade: the highest and lowest net profit, each with the close time as date. longsWon.all and shortsWon.all: number of Buy and Sell trades. The example covers three trades, calculated at 2026-09-16T12:00:00 UTC.",
            "content": {
              "application/json": {
                "example": {
                  "trades": 3,
                  "lots": 0.35,
                  "summary": {
                    "totalProfit": 67.98,
                    "dayProfit": 1.18,
                    "weekProfit": 67.98,
                    "monthProfit": 67.98
                  },
                  "profitability": {
                    "wonTrades": 2,
                    "lostTrades": 1
                  },
                  "averageWin": {
                    "averageUsd": 47.19
                  },
                  "averageLost": {
                    "averageUsd": 26.4
                  },
                  "bestTrade": {
                    "profit": 66.8,
                    "date": "2026-09-13T10:05:33"
                  },
                  "worstTrade": {
                    "profit": -26.4,
                    "date": "2026-09-16T11:48:02"
                  },
                  "longsWon": {
                    "all": 1
                  },
                  "shortsWon": {
                    "all": 2
                  }
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, or is not a valid, active key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account <login> is not connected\"), did not answer within 30 seconds, or reported a failure. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/TradeStatsEquityHistory": {
      "get": {
        "tags": [
          "History & analytics"
        ],
        "operationId": "trade-stats-equity-history",
        "summary": "Get the balance curve (wrapped)",
        "description": "Returns the account's running balance and realised-P/L curve for a time window, wrapped in a charts object.\n\nReturns the same points as EquityHistory, wrapped under a charts key. The curve is rebuilt from the account's deal history and sorted oldest first. You get a point for every closing fill, every deposit, withdrawal or other balance operation, and every opening fill that carried a commission or other charge. Opening fills with no charge are skipped. Each point carries the running balance and the running realised P/L (trading results only) at that moment. Use it to draw a balance or realised-P/L chart for a period. It covers closed results and account operations only, so floating P/L on open positions is not included.\n\n- The only difference from EquityHistory is the {\"charts\": [...]} wrapper. The points are identical.\n\n- balance starts at 0 two days before from and adds up only the deals since then. It matches the real account balance only when the window starts before the account's first deposit. With the 90-day default it is usually an offset curve.\n\n- If the window runs up to now, you can anchor the curve by shifting every point by AccountSummary.balance minus the last point's balance. This is exact only when no credit operations fall in the window, because credit adjustments are added to balance here.\n\n- realizedPL counts trading results only (profit, swap and commission on fills). Deposits, withdrawals and other balance operations move balance but not realizedPL.\n\n- Point times are on the broker's trade-server clock, not UTC, and from/to are compared directly with them. For window edges that line up with the times you see, pass bare date-times on that same clock.\n\n- A closing fill that nets exactly zero still produces a point, with an unchanged balance.\n\n- GET /v1/TradeUnrealizedPL exists only for compatibility. It needs no id and always returns {\"data\": []}. Read floating P/L from the profit fields of AccountSummary and OpenedOrders instead.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, inclusive. It is compared with each point's time. A date-time without a zone suffix is read as UTC. A value that cannot be parsed is ignored and the default is used. Default: 90 days before now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-13T00:00:00"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, inclusive. If you leave it out, points stamped up to two days after the current UTC time are still included. If you pass a value that cannot be parsed, the window ends at the current time with no allowance after it. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-30T23:59:59"
          }
        ],
        "responses": {
          "200": {
            "description": "{ charts: [...] }, where each element is { time, balance, realizedPL }. time has the form YYYY-MM-DDTHH:mm:ss with no zone, on the broker's trade-server clock. balance and realizedPL are running totals rounded to 2 decimal places. The example uses from=2026-09-13T00:00:00 on an account funded with 10,000 on 2026-09-01. That deposit is more than two days before from, so it is not in the running total. The first point still includes the commission of an opening fill on 2026-09-12, which falls inside the two-day lead-in.",
            "content": {
              "application/json": {
                "example": {
                  "charts": [
                    {
                      "time": "2026-09-13T10:05:33",
                      "balance": 66.8,
                      "realizedPL": 66.8
                    },
                    {
                      "time": "2026-09-14T09:31:07",
                      "balance": 66.45,
                      "realizedPL": 66.45
                    },
                    {
                      "time": "2026-09-15T14:02:45",
                      "balance": 94.38,
                      "realizedPL": 94.38
                    },
                    {
                      "time": "2026-09-16T07:12:40",
                      "balance": 93.68,
                      "realizedPL": 93.68
                    },
                    {
                      "time": "2026-09-16T11:48:02",
                      "balance": 67.98,
                      "realizedPL": 67.98
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, or is not a valid, active key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account <login> is not connected\"), did not answer within 30 seconds, or reported a failure. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/EquityHistory": {
      "get": {
        "tags": [
          "History & analytics"
        ],
        "operationId": "equity-history",
        "summary": "Get the balance curve",
        "description": "Returns the account's running balance and realised P/L as an array of points, one per deal that moves the account, for a time window.\n\nReplays the account's deals in time order and returns a point for each one: trade exits, deposits, withdrawals, credit and other cash operations. Opening fills are included only if they charged a fee (commission or swap). Each point holds the running balance and running realised P/L at that moment, oldest first. The window only decides which points are returned. Running totals carry on across the start of the window, so the first point continues the curve rather than restarting at zero. Despite the name, this is a balance curve: floating P/L on open positions is not included.\n\n- balance and realizedPL both start at 0 two days before from, not at the account's opening balance. They track the real account only if that point is before the account's first deposit, so for older accounts the default 90-day window gives a curve shifted by a constant.\n\n- To anchor the curve when you leave out to, add AccountSummary.balance minus the last point's balance to every point. This does not work when you set to, because the last point then is not the latest balance.\n\n- Credit operations and other non-trade cash movements move balance just like deposits and withdrawals, while only trading deals move realizedPL. AccountSummary.balance does not include credit, so on an account that has received credit the curve sits above it by the credit total.\n\n- Point times are the broker's server time, not UTC, and from and to are compared against those times directly. Expect an offset of a few hours from UTC on most brokers.\n\n- An opening fill with no commission or swap produces no point. One that charges commission produces a small step down. A break-even exit still produces a point even though balance does not change.\n\n- There is one point per deal and no down-sampling, so several points can share a timestamp, and long windows on active accounts return large arrays.\n\n- Points are selected by deal time, not by trade close time, so this window filter is not the same as OrderHistory's.\n\n- This endpoint returns a bare array. TradeStatsEquityHistory returns the same points wrapped as {\"charts\": [...]}.\n\n- GET /v1/TradeUnrealizedPL always returns {\"data\": []}. For floating P/L, read the profit fields of AccountSummary and OpenedOrders.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, inclusive, compared against each point's time. A date-time without a zone suffix is read as UTC. Numbers above 100000000000 are treated as milliseconds. If the value cannot be parsed it is ignored and the default is used. Default: 90 days before now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-08-25T00:00:00"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, inclusive. If you leave it out, points stamped up to two days after the current UTC time are still returned, so recent deals on brokers whose server clock runs ahead of UTC are not lost. If you supply it, points after it are dropped exactly. A value that cannot be parsed is treated as now, without the two-day allowance. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-30T23:59:59"
          }
        ],
        "responses": {
          "200": {
            "description": "A JSON array of { time, balance, realizedPL }, oldest first. time is YYYY-MM-DDTHH:mm:ss on the broker's server clock, with no zone suffix. balance is the running total of every deal replayed (trades, deposits, withdrawals, credit and other cash operations). realizedPL is the running total of trading deals only (profit + swap + commission). Both are numbers rounded to 2 decimal places. In this example the window starts before the account's first deposit and there is no credit, so balance follows the account's real balance.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "time": "2026-09-01T08:00:00",
                    "balance": 10000,
                    "realizedPL": 0
                  },
                  {
                    "time": "2026-09-12T15:20:11",
                    "balance": 9999.75,
                    "realizedPL": -0.25
                  },
                  {
                    "time": "2026-09-13T10:05:33",
                    "balance": 10066.8,
                    "realizedPL": 66.8
                  },
                  {
                    "time": "2026-09-14T09:31:07",
                    "balance": 10066.45,
                    "realizedPL": 66.45
                  },
                  {
                    "time": "2026-09-15T14:02:45",
                    "balance": 10094.38,
                    "realizedPL": 94.38
                  },
                  {
                    "time": "2026-09-16T07:12:40",
                    "balance": 10093.68,
                    "realizedPL": 93.68
                  },
                  {
                    "time": "2026-09-16T11:48:02",
                    "balance": 10067.98,
                    "realizedPL": 67.98
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, or is not a valid, active key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account <login> is not connected\"), did not answer within 30 seconds, or reported a failure while reading its history. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/HistoryDealsByPositionId": {
      "get": {
        "tags": [
          "History & analytics"
        ],
        "operationId": "history-deals-by-position-id",
        "summary": "List the deals of a position",
        "description": "Returns every deal (fill) recorded for one position id, as the account records them.\n\nReturns all deals that belong to a single position: the entry, any partial exits and the final exit, each with its own price, volume, commission, swap and profit. Use it to drill into a closed-trade row (its ticket is the position id) or to check how an open position was filled. It covers the whole life of the position and takes no time window. Rows come back as the account records them: times are epoch seconds rather than date strings, type and entry are upper-case codes, and profit is per deal and gross.\n\n- time is an integer number of epoch seconds on the broker's trade-server clock, not the YYYY-MM-DDTHH:mm:ss string the closed-trade endpoints use. Format it as UTC and you get the same wall-clock value those endpoints show.\n\n- A closing deal's type is the opposite of the position's direction: a Buy position is opened by a BUY deal and closed by a SELL deal. Read the position's direction from the IN deal.\n\n- profit on each deal is gross. The trade's net result is the sum of profit + swap + commission over all its deals. When the closed-trade row covers all of the position's deals, this matches that row's profit, which is rounded to 2 decimals.\n\n- The order field on each deal is the real order ticket that produced it. Closed-trade rows from OrderHistory put the position id in orderTicket, ticket and positionTicket, so use this endpoint when you need the actual order tickets.\n\n- The field names here (single lower-case words) differ from the camelCase rows of OrderHistory, so do not reuse the same parser for both.\n\n- The API does not re-sort the deals. Sort by time on your side if your code depends on the order.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "positionId",
            "in": "query",
            "required": true,
            "description": "The position id: the ticket of a closed-trade row from OrderHistory, or of an open position. Required unless you send ticket instead.",
            "schema": {
              "type": "integer"
            },
            "example": 512340871
          },
          {
            "name": "ticket",
            "in": "query",
            "required": false,
            "description": "Alias for positionId, read only when positionId is absent. If positionId is present but invalid, ticket is ignored.",
            "schema": {
              "type": "integer"
            },
            "example": 512340871
          }
        ],
        "responses": {
          "200": {
            "description": "A JSON array of deals. Each has ticket (deal ticket), order (the order that produced the deal), position, time (epoch seconds), type (BUY, SELL, BALANCE, CREDIT or OTHER), entry (IN, OUT, INOUT, OUT_BY or OTHER), symbol, volume, price, commission, swap, profit (gross, for this deal only), magic and comment.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "ticket": 498177302,
                    "order": 512340871,
                    "position": 512340871,
                    "time": 1789378267,
                    "type": "BUY",
                    "entry": "IN",
                    "symbol": "EURUSD",
                    "volume": 0.1,
                    "price": 1.08425,
                    "commission": -0.35,
                    "swap": 0,
                    "profit": 0,
                    "magic": 0,
                    "comment": ""
                  },
                  {
                    "ticket": 498190846,
                    "order": 512361577,
                    "position": 512340871,
                    "time": 1789480965,
                    "type": "SELL",
                    "entry": "OUT",
                    "symbol": "EURUSD",
                    "volume": 0.1,
                    "price": 1.08712,
                    "commission": -0.35,
                    "swap": -0.42,
                    "profit": 28.7,
                    "magic": 0,
                    "comment": ""
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`ERROR` — positionId is not a positive integer, or, when positionId is absent, ticket is missing or not a positive integer. Body: {\"code\":\"ERROR\",\"message\":\"positionId (or ticket) required\"}. This check runs before the id check.\n\n`ERROR` — The id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, or is not a valid, active key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account <login> is not connected\"), did not answer within 30 seconds, or could not load the position's history. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/v1/PendingOrderHistory": {
      "get": {
        "tags": [
          "History & analytics"
        ],
        "operationId": "pending-order-history",
        "summary": "List past pending orders",
        "description": "Returns the limit, stop and stop-limit orders in the account's order history within a time window.\n\nLists the pending orders in the account's order history together with the state each one was recorded in. Market buy and sell orders are left out. An order is in the window when its completion time (time_done, or time_setup if it has no completion time) is inside it; the default window is the last 90 days. Rows come back as the account records them: snake_case field names, epoch-second times and upper-case type codes. For pending orders that are still working, use OpenedOrders.\n\n- type is one of BUY_LIMIT, SELL_LIMIT, BUY_STOP, SELL_STOP, BUY_STOP_LIMIT or SELL_STOP_LIMIT. Any other non-market order kind, such as a close-by order, shows as OTHER.\n\n- state is one of started, placed, canceled, partial, filled, rejected, expired, request_add, request_modify, request_cancel or unknown. canceled is spelled with one l.\n\n- These rows use snake_case names (price_open, time_done) and the values are not reformatted, so they look different from the OpenedOrders rows (priceOpen, timeSetup) for the same order.\n\n- symbol is the account's own symbol name and may include a broker suffix (for example XAUUSD.s).\n\n- time_setup and time_done are integer epoch seconds on the broker's trade-server clock. Format them as UTC to get the same wall-clock values the other history endpoints show.\n\n- The default for from is 90 days before the current time, not 90 days before to. If you send only a to that is more than 90 days in the past, the window is empty and you get [], so always send from together with to.\n\n- volume_initial is the volume originally requested. There is no filled-volume field.\n\n- price_stoplimit is 0 unless the order is a stop-limit order.\n\n- The response is not paginated and has no symbol filter, and the API does not re-sort the rows. Filter and sort on your side.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.",
            "schema": {
              "type": "string"
            },
            "example": "acct-700100200"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, inclusive, matched against time_done (or time_setup). A date-time without a zone suffix is read as UTC. If the value cannot be parsed it is ignored and the default is used. Default: 90 days before now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01T00:00:00"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, inclusive. If you leave it out, orders stamped up to two days ahead of the current UTC time are still included. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-30T23:59:59"
          }
        ],
        "responses": {
          "200": {
            "description": "A JSON array of orders. Each has ticket, symbol, type, state, volume_initial (requested volume), price_open (entry price), sl, tp, price_stoplimit, time_setup and time_done (epoch seconds), magic and comment.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "ticket": 512355018,
                    "symbol": "EURUSD",
                    "type": "BUY_LIMIT",
                    "state": "filled",
                    "volume_initial": 0.1,
                    "price_open": 1.0825,
                    "sl": 1.078,
                    "tp": 1.0905,
                    "price_stoplimit": 0,
                    "time_setup": 1789027200,
                    "time_done": 1789134130,
                    "magic": 0,
                    "comment": ""
                  },
                  {
                    "ticket": 512357730,
                    "symbol": "XAUUSD",
                    "type": "SELL_STOP",
                    "state": "canceled",
                    "volume_initial": 0.05,
                    "price_open": 2385.5,
                    "sl": 2401,
                    "tp": 2350,
                    "price_stoplimit": 0,
                    "time_setup": 1789203600,
                    "time_done": 1789489800,
                    "magic": 20260912,
                    "comment": ""
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`ERROR` — The id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {\"code\":\"ERROR\",\"message\":\"account id (?id=) required\"}."
          },
          "401": {
            "description": "`Unauthorized` — The ApiKey header is missing, or is not a valid, active key. Body: {\"message\":\"ApiKey required\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "502": {
            "description": "`ERROR` — The account has no live session (message \"account <login> is not connected\"), did not answer within 30 seconds, or could not load its order history. Body: {\"code\":\"ERROR\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/orchestrator/connect": {
      "post": {
        "tags": [
          "Copy-trade & system"
        ],
        "operationId": "orchestrator-connect",
        "summary": "Connect an account",
        "description": "Start a managed session for an MT5 account and wait until it is signed in to the broker.\n\nBrings an MT5 account online and returns only once it is signed in to the broker's trade server and ready for API calls. If the account is already online, the call returns straight away with alreadyConnected: true and the running session is not restarted, although the idleHours, server and symbol you send replace the ones on record for that account. Starting a new session usually takes tens of seconds and can take up to about two minutes, so set your HTTP client timeout to match. After a successful connect the service keeps the account online and restarts the session automatically if it drops. That continues until you call POST /orchestrator/disconnect, the account's idle window runs out, or automatic recovery gives up. Use the returned id (acct-<login>) or the bare login as ?id= on /v1 calls.\n\n- This endpoint requires the service session token in an Authorization: Bearer header. Your ApiKey and portal tokens are rejected here with 401, so use GET /v1/ConnectEx if you authenticate with an ApiKey.\n\n- Set your HTTP client timeout well above two minutes. A new session can take up to about two minutes to come online after it gets its turn to start. Sessions start one at a time across the service, so time spent waiting for that turn is added on top.\n\n- Concurrent connect requests for the same login are merged into a single attempt, and every caller gets the same outcome.\n\n- A 200 with alreadyConnected: true confirms the account is online. The password is compared only when the service has a verified record for that session, so do not use a repeat connect as a password check.\n\n- If the account is already online and you send a different password, you get 409 CREDENTIALS_MISMATCH and the running session is not touched. Call POST /orchestrator/disconnect, then connect again with the new password.\n\n- A repeat connect for an account that is already online replaces its idle window. If you leave idleHours out, the account goes back to the service default, so send the same idleHours every time you connect.\n\n- Send numbers as JSON numbers. Unknown body fields are silently ignored, not rejected, so a typo in an optional field name such as idleHours fails quietly.\n\n- When idleHours (or the service default) runs out with no API activity on the account, its session is stopped. Calls for that account then fail as not connected until you connect again. The idle check runs about once a minute, so the stop happens shortly after the window elapses, not to the second.\n\n- If automatic recovery of a dropped account fails five times in a row, the service stops trying and the account must be connected again with this call.\n\n- The same operation is available with ApiKey authentication as GET /v1/ConnectEx. That variant returns the id as plain text. On failure it returns {code, message} with the HTTP status set by errorReplyStatusCode (default 405), keeping the codes above and using CONNECT_FAILED for any other failure.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "integer",
                    "description": "The MT5 account login (account number). Send it as a JSON number. A quoted string such as \"700100200\" is rejected with 400."
                  },
                  "password": {
                    "type": "string",
                    "description": "The MT5 account password, either the master or the investor (read-only) password. It is used to sign in to the broker, is never written to logs and is not kept in plain text afterwards. Use the master password if you intend to trade."
                  },
                  "server": {
                    "type": "string",
                    "description": "The broker's MT5 trade server name, exactly as MetaTrader 5 shows it."
                  },
                  "idleHours": {
                    "type": "number",
                    "description": "Stop this account's session after this many hours with no API activity on it. 0 means never stop it. If you leave the field out, the service default applies. Fractions are allowed, so 0.5 means 30 minutes. Every connect call sets this value again, including a call for an account that is already online. Default: service default."
                  },
                  "symbol": {
                    "type": "string",
                    "description": "Optional. Overrides the default symbol the account session opens on. It must be a symbol that exists on this broker exactly as written. If it does not exist, the session never becomes ready and the connect fails. Most callers should leave it out."
                  }
                },
                "required": [
                  "user",
                  "password",
                  "server"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The account is online. id is the account id to use with ?id= on /v1 calls. login echoes the account number. status is always \"connected\". alreadyConnected is true when the account was already online and the call answered immediately. It is false when the account came online during this call, which includes waiting for a session that was already starting.",
            "content": {
              "application/json": {
                "example": {
                  "id": "acct-700100200",
                  "login": 700100200,
                  "status": "connected",
                  "alreadyConnected": false
                }
              }
            }
          },
          "400": {
            "description": "`Bad Request` — The body fails validation. Examples: user is not a positive integer JSON number, password or server is empty, idleHours is outside 0-720, or symbol contains invalid characters. Body: {\"statusCode\":400,\"message\":[\"user must be an integer number\", ...],\"error\":\"Bad Request\"}.\n\n`LOGIN_REJECTED` — The broker explicitly rejected the login, password or server name. This usually comes back within seconds. Body: {\"code\":\"LOGIN_REJECTED\",\"message\":\"...\"}.\n\n`NOT_AUTHORIZED` — The session started but did not reach the broker's trade server within the wait window, which is about 75 seconds by default. Either the login, password or server name is wrong, or the broker is unreachable right now. Body: {\"code\":\"NOT_AUTHORIZED\",\"message\":\"...\"}."
          },
          "401": {
            "description": "`Unauthorized` — The Authorization: Bearer header is missing, malformed or does not carry the service session token. An ApiKey is not accepted on this endpoint. Body: {\"message\":\"Unauthorized\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "409": {
            "description": "`CREDENTIALS_MISMATCH` — The account is already online, or its session is still starting, under a different password than the one sent. The running session is left untouched. To switch passwords, call POST /orchestrator/disconnect first and then connect again. Body: {\"code\":\"CREDENTIALS_MISMATCH\",\"message\":\"...\"}."
          },
          "500": {
            "description": "`Internal Server Error` — The account session could not be started. The body is a plain JSON string that describes the failure."
          },
          "501": {
            "description": "`Not Implemented` — Sessions cannot be started on this deployment. The body is a plain JSON string."
          },
          "503": {
            "description": "`Service Unavailable` — Account session management is not enabled on this deployment. The body is a plain JSON string."
          },
          "504": {
            "description": "`CONNECT_TIMEOUT` — The account did not come online within the connect time limit, which is about two minutes by default. Check the login, password and server name. Body: {\"code\":\"CONNECT_TIMEOUT\",\"message\":\"...\"}."
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/orchestrator/disconnect": {
      "post": {
        "tags": [
          "Copy-trade & system"
        ],
        "operationId": "orchestrator-disconnect",
        "summary": "Disconnect an account",
        "description": "Stop an account's session and take it out of automatic recovery.\n\nThis is the call that actually takes an account offline. It stops the account's session and removes it from automatic recovery, and the account stays offline until you connect it again. GET /v1/Disconnect is a deliberate no-op and stops nothing, so use this endpoint when you mean it. The call also clears the record of which password last verified for the account, which makes it the way to move an account to a changed broker password: disconnect, then connect with the new one. The response comes back once the stop has been attempted, which can take several seconds.\n\n- The call is idempotent. A login with no running session still gets 200 with status \"disconnected\".\n\n- A 200 response does not by itself prove the session has ended. To confirm, check that the login no longer appears in running or connected in GET /orchestrator/status.\n\n- Disconnecting closes no trades. Open positions and pending orders stay on the broker account exactly as they were.\n\n- No password is needed to disconnect, but connecting the account again with POST /orchestrator/connect does need one.\n\n- The Bearer token is service-wide, so this call can stop any account on the service. Treat the token accordingly.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "integer",
                    "description": "The MT5 account login (account number) to take offline. Send it as a JSON number."
                  }
                },
                "required": [
                  "user"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stop has been attempted and the account is no longer kept online. id is the account id, login echoes the account number, and status is always \"disconnected\".",
            "content": {
              "application/json": {
                "example": {
                  "id": "acct-700100200",
                  "login": 700100200,
                  "status": "disconnected"
                }
              }
            }
          },
          "400": {
            "description": "`Bad Request` — user is missing or is not a positive integer JSON number. Body: {\"statusCode\":400,\"message\":[...],\"error\":\"Bad Request\"}."
          },
          "401": {
            "description": "`Unauthorized` — The Authorization: Bearer header is missing, malformed or wrong."
          },
          "503": {
            "description": "`Service Unavailable` — Account session management is not enabled on this deployment. The body is a plain JSON string."
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/orchestrator/bulk-order": {
      "post": {
        "tags": [
          "Copy-trade & system"
        ],
        "operationId": "orchestrator-bulk-order",
        "summary": "Place an order on many accounts",
        "description": "Place the same market order on many accounts at once (copy-trade fan-out).\n\nPlaces one market order (Buy or Sell, same symbol, same volume) on many accounts in a single request, sending it to all of them in parallel so they fill at about the same moment. WARNING: if you leave out logins, send it as null or send an empty array, the order is placed on EVERY account currently connected to the service, not only the accounts you meant. Always send an explicit logins list unless that is really what you want. The request returns 200 even when some or all accounts fail, so check the top-level ok and failed counts and each results[] item. The symbol is matched to each account's broker symbol name, and the name actually used comes back on each item.\n\n- Leaving out logins, sending logins: null or sending logins: [] places the order on EVERY account currently connected to the service. The connected list in GET /orchestrator/status shows exactly which accounts that would hit.\n\n- Unknown body fields are silently ignored, not rejected. A typo such as \"login\" or \"accounts\" in place of \"logins\" is dropped, and the order then goes to every connected account.\n\n- Failures on individual accounts never change the HTTP status. A 200 can mean every account failed, so always check ok, failed and each item's ok.\n\n- There is no idempotency key on this endpoint. Retrying a request that timed out on your side places the orders again on every account. Unlike GET /v1/OrderSendTask, which accepts clientOrderId, nothing here de-duplicates.\n\n- logins is not de-duplicated. A login listed twice gets two orders.\n\n- Listed logins that are not connected are not started by this call. Their item fails with \"account <login> is not connected\".\n\n- An item is ok only when the broker reports the order fully executed and returns a non-zero ticket. An item with ok: false and a non-zero ticket (a partial fill, for example) means the broker did act, so check the account before you retry.\n\n- Each account has its own time limit for the trade reply (10 seconds by default). A slow account shows up as a failed item and does not fail the whole request, but the order was already sent, so a timed-out item may still have executed. Check the account's positions before you retry it.\n\n- To match the symbol, the service needs the account's symbol list. If that list has not been fetched in the last ten minutes, it is fetched first, which can make that account's item noticeably slower. This applies the first time and again roughly every ten minutes.\n\n- This endpoint places market orders only. Pending orders are not supported here.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "operation": {
                    "type": "string",
                    "enum": [
                      "Buy",
                      "Sell"
                    ],
                    "description": "Order direction. Must be exactly \"Buy\" or \"Sell\"."
                  },
                  "symbol": {
                    "type": "string",
                    "description": "The base symbol, for example XAUUSD or EURUSD. It is matched per account to that broker's exact symbol name, so XAUUSD may be placed as XAUUSD.s on a broker that adds a suffix. A symbol that cannot be matched is sent as given."
                  },
                  "volume": {
                    "type": "number",
                    "description": "Volume in lots, sent as a JSON number, not a string. The same volume is placed on every target account, with no per-account scaling."
                  },
                  "sl": {
                    "type": "number",
                    "description": "Optional stop-loss as an absolute price, applied the same on every account. 0 is treated as not set."
                  },
                  "tp": {
                    "type": "number",
                    "description": "Optional take-profit as an absolute price, applied the same on every account. 0 is treated as not set."
                  },
                  "comment": {
                    "type": "string",
                    "description": "Optional order comment, attached to every order."
                  },
                  "logins": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "The account logins to place the order on, as JSON integers. If you leave it out, send null or send an empty array, the order goes to EVERY account currently connected to the service. Default: ALL currently connected accounts."
                  }
                },
                "required": [
                  "operation",
                  "symbol",
                  "volume"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A bulk result. requested is the number of target accounts. ok is how many succeeded. failed is requested minus ok. ms is the wall-clock time in milliseconds for the whole fan-out. results has one item per target login, in the same order as the logins. Each item has login, symbol (the broker symbol name actually used) and ok. When the broker replied, the item also has ticket (the order ticket, or the deal ticket if no order ticket came back, else 0) and price (execution price). A failed item adds error: the broker's comment or \"retcode <n>\" when the broker replied, or a reason such as \"account <login> is not connected\" when the order was never answered. An item that got no broker reply carries only login, symbol, ok and error.",
            "content": {
              "application/json": {
                "example": {
                  "requested": 3,
                  "ok": 2,
                  "failed": 1,
                  "ms": 412,
                  "results": [
                    {
                      "login": 700100200,
                      "symbol": "XAUUSD.s",
                      "ok": true,
                      "ticket": 118820044,
                      "price": 2401.35
                    },
                    {
                      "login": 700100201,
                      "symbol": "XAUUSD",
                      "ok": true,
                      "ticket": 408815223,
                      "price": 2401.42
                    },
                    {
                      "login": 700100202,
                      "symbol": "XAUUSD",
                      "ok": false,
                      "error": "account 700100202 is not connected"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`Bad Request` — The body fails validation. Examples: operation is not exactly \"Buy\" or \"Sell\", symbol is empty or longer than 64 characters, volume is not a positive JSON number (a string such as \"0.01\" is rejected), comment is longer than 31 characters, or logins is not an array of integers. Body: {\"statusCode\":400,\"message\":[...],\"error\":\"Bad Request\"}."
          },
          "401": {
            "description": "`Unauthorized` — The Authorization: Bearer header is missing, malformed or not the service session token. A customer API key or portal token is not accepted here. Body: {\"statusCode\":401,\"message\":\"Unauthorized\",\"error\":\"Unauthorized\"}."
          },
          "503": {
            "description": "`Service Unavailable` — Account session management is not enabled on this deployment. The body is the plain JSON string \"orchestrator is disabled\"."
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/orchestrator/bulk-close": {
      "post": {
        "tags": [
          "Copy-trade & system"
        ],
        "operationId": "orchestrator-bulk-close",
        "summary": "Close all positions on many accounts",
        "description": "Close every open position on many accounts at once.\n\nCloses every open position, in full, on each target account. Accounts are handled in parallel, and within one account positions are closed one after another. WARNING: if you leave out logins, or send it as an empty array or null, EVERY account currently connected to the service is flattened, not only the accounts you meant. Every position is closed whatever its symbol or however it was opened, not only positions placed through bulk-order. The request returns 200 with per-account outcomes even when some closes fail, so check each results[] item.\n\n- Leaving out logins, sending logins: null, sending logins: [] or sending an empty body closes positions on EVERY account currently connected to the service. There is no symbol filter.\n\n- Unknown or misspelled body fields are silently ignored, not rejected. A body such as {\"login\": 700100200} or {\"accounts\": [700100200]} counts as an empty body and closes positions on EVERY connected account, so always send the field as logins.\n\n- Only open positions are closed, each in full. Pending orders are left in place.\n\n- An account with no open positions reports ok: true with closed: 0.\n\n- When some closes on an account fail, its item has ok: false and closed set to the number that did close, with no error text. Read the account's open positions again before retrying, because a close that timed out may still have gone through at the broker.\n\n- If the broker fills a close only in part, that position is counted as failed and not counted in closed, and the rest of its volume stays open.\n\n- An item carries error only when the account's positions could not be read at all, for example because the account is not connected.\n\n- Positions opened while the call is running are not included. Each account's position list is read once, at the start.\n\n- The call takes longer the more positions an account has, because positions within one account are closed one at a time.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "logins": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "The account logins whose positions should be closed. If you leave it out, send null, send an empty array or send an empty body {}, positions are closed on EVERY account currently connected to the service. Default: ALL currently connected accounts."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A per-account summary. requested is the number of target accounts. ok is how many accounts had every position closed. failed is requested minus ok. ms is the wall-clock time in milliseconds for the whole call. results has one item per target account (in the order you listed them, when you pass logins): login, ok, closed (how many positions were closed on that account) and error (only when the account's positions could not be read at all, in which case closed is absent).",
            "content": {
              "application/json": {
                "example": {
                  "requested": 3,
                  "ok": 2,
                  "failed": 1,
                  "ms": 1380,
                  "results": [
                    {
                      "login": 700100200,
                      "ok": true,
                      "closed": 2
                    },
                    {
                      "login": 700100201,
                      "ok": true,
                      "closed": 0
                    },
                    {
                      "login": 700100202,
                      "ok": false,
                      "error": "account 700100202 is not connected"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`Bad Request` — logins is present but is not an array of integers. Body: {\"statusCode\":400,\"message\":[...],\"error\":\"Bad Request\"}."
          },
          "401": {
            "description": "`Unauthorized` — The Authorization: Bearer header is missing, malformed or wrong. Body: {\"message\":\"Unauthorized\",\"error\":\"Unauthorized\",\"statusCode\":401}."
          },
          "503": {
            "description": "`Service Unavailable` — Account session management is not enabled on this deployment. The body is the plain JSON string \"orchestrator is disabled\"."
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/orchestrator/status": {
      "get": {
        "tags": [
          "Copy-trade & system"
        ],
        "operationId": "orchestrator-status",
        "summary": "Get session status",
        "description": "Snapshot of running, connected and automatically-recovered account sessions on the service.\n\nReturns a snapshot of the service's account sessions. It shows which logins have a running session, which are connected and ready for API calls, which the service is currently keeping online, and any running sessions it is not managing. Use it to confirm that a connect or disconnect took effect, or to see exactly which accounts a bulk-order or bulk-close with no logins would hit (the connected list). counts gives the size of each list. A login can leave the managed list without a disconnect call: when it reaches its idle timeout, or when automatic recovery gives up on it.\n\n- The connected list is exactly the set of accounts that bulk-order and bulk-close target when you leave out logins.\n\n- A login can appear in running but not in connected while its session is still starting, or after it has lost its link.\n\n- A login drops out of desired, and its session is stopped, once it has had no API activity for its idle timeout (idleHours on POST /orchestrator/connect, or the service default). Call POST /orchestrator/connect again to bring it back.\n\n- If automatic recovery repeatedly fails to bring a login back, the service stops trying and removes it from desired. Check the account and call POST /orchestrator/connect again.\n\n- Logins in orphans are not restarted automatically if they drop. Call POST /orchestrator/disconnect to stop one, or POST /orchestrator/connect to bring it under management.\n\n- startedAt is epoch milliseconds. After a service restart it shows when the service resumed tracking the session, which can be later than the session's real start.\n\n- This endpoint still returns 200 when session management is switched off. Check enabled rather than relying on an error.\n\n- The snapshot covers the whole service, not only the accounts you connected.",
        "responses": {
          "200": {
            "description": "enabled: whether account session management is switched on. windows: whether the service host runs Windows. platform: the host operating system identifier (for example \"linux\"). canLaunch: whether this host can start account sessions. running: one entry per running session, with login, pid (an internal identifier, useful only for support diagnostics) and startedAt (epoch milliseconds). connected: logins that are connected and ready for API calls. desired: logins the service is currently keeping online and will try to restart automatically if they drop. orphans: logins with a running session that the service is not keeping online. counts: the length of each list.",
            "content": {
              "application/json": {
                "example": {
                  "enabled": true,
                  "windows": false,
                  "platform": "linux",
                  "canLaunch": true,
                  "running": [
                    {
                      "login": 700100200,
                      "pid": 48213,
                      "startedAt": 1791363600000
                    },
                    {
                      "login": 700100201,
                      "pid": 48377,
                      "startedAt": 1791363655000
                    }
                  ],
                  "connected": [
                    700100200,
                    700100201
                  ],
                  "desired": [
                    700100200,
                    700100201
                  ],
                  "orphans": [],
                  "counts": {
                    "running": 2,
                    "connected": 2,
                    "desired": 2,
                    "orphans": 0
                  }
                }
              }
            }
          },
          "401": {
            "description": "`Unauthorized` — The Authorization: Bearer header is missing, malformed or wrong."
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "Copy-trade & system"
        ],
        "operationId": "healthz",
        "summary": "Health check",
        "description": "Liveness check for the service, with the number of connected accounts.\n\nA liveness check you can call without authentication, suitable for uptime monitors and load-balancer probes. While the service is up and able to serve accounts, it returns 200 with status \"ok\" and the number of accounts currently connected. Otherwise it returns 503 with status \"unavailable\". It reports a count only, never logins.\n\n- A 200 says nothing about any one account, which can be offline while /healthz is healthy. To confirm a specific account, use the per-account /v1 checks such as GET /v1/CheckConnect with your ApiKey. GET /orchestrator/status also shows this, but it requires the Bearer session token.\n\n- The response is JSON, not plain text, on both 200 and 503.\n\n- /healthz is not under /v1 and needs no ApiKey or Bearer token.\n\n- accounts can drop to 0 while /healthz still returns 200; a healthy service with no connected accounts is not an error.",
        "responses": {
          "200": {
            "description": "status is always \"ok\" on a 200. accounts is the number of accounts currently connected to the service.",
            "content": {
              "application/json": {
                "example": {
                  "status": "ok",
                  "accounts": 2
                }
              }
            }
          },
          "503": {
            "description": "`unavailable` — The service cannot currently serve accounts. The body is exactly {\"status\":\"unavailable\"}, with no other fields."
          }
        },
        "security": []
      }
    },
    "/events": {
      "get": {
        "tags": [
          "Real-time"
        ],
        "operationId": "events",
        "summary": "Account event stream",
        "description": "**WebSocket.** Connect with a WebSocket client (wss://), not plain HTTP.\n\nStream live quotes, deal executions and order changes for one or more accounts over a single WebSocket.\n\nUse one WebSocket (wss://api.traderail.cloud/events) to receive real-time activity for any number of accounts. Once connected you get a `welcome` frame. Send `watch` with an account login to receive that account's `deal` and `order` events, and send `subscribe` with symbols to also receive `quote` events for them. Every event the server pushes includes the `accountId` it came from, so one connection can carry many accounts. Subscriptions belong to the connection, so after you reconnect you must send your watch and subscribe messages again. Nothing is buffered while you are disconnected, so use the history endpoints to catch up after a reconnect.\n\n- Authentication uses the single master session token, sent as an `Authorization: Bearer <token>` header or as a `?token=<token>` query parameter. Customer portal tokens (`tk_...`) and `ApiKey` values are not accepted on this socket.\n\n- The `Bearer ` prefix is case-sensitive. When the Authorization header carries a non-empty `Bearer` token, `?token=` is ignored, so a wrong header token is rejected even when the query token is correct. A header in any other form, such as lowercase `bearer ...`, is skipped and the query token is used instead.\n\n- Browsers cannot set headers on a WebSocket, so browser code has to use `?token=`. Treat that URL as a secret.\n\n- A bad token does not fail the HTTP handshake: the socket opens and is then closed with code 1008. Wait for the `welcome` frame before you treat the connection as authenticated.\n\n- The session token is not tied to particular accounts: an authenticated connection can watch any login.\n\n- Account ids in messages must be the numeric login, as a number or a numeric string. The `acct-<login>` form that /v1 accepts is rejected here with `{\"event\":\"error\",\"data\":{\"message\":\"id required\"}}`, and the connection stays open.\n\n- Watching an account is not checked: any positive login is accepted, and you receive nothing until that account has an active session.\n\n- `subscribe` also starts watching the account. Its `subscribed` reply lists every symbol this connection is subscribed to on that account, not only the ones you just added.\n\n- `unsubscribe` with no symbols, an empty `symbols` list, or a list in which every entry is invalid clears all quote subscriptions for the account. You keep receiving its deal and order events. `unwatch` stops both.\n\n- Symbols must be the broker's exact names, including any suffix (for example `XAUUSD.s`). Unlike the /v1 endpoints, this socket does not map base names such as `XAUUSD` to the broker's name. Use /v1/SymbolList to look up exact names.\n\n- An unknown symbol is still acknowledged in `subscribed` but never produces quotes.\n\n- The `subscribed` reply is sent at once and does not confirm the account accepted the subscription. If the account is not connected yet, quotes start when its session comes up, and subscriptions are restored automatically if the account's session restarts.\n\n- Quotes are sampled, not streamed tick by tick. Each symbol gets at most about one quote every 50 ms, carrying the latest tick, so some intermediate ticks are never delivered.\n\n- Right after a subscription change on the account, including one made by another connection, you may get the current quote again even if the price has not moved. While the market is closed, that quote's `time` can be old.\n\n- `last` is often 0 for symbols that do not report a last-trade price, such as most FX pairs.\n\n- One trade can produce several `order` events (add, update, delete) as well as a `deal` event. Do not rely on a fixed order of events.\n\n- Occasionally a `deal` event carries only `ticket`, `order`, `symbol`, `type`, `volume` and `price`, without entry, position, profit or reason. Code that reads deal events must handle these missing fields.\n\n- If the account has an idle timeout (ConnectEx `idleHours`, or the server default), events and quotes delivered on this socket do not count as API activity. An account that is only streamed can be stopped when the timeout expires. Its events then stop, and this socket gets no message about it.\n\n- Malformed JSON and unknown `event` names are ignored silently. The server sends no reply to them.\n\n- The server sends no heartbeats. On an idle connection, send `{\"event\":\"ping\"}` from time to time; the reply is `{\"event\":\"pong\",\"data\":{}}`.\n\n- The path must be exactly `/events`. Other paths, including `/events/`, are not upgraded.",
        "x-websocket": true,
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": false,
            "description": "`Bearer <SESSION_TOKEN>`, the master session token that also protects /orchestrator/*. You must send either this header or the `token` query parameter. When this header carries a non-empty `Bearer` token, the query parameter is ignored.",
            "schema": {
              "type": "string"
            },
            "example": "Bearer <SESSION_TOKEN>"
          },
          {
            "name": "token",
            "in": "query",
            "required": false,
            "description": "The same master session token as a query parameter, for clients that cannot set headers on a WebSocket, such as browsers. The server uses it when there is no Authorization header, or when that header does not start with `Bearer ` (case-sensitive) or carries an empty token.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-websocket-client-message": {
          "description": "One JSON text message per frame.",
          "schema": {
            "type": "object",
            "properties": {
              "event": {
                "type": "string",
                "enum": [
                  "watch",
                  "unwatch",
                  "subscribe",
                  "unsubscribe",
                  "ping"
                ],
                "description": "Name of the client message. Each client frame is one JSON text message shaped as {\"event\": \"<name>\", \"data\": {...}}. `ping` needs no `data`."
              },
              "data": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer",
                    "description": "The account login. Required for watch, unwatch, subscribe and unsubscribe. It must be a positive integer. The `acct-<login>` form is not accepted here."
                  },
                  "symbols": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Used by subscribe and unsubscribe: the broker's exact symbol names, including any suffix. Each entry is trimmed. Empty entries, non-strings and names longer than 32 characters are dropped silently. If you send `unsubscribe` with no symbols, an empty list, or a list in which every entry is dropped, all quote subscriptions for that account are removed."
                  },
                  "symbol": {
                    "type": "string",
                    "description": "A single symbol, as an alternative to `symbols`. It is read only when `symbols` is not an array."
                  }
                }
              }
            },
            "required": [
              "event"
            ]
          }
        },
        "responses": {
          "101": {
            "description": "After the upgrade, every frame is one JSON text message. Lines starting with `>` are sent by the client and lines starting with `<` are sent by the server. Server-to-client frames come in three kinds. `welcome` arrives once after a successful connect. Replies to your messages are `watching`, `subscribed`, `unsubscribed`, `pong` and `error`; they have the shape {event, data} and no accountId. Pushed events are `quote`, `deal` and `order`, with the shape {event, accountId, data}. quote data: {symbol, bid, ask, last, time}. deal data: {ticket, order, position, symbol, type (BUY|SELL|BALANCE|CREDIT|OTHER), entry (IN|OUT|INOUT|OUT_BY|OTHER), reason (CLIENT|MOBILE|WEB|EXPERT|SL|TP|SO|ROLLOVER|VMARGIN|SPLIT|OTHER), volume, price, commission, swap, profit, time, magic, comment}. order data: {ticket, symbol, transaction (add|update|delete), type (BUY|SELL|BUY_LIMIT|SELL_LIMIT|BUY_STOP|SELL_STOP|BUY_STOP_LIMIT|SELL_STOP_LIMIT|OTHER), price, volume}. All `time` values are Unix epoch seconds.",
            "content": {
              "application/json": {
                "example": "< {\"event\":\"welcome\",\"data\":{\"ok\":true}}\n> {\"event\":\"watch\",\"data\":{\"id\":700100200}}\n< {\"event\":\"watching\",\"data\":{\"accounts\":[700100200]}}\n> {\"event\":\"subscribe\",\"data\":{\"id\":700100200,\"symbols\":[\"EURUSD\",\"XAUUSD\"]}}\n< {\"event\":\"subscribed\",\"data\":{\"id\":700100200,\"symbols\":[\"EURUSD\",\"XAUUSD\"]}}\n< {\"event\":\"quote\",\"accountId\":700100200,\"data\":{\"symbol\":\"EURUSD\",\"bid\":1.08452,\"ask\":1.08461,\"last\":0,\"time\":1791374400}}\n< {\"event\":\"quote\",\"accountId\":700100200,\"data\":{\"symbol\":\"XAUUSD\",\"bid\":2412.35,\"ask\":2412.62,\"last\":0,\"time\":1791374400}}\n< {\"event\":\"order\",\"accountId\":700100200,\"data\":{\"ticket\":96000457,\"symbol\":\"EURUSD\",\"transaction\":\"add\",\"type\":\"BUY_LIMIT\",\"price\":1.0825,\"volume\":0.1}}\n< {\"event\":\"deal\",\"accountId\":700100200,\"data\":{\"ticket\":95000123,\"order\":96000456,\"position\":94000789,\"symbol\":\"XAUUSD\",\"type\":\"SELL\",\"entry\":\"OUT\",\"reason\":\"TP\",\"volume\":0.1,\"price\":2412.35,\"commission\":0,\"swap\":0,\"profit\":42.5,\"time\":1791374405,\"magic\":0,\"comment\":\"[tp 2412.35]\"}}\n> {\"event\":\"unsubscribe\",\"data\":{\"id\":700100200,\"symbols\":[\"XAUUSD\"]}}\n< {\"event\":\"unsubscribed\",\"data\":{\"id\":700100200,\"symbols\":[\"EURUSD\"]}}\n> {\"event\":\"unwatch\",\"data\":{\"id\":700100200}}\n< {\"event\":\"watching\",\"data\":{\"accounts\":[]}}\n> {\"event\":\"ping\"}\n< {\"event\":\"pong\",\"data\":{}}\n> {\"event\":\"watch\",\"data\":{\"id\":\"acct-700100200\"}}\n< {\"event\":\"error\",\"data\":{\"message\":\"id required\"}}"
              }
            }
          }
        },
        "x-websocket-close-codes": [
          {
            "code": 1008,
            "reason": "unauthorized",
            "when": "The session token is missing, not in `Bearer <token>` form (when sent as a header), or wrong. The WebSocket upgrade still succeeds, then the server immediately closes the socket with close code 1008 and reason `unauthorized`. No `welcome` frame is sent."
          }
        ],
        "security": [
          {
            "bearer": []
          }
        ]
      }
    }
  }
}
