MT4 / MT5 brokers
Your clients trade on MetaTrader 4 or 5; Octolit gives you the CRM, the client dashboard and trader, sign-in, and the Partner API and webhooks around them. This guide covers the broker side: how MT logins are tied to clients, what MT accounts look like in the API, what money posting does for them, and which trading calls do not apply.
#Overview
POST …/users/{external_id}/mt_links, scope mt:link, up to 20 per usertype.provider = "mt", mt_login set409 mt_account on the platform's order path)The platform does not replace your MetaTrader server. It records which MT login belongs to which client, lists the client's accounts, books deposits and withdrawals through the CRM's transaction service, and keeps your own systems in the loop with webhooks and the Partner API.
#Two ways to run
Link only
- The client already has an MT login on your server; you tell the platform about it with one call.
- The MT side itself is not touched.
- The platform records the link (platform, server, login, label); the Partner API returns it.
- Execution stays on your MT server.
Connected MT server
- Trading accounts whose type has
provider = "mt"; such accounts carry anmt_login. - Balance and the other figures are the last ones the connector reported.
- A deposit or withdrawal booked through the CRM or the Partner API includes the balance call to the MT server.
- Without a connected MT server, orders for a MetaTrader account are placed in MetaTrader (
409 mt_account).
Both can run side by side: a client may have accounts the platform executes (provider = "internal"), MT accounts, and plain links.
#Before you start
- A Partner API key with the
mt:linkscope (CRM Settings > API & Webhooks);accounts:readto read accounts and balances,users:writeto create clients,sso:issuefor sign-in links. - The client exists in the platform with your
external_id- create it withPUT …/users/{external_id}(see Provision users and accounts). - The MT server name as you call it; one login per server belongs to one user.
#Link a login
POST …/users/{external_id}/mt_links records that the login on the named server belongs to this client. One login per server belongs to one client; a user may have at most 20 links.
| Field | Rule | Meaning |
|---|---|---|
platform | required | mt4 or mt5. |
server | required, up to 120 chars | The MT server name as you, the broker, call it (for example YourBrand-Live). |
login | required, up to 64 chars | The MT account login, as a string. |
label | up to 64 chars | A free label for the link (optional). |
trading_account_id | integer | One of the user's trading accounts (optional). |
curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213
}'const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links", {
method: "POST",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213
}),
});
if (!res.ok) throw new Error(`POST failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"POST",
"https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"]},
json={
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213,
},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
"Content-Type: " . "application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"platform" => "mt5",
"server" => "YourBrand-Live",
"login" => "5012345",
"label" => "Main MT5",
"trading_account_id" => 88213,
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
req.Content = new StringContent("""
{
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213
}
""", Encoding.UTF8, "application/json");
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213
}
"""))
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());body := strings.NewReader(`{
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213
}`)
req, err := http.NewRequest("POST", "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))var req = URLRequest(url: URL(string: "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links")!)
req.httpMethod = "POST"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = """
{
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213
}
""".data(using: .utf8)
let (data, response) = try await URLSession.shared.data(for: req)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self))val client = OkHttpClient()
val req = Request.Builder()
.url("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("POST", """{
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213
}""".toRequestBody("application/json".toMediaType()))
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} {
"mt_link": {
"id": 417,
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213,
"created_at": "2026-11-11T10:02:14Z",
"updated_at": "2026-11-11T10:02:14Z"
}
}The answer carries the link's id - keep it, the remove call needs it. The full mt_link object (including its status string) is in the reference; label and trading_account_id are null when not given.
| Status | Code | Meaning |
|---|---|---|
| 403 | insufficient_scope | The key does not carry mt:link. |
| 403 | brand_disabled | The brand has the "Partner API" switch off. |
| 404 | user_not_found | No client with that external_id in your company. |
| 409 | already_linked | That MT login on that server is already linked. Remove the old link first. |
| 422 | invalid_request | A field is missing, unknown or invalid; details.errors names it. |
{
"error": {
"code": "already_linked",
"message": "That MT login on that server is already linked.",
"details": {}
}
}#List and remove links
GET …/users/{external_id}/mt_links returns every link of the client; DELETE …/users/{external_id}/mt_links/{link_id} removes one. After the removal the same login on that server can be linked to another user; the MT side is not touched.
curl "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY"const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links", {
method: "GET",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
},
});
if (!res.ok) throw new Error(`GET failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"GET",
"https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"]},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Get, "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());var body io.Reader
req, err := http.NewRequest("GET", "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))var req = URLRequest(url: URL(string: "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links")!)
req.httpMethod = "GET"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
let (data, response) = try await URLSession.shared.data(for: req)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self))val client = OkHttpClient()
val req = Request.Builder()
.url("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.get()
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} {
"mt_links": [
{
"id": 417,
"platform": "mt5",
"server": "YourBrand-Live",
"login": "5012345",
"label": "Main MT5",
"trading_account_id": 88213,
"created_at": "2026-11-11T10:02:14Z",
"updated_at": "2026-11-11T10:02:14Z"
},
{
"id": 418,
"platform": "mt4",
"server": "YourBrand-Demo",
"login": "880231",
"label": null,
"trading_account_id": null,
"created_at": "2026-11-11T10:05:40Z",
"updated_at": "2026-11-11T10:05:40Z"
}
]
}curl -X DELETE "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY"const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417", {
method: "DELETE",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
},
});
if (!res.ok) throw new Error(`DELETE failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"DELETE",
"https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"]},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Delete, "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());var body io.Reader
req, err := http.NewRequest("DELETE", "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))var req = URLRequest(url: URL(string: "https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417")!)
req.httpMethod = "DELETE"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
let (data, response) = try await URLSession.shared.data(for: req)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self))val client = OkHttpClient()
val req = Request.Builder()
.url("https://api.your-brand.com/api/external/v1/users/cust_10042/mt_links/417")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("DELETE", null)
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} {
"unlinked": true
}#MT accounts in the API
MT accounts appear in GET …/users/{external_id}/accounts and GET …/balance next to the platform's own accounts. Tell them apart by type.provider.
curl "https://api.your-brand.com/api/external/v1/users/cust_10042/accounts" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY"const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/accounts", {
method: "GET",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
},
});
if (!res.ok) throw new Error(`GET failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"GET",
"https://api.your-brand.com/api/external/v1/users/cust_10042/accounts",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"]},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/users/cust_10042/accounts");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Get, "https://api.your-brand.com/api/external/v1/users/cust_10042/accounts");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.your-brand.com/api/external/v1/users/cust_10042/accounts"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());var body io.Reader
req, err := http.NewRequest("GET", "https://api.your-brand.com/api/external/v1/users/cust_10042/accounts", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))var req = URLRequest(url: URL(string: "https://api.your-brand.com/api/external/v1/users/cust_10042/accounts")!)
req.httpMethod = "GET"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
let (data, response) = try await URLSession.shared.data(for: req)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self))val client = OkHttpClient()
val req = Request.Builder()
.url("https://api.your-brand.com/api/external/v1/users/cust_10042/accounts")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.get()
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} {
"accounts": [
{
"id": 88213,
"name": "Main MT5",
"currency": "USD",
"demo": false,
"type": {
"id": 7,
"name": "MT5 Standard",
"provider": "mt"
},
"balance": "1250.00",
"equity": "1262.40",
"margin": "41.20",
"free_margin": "1221.20",
"margin_level": "3064.1",
"open_pl": "12.40",
"closed_pl": "0",
"credit": "0",
"mt_login": "5012345",
"created_at": "2026-11-11T09:31:00Z"
},
{
"id": 88214,
"name": "Practice",
"currency": "USD",
"demo": true,
"type": {
"id": 2,
"name": "Demo",
"provider": "internal"
},
"balance": "10000.00",
"equity": "10000.00",
"margin": "0",
"free_margin": "10000.00",
"margin_level": null,
"open_pl": "0",
"closed_pl": "0",
"credit": "0",
"mt_login": null,
"created_at": "2026-11-11T09:31:00Z"
}
]
}type.providerismtfor an MT account,internalfor an account the platform executes.mt_loginis set for MT accounts only.- In the balance call, values are the platform's live figures at
as_of; MT accounts show the last figures the connector reported. - A link (previous section) is a record, not a trading account. Set
trading_account_idon the link when the login belongs to one of the user's trading accounts.
#Money
Deposits and withdrawals are booked the same way as for every other account: in the CRM by your staff, or from your backend with POST …/users/{external_id}/transactions (scope balance:write, idempotency key required - see Money posting). The money is booked by the CRM's own transaction service - balance update, first-deposit date, client e-mails, postbacks and, for an MT account, the balance call to the MT server. status: pending waits for approval in the CRM first; deposit.status / withdrawal.status webhooks follow like for any other transaction.
#Trading APIs and MetaTrader accounts
The platform's order path - the Trading API (client keys) and trading on behalf (your Partner key) - answers with a clear code for MetaTrader accounts:
| Status | Code | When |
|---|---|---|
| 409 | mt_account | Trading API: a MetaTrader account without a connected MT server cannot trade through the API - its orders are placed in MetaTrader. Trading on behalf: a MetaTrader account is not traded through the API. |
| 409 | mt_position | Trading API: a MetaTrader position without a connected MT server cannot be changed through the API. |
{
"error": {
"code": "mt_account",
"message": "A MetaTrader account is not traded through this API.",
"details": {}
}
}Reads are not affected: accounts, balance and positions through the Partner API; quotes and bars through both APIs. The full code lists are in the error reference.
#What the client sees
Clients sign in to https://live.your-brand.com as every other client - with their password, or by single sign-on from your site or app (SSO guide, POST …/sso with target: trader or dashboard).
- Accounts: the trading accounts the platform holds for the client - MT accounts with their
mt_loginand the last figures the connector reported, internal accounts with live figures. - Money: deposits, withdrawals and credits booked by the CRM's transaction service, with their status.
- Trading: orders for a MetaTrader account are placed in MetaTrader, not through the platform's order path.
Inside your own app the Embed SDK shows the same trader.
#Webhooks
What the CRM does for an MT client reaches your systems through the webhooks: client.created, client.updated, client.login, client.inactive, client.deleted for the client; account.created for a new trading account; deposit.status and withdrawal.status for money. Use the clients delta (GET …/clients?updated_since=…) for a periodic full reconciliation with your MT user base.
#Checklist
Clients
Every MT user exists in the platform with your
external_id(PUT …/users/{external_id}).Links
Every MT login is linked with the right server name, at most 20 per user;
409 already_linkedhandled by removing the stale link first.Money
Deposits and withdrawals go through the CRM or
POST …/transactionswith an idempotency key.Trading calls
Your integration treats
409 mt_account/409 mt_positionas "placed in MetaTrader", not as a failure to retry.Sign-in
A "Dashboard" button on your site or app uses a one-time login link (
POST …/sso) or your signed JWT.Webhooks
Your endpoint verifies the signature and keys events by
id; the clients delta runs once a day as the safety net.