Introduction
Welcome to the OpenAPI Documentation! This guide equips market makers with essential tools for seamless integration with our platform. Explore endpoints, request/response formats, authentication methods, and best practices to optimize your trading strategies.
Our support team is available for any assistance. Let's enhance financial markets together!
Changelog for OpenAPI
Last Updated: 2025-02-12
2025-02-12
REST API:
- Added vat and vatAsset in getTrades endpoint
2024-12-04
REST API:
- Added new API: https://openapi.m2.com/spot-api/api/public/v2/instruments, Get all available instruments for trading.
Prod urls
We have following base urls:
env | Prod |
---|---|
UI | https://m2.com |
REST API | https://openapi.m2.com |
WS | wss://stream.m2.com/endpoint |
HTTP Response Handling
We have three types of responses:
- Normal response with a 200 HTTP header.
- Error response with a 4xx/5xx HTTP header.
- Error response with a 200 HTTP header (legacy part).
You need to handle all three cases to successfully integrate, especially the third one. For the third case, you should validate each response for internal body content to understand if you get an error or not. Your program should validate each 200 HTTP header response to ensure it doesn't contain keywords "code" and "msg". If it does, validate the string value of "code". If it's not 200, assume that you have an error.
Examples:
Normal Response: Status: 200
{
"orderId": 3000000013200078,
"clOrdId": "api_99313426_1699341538302",
"symbol": "BTC-AED",
"transactTime": 1699341538302
}
Error Response: Status: 400 (can be any 4xx status, but in most cases just 400)
{
"code": "100200",
"msg": "business error",
"userMsg": "Invalid field for market buy order: orderQty"
}
Error Response: Status: 200 (handle this case too to ensure your 200 is not a real error)
{
"code": "500",
"msg": "amount_too_small",
"data": null
}
API Key & Signature
All private endpoints, including 2 websocket private endpoints, require authentication using an apiKey.
Get apiKey & secretKey
Currently, you can't generate an apiKey from the UI directly. You would have to manually obtain it. Below is a 5-step example of how to get an apiKey:
- Login into the UI.
- Enable 2FA (settings -> security -> 2FA Authentication -> enable 2FA). Use Google Authenticator or any other Auth app to set up 2-factor-authentication.
- Extract Exchange-Token from UI (in the browser, go to `inspect -> network -> choose any request and check request headers for this token).
- Manually request email OTP for apiKey creation.
- Generate apiKey pair (apiKey & secretKey).
Step 3
# Set your token here
TOKEN=YOUR_TOKEN
Set Token
Step 4: This would send an email with OTP code with the subject "M2 Email Verification For Create Api key"
curl -d '{"operationType":7}' -H 'Content-Type: application/json' -H "exchange-token: $TOKEN" https://openapi.m2.com/user-center/common/sendEmailAuthCode
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
public class Main {
public static void main(String[] args) throws Exception {
String token = "YOUR_TOKEN_HERE";
String url = "https://openapi.m2.com/user-center/common/sendEmailAuthCode";
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("exchange-token", token);
String jsonInputString = "{\"operationType\":7}";
con.setDoOutput(true);
try (OutputStream os = con.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = con.getResponseCode();
System.out.println("Response Code: " + responseCode);
}
}
import requests
url = 'https://openapi.m2.com/user-center/common/sendEmailAuthCode'
token = 'YOUR_TOKEN_HERE'
payload = {'operationType': 7}
headers = {'Content-Type': 'application/json', 'exchange-token': token}
response = requests.post(url, json=payload, headers=headers)
print('Response Code:', response.status_code)
const axios = require('axios');
const url = 'https://openapi.m2.com/user-center/common/sendEmailAuthCode';
const token = 'YOUR_TOKEN_HERE';
const payload = { operationType: 7 };
const headers = {
'Content-Type': 'application/json',
'exchange-token': token
};
axios.post(url, payload, { headers })
.then(response => {
console.log('Response Code:', response.status);
})
.catch(error => {
console.error('Error:', error);
});
Step 5 (create apiKey with read & write privilege [5,6])
curl -d '{"emailAuthCode":"111111","googleAuthCode":"111111","alias":"my_key","moduleIds":[5,6]}' -H 'Content-Type: application/json' -H "exchange-token: $TOKEN" https://openapi.m2.com/user-center/v1/api/create
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
public class Main {
public static void main(String[] args) throws Exception {
String token = "YOUR_TOKEN_HERE";
String url = "https://openapi.m2.com/user-center/v1/api/create";
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("exchange-token", token);
String jsonInputString = "{\"emailAuthCode\":\"111111\",\"googleAuthCode\":\"111111\",\"alias\":\"my_key\",\"moduleIds\":[5,6]}";
con.setDoOutput(true);
try (OutputStream os = con.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = con.getResponseCode();
System.out.println("Response Code: " + responseCode);
}
}
import requests
url = 'https://openapi.m2.com/user-center/v1/api/create'
token = 'YOUR_TOKEN_HERE'
payload = {
'emailAuthCode': '111111',
'googleAuthCode': '111111',
'alias': 'my_key',
'moduleIds': [5, 6]
}
headers = {'Content-Type': 'application/json', 'exchange-token': token}
response = requests.post(url, json=payload, headers=headers)
print('Response Code:', response.status_code)
const axios = require('axios');
const url = 'https://openapi.m2.com/user-center/v1/api/create';
const token = 'YOUR_TOKEN_HERE';
const payload = {
emailAuthCode: '111111',
googleAuthCode: '111111',
alias: 'my_key',
moduleIds: [5, 6]
};
const headers = {
'Content-Type': 'application/json',
'exchange-token': token
};
axios.post(url, payload, { headers })
.then(response => {
console.log('Response Code:', response.status);
})
.catch(error => {
console.error('Error:', error);
});
When we create an apiKey, we should decide its privilege (what our key can access). When you create an apiKey, you can pass 1 or many moduleIds, depending on what access you want to provide for your apiKey. Currently, we have 4 levels of privilege:
- ModuleID 5: enable reading (4 GET API endpoints).
- ModuleID 6: enable writing (4 POST API endpoints).
Sign the request with secretKey
- Each request sent to a private REST API endpoint should be signed.
- We use HMAC sha256 signature.
- Sig = ${timestamp} + ${http method} + ${http request path} + ${request param} + ${request body}.
- Timestamp should be an up-to-date value within a 10 min range from the current timestamp, otherwise 401 returned.
Example to request position API
# Generate signature using HMAC SHA256 algorithm
API_KEY=YOUR_API_KEY
SECRET=YOUR_SECRET
# Send GET request to validate read API (moduleId=5)
# Replace YOUR_TIMESTAMP with timestamp
curl -H 'Content-Type: application/json' -H "EXCHANGE-API-TIMESTAMP: YOUR_TIMESTAMP" -H "EXCHANGE-API-KEY: $API_KEY" -H "EXCHANGE-API-SIGN: $SIG" https://openapi.m2.com/spot-api/api/private/v1/positions
# Generate signature using HMAC SHA256 algorithm
# Replace YOUR_TIMESTAMP with timestamp
SIG=hmac('YOUR_TIMESTAMPPOST/spot-api/api/private/v1/order{"symbol":"BTC-AED","ordType":"2","orderQty":15,"price":"2.5", "side":"2"}', $SECRET)
# Send POST request to validate write API (moduleId=6)
# Replace YOUR_TIMESTAMP with timestamp
curl -H 'Content-Type: application/json' -H "EXCHANGE-API-TIMESTAMP: YOUR_TIMESTAMP" -H "EXCHANGE-API-KEY: $API_KEY" -H "EXCHANGE-API-SIGN: $SIG" -d '{"symbol":"BTC-AED","ordType":"2","orderQty":15,"price":"2.5","side":"2"}' https://openapi.m2.com/spot-api/api/private/v1/order
# Generate signature using HMAC SHA256 algorithm (pay attention that we don't use `?` in signature, we concatenate path+query without `?`)
# Replace YOUR_TIMESTAMP with timestamp
SIG=hmac('YOUR_TIMESTAMPGET/spot-api/api/private/v1/orderstype=COMPLETED&size=100', $SECRET)
# Send GET request to validate read API (moduleId=6) with "URL query params"
# Replace YOUR_TIMESTAMP with timestamp
curl -H 'Content-Type: application/json' -H "EXCHANGE-API-TIMESTAMP: YOUR_TIMESTAMP" -H "EXCHANGE-API-KEY: $API_KEY" -H "EXCHANGE-API-SIGN: $SIG" 'https://openapi.m2.com/spot-api/api/private/v1/orders?type=COMPLETED&size=100'
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.io.IOException;
import java.time.Instant;
public class Main {
private static final String API_KEY = "YOUR_API_KEY";
private static final String SECRET = "YOUR_SECRET_KEY";
public static void main(String[] args) {
sendGetRequestForPositions();
sendPostRequestForOrder();
sendGetRequestForCompletedOrders();
}
private static void sendGetRequestForPositions() {
try {
String url = "https://openapi.m2.com/spot-api/api/private/v1/positions";
String timestamp = String.valueOf(Instant.now().toEpochMilli());
String signature = generateSignature("GET", "/spot-api/api/private/v1/positions", "", timestamp);
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("EXCHANGE-API-TIMESTAMP", timestamp);
connection.setRequestProperty("EXCHANGE-API-KEY", API_KEY);
connection.setRequestProperty("EXCHANGE-API-SIGN", signature);
// Handle response...
handleResponse(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sendPostRequestForOrder() {
try {
String url = "https://openapi.m2.com/spot-api/api/private/v1/order";
String timestamp = String.valueOf(Instant.now().toEpochMilli());
String requestBody = "{\"symbol\":\"BTC-AED\",\"ordType\":\"2\",\"orderQty\":1,\"price\":\"100\",\"side\":\"2\"}";
String signature = generateSignature("POST", "/spot-api/api/private/v1/order", requestBody, timestamp);
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("EXCHANGE-API-TIMESTAMP", timestamp);
connection.setRequestProperty("EXCHANGE-API-KEY", API_KEY);
connection.setRequestProperty("EXCHANGE-API-SIGN", signature);
connection.setDoOutput(true);
connection.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
// Handle response...
handleResponse(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sendGetRequestForCompletedOrders() {
try {
String url = "https://openapi.m2.com/spot-api/api/private/v1/orders?type=COMPLETED&size=100";
String timestamp = String.valueOf(Instant.now().toEpochMilli());
String signature = generateSignature("GET", "/spot-api/api/private/v1/orders", "type=COMPLETED&size=100", timestamp);
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("EXCHANGE-API-TIMESTAMP", timestamp);
connection.setRequestProperty("EXCHANGE-API-KEY", API_KEY);
connection.setRequestProperty("EXCHANGE-API-SIGN", signature);
// Handle response...
handleResponse(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
private static String generateSignature(String method, String path, String params, String timestamp) throws NoSuchAlgorithmException, InvalidKeyException {
String data = timestamp + method + path + params;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hmacData = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
// convert bytes to hex
StringBuilder result = new StringBuilder();
for (byte b : hmacData) {
result.append(String.format("%02x", b));
}
return result.toString();
}
private static void handleResponse(HttpURLConnection connection) throws IOException {
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
BufferedReader in;
if (responseCode == HttpURLConnection.HTTP_OK) {
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Print the response body
System.out.println("Response Body: " + response.toString());
}
}
import requests
import hashlib
import hmac
import time
import json
API_KEY = "YOUR_API_KEY"
SECRET = "YOUR_API_SECRET"
def send_get_request_for_positions():
try:
url = "https://openapi.m2.com/spot-api/api/private/v1/positions"
timestamp = str(int(time.time() * 1000))
signature = generate_signature("GET", "/spot-api/api/private/v1/positions", "", timestamp)
headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
}
response = requests.get(url, headers=headers)
handle_response(response)
except Exception as e:
print(e)
def send_post_request_for_order():
try:
url = "https://openapi.m2.com/spot-api/api/private/v1/order"
timestamp = str(int(time.time() * 1000))
request_body = {
"symbol": "BTC-AED",
"ordType": "2",
"orderQty": 1,
"price": "100",
"side": "2"
}
request_body_json = json.dumps(request_body)
signature = generate_signature("POST", "/spot-api/api/private/v1/order", request_body_json, timestamp)
headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
}
response = requests.post(url, headers=headers, data=request_body_json)
handle_response(response)
except Exception as e:
print(e)
def send_get_request_for_completed_orders():
try:
url = "https://openapi.m2.com/spot-api/api/private/v1/orders?type=COMPLETED&size=100"
timestamp = str(int(time.time() * 1000))
signature = generate_signature("GET", "/spot-api/api/private/v1/orders", "type=COMPLETED&size=100", timestamp)
headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
}
response = requests.get(url, headers=headers)
handle_response(response)
except Exception as e:
print(e)
def generate_signature(method, path, params, timestamp):
data = timestamp + method + path + params
signature = hmac.new(SECRET.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest()
return signature
def handle_response(response):
print("Response Code:", response.status_code)
print("Response Body:", response.text)
def main():
send_get_request_for_positions()
send_post_request_for_order()
send_get_request_for_completed_orders()
if __name__ == "__main__":
main()
const axios = require('axios');
const crypto = require('crypto');
const API_KEY = "YOUR_API_KEY"
const SECRET = "YOUR_SECRET_KEY"
function sendGetRequestForPositions() {
try {
const url = "https://openapi.m2.com/spot-api/api/private/v1/positions";
const timestamp = Math.floor(Date.now());
const signature = generateSignature("GET", "/spot-api/api/private/v1/positions", "", timestamp);
const headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
};
axios.get(url, { headers })
.then(response => handleResponse(response))
.catch(error => console.error(error));
} catch (error) {
console.error(error);
}
}
function sendPostRequestForOrder() {
try {
const url = "https://openapi.m2.com/spot-api/api/private/v1/order";
const timestamp = Math.floor(Date.now());
const requestBody = {
symbol: "BTC-AED",
ordType: "2",
orderQty: 1,
price: "100",
side: "2"
};
const request_body_json = JSON.stringify(requestBody);
const signature = generateSignature("POST", "/spot-api/api/private/v1/order", request_body_json, timestamp);
const headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
};
axios.post(url, request_body_json, { headers })
.then(response => handleResponse(response))
.catch(error => console.error(error));
} catch (error) {
console.error(error);
}
}
function sendGetRequestForCompletedOrders() {
try {
const url = "https://openapi.m2.com/spot-api/api/private/v1/orders?type=COMPLETED&size=100";
const timestamp = Math.floor(Date.now());
const signature = generateSignature("GET", "/spot-api/api/private/v1/orders", "type=COMPLETED&size=100", timestamp);
const headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
};
axios.get(url, { headers })
.then(response => handleResponse(response))
.catch(error => console.error(error));
} catch (error) {
console.error(error);
}
}
function generateSignature(method, path, params, timestamp) {
const data = timestamp + method + path + params;
const signature = crypto.createHmac('sha256', SECRET).update(data).digest('hex');
return signature;
}
function handleResponse(response) {
console.log("Response Code:", response.status);
console.log("Response Body:", response.data);
}
function main() {
sendGetRequestForPositions();
sendPostRequestForOrder();
sendGetRequestForCompletedOrders();
}
main();
REST API
Public Endpoints
getInstruments (deprecated)
Get all available instruments for trading.
Request
// Request params
{
"symbol": "BTC-AED",
"securityId": 1,
"securityType": "SPOT_PAIR"
}
# Request
curl -X GET https://openapi.m2.com/spot-api/api/public/v1/instruments
// Request
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) {
try {
String url = "https://openapi.m2.com/spot-api/api/public/v1/instruments";
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
// Handle response...
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
BufferedReader in;
if (responseCode == HttpURLConnection.HTTP_OK) {
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Print the response body
System.out.println("Response Body: " + response.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Request
const axios = require("axios");
axios.get('https://openapi.m2.com/spot-api/api/public/v1/instruments')
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
# Request
import requests
response = requests.get('https://openapi.m2.com/spot-api/api/public/v1/instruments')
print("Response Code:", response.status_code)
print("Response:", response.json())
Request Fields
json field name | type | required | description |
---|---|---|---|
symbol | string | N | symbol of instrument (used for filtering) |
securityId | int | N | unique integer ID of instrument |
securityType | string | N | type of position (used for filtering) |
Search Rules:
- If symbol is present, search by symbol and return a single instrument.
- If securityType is present, filter by securityType and return a list of instruments.
- If securityId is present, filter by both securityType and securityId and return a single instrument. (Filter by securityId works only in combination with securityType)
Response
// Response
[
{
"symbol": "BTC-AED",
"securityId": 1,
"securityType": "SPOT_PAIR",
"instrumentStatus": "LIVE",
"baseAssetIncrement": 0.000001,
"quoteAssetIncrement": 0.01,
"baseAssetMinOrderSize": 0.01,
"quoteAssetMinOrderSize": 100,
"open": true,
"fiat": false,
}
]
Response Fields
json field name | type | description |
---|---|---|
symbol | string | symbol of instrument |
securityId | int | unique integer ID of instrument |
securityType | string | type of instrument |
instrumentStatus | string | status of pair (only for SPOT_PAIR) |
baseAssetIncrement | decimal | increment value for base asset |
quoteAssetIncrement | decimal | increment value for quote asset |
baseAssetMinOrderSize | decimal | min amount of base currency to submit the order |
quoteAssetMinOrderSize | decimal | min amount of quote currency to submit the order |
open | boolean | field to show whether trading is open |
fiat | boolean | field to show whether it's a fiat instrument |
getInstruments V2 (recommended)
Get all available instruments for trading.
Request
// Request params
{
"symbol": "BTC-AED",
"securityId": 1,
"securityType": "SPOT_PAIR"
}
# Request
curl -X GET https://openapi.m2.com/spot-api/api/public/v2/instruments
// Request
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) {
try {
String url = "https://openapi.m2.com/spot-api/api/public/v2/instruments";
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
// Handle response...
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
BufferedReader in;
if (responseCode == HttpURLConnection.HTTP_OK) {
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Print the response body
System.out.println("Response Body: " + response.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Request
const axios = require("axios");
axios.get('https://openapi.m2.com/spot-api/api/public/v2/instruments')
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
# Request
import requests
response = requests.get('https://openapi.m2.com/spot-api/api/public/v2/instruments')
print("Response Code:", response.status_code)
print("Response:", response.json())
Request Fields
json field name | type | required | description |
---|---|---|---|
symbol | string | N | symbol of instrument (used for filtering) |
securityId | int | N | unique integer ID of instrument |
securityType | string | N | type of position (used for filtering) |
Search Rules:
- If symbol is present, search by symbol and return a single instrument.
- If securityType is present, filter by securityType and return a list of instruments.
- If securityId is present, filter by both securityType and securityId and return a single instrument. (Filter by securityId works only in combination with securityType)
Response
// Response
[
{
"symbol": "BTC-AED",
"securityId": 1,
"securityType": "SPOT_PAIR",
"instrumentStatus": "LIVE",
"baseAssetIncrement": 0.000001,
"quoteAssetIncrement": 0.01,
"baseAssetMinOrderSize": 0.01,
"quoteAssetMinOrderSize": 100,
"apiTradeSupported": true,
"open": true,
"fiat": false,
}
]
Response Fields
json field name | type | description |
---|---|---|
symbol | string | symbol of instrument |
securityId | int | unique integer ID of instrument |
securityType | string | type of instrument |
instrumentStatus | string | status of pair (only for SPOT_PAIR) |
baseAssetIncrement | decimal | increment value for base asset |
quoteAssetIncrement | decimal | increment value for quote asset |
baseAssetMinOrderSize | decimal | min amount of base currency to submit the order |
quoteAssetMinOrderSize | decimal | min amount of quote currency to submit the order |
open | boolean | field to show whether trading is open |
fiat | boolean | field to show whether it's a fiat instrument |
apiTradeSupported | boolean | field to show whether it can support API spot trading |
getOrderbook
Return the current orderbook with bids/asks.
Request
// Request parameters
{
"symbol": "BTC-AED",
"depth": 20
}
# Request
curl -X GET 'https://openapi.m2.com/market-data-server/api/public/v1/orderbook?symbol=BTC-AED&depth=20'
// Request
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) {
try {
String symbol = "BTC-AED";
int depth = 20;
String url = "https://openapi.m2.com/market-data-server/api/public/v1/orderbook?symbol=" + symbol + "&depth=" + depth;
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
// Handle response...
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
BufferedReader in;
if (responseCode == HttpURLConnection.HTTP_OK) {
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Print the response body
System.out.println("Response Body: " + response.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Request
const axios = require('axios');
const symbol = 'BTC-AED';
const depth = 20;
const url = `https://openapi.m2.com/market-data-server/api/public/v1/orderbook?symbol=${symbol}&depth=${depth}`;
axios.get(url)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
# Request
import requests
symbol = 'BTC-AED'
depth = 20
url = f'https://openapi.m2.com/market-data-server/api/public/v1/orderbook?symbol={symbol}&depth={depth}'
response = requests.get(url)
print("Response Code:", response.status_code)
print("Response:", response.json())
Request Fields
json field name | type | required | description |
---|---|---|---|
symbol | string | Y | symbol for which user want to see current orderbook |
depth | int | N | depth of orderbook - how many bids/asks to show. Allowed values: 1, 5, 20, 100. Default is 20 |
Response
// Response
{
"symbol": "BTC-AED",
"depth": 20,
"transactTime": 1692249254611,
"lastUpdateId": 1234,
"bids": [[30000, 1], [29000, 2], [28000, 3]],
"asks": [[31500, 1], [32000, 2], [32500, 3]]
}
Response Fields
json field name | type | description |
---|---|---|
symbol | string | symbol for which user want to see current orderbook |
depth | int | depth of orderbook - how many bids/asks to show |
transactTime | long | time generated by the server when it builds the response |
lastUpdateId | long | unique ID of each load |
bids | decimal[][] | all buy orders combined with total quantity per price, available in orderbook, sorted by descending order |
asks | decimal[][] | all sell orders combined with total quantity per price, available in orderbook, sorted by ascending order |
getL3Orderbook
Returns the full buy/sell information of the current order book, including order information
Request
# Request
curl -X GET 'https://openapi.m2.com/market-data-server/api/public/v1/l3Orderbook?symbol=BTC-AED'
// Request
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) {
try {
String symbol = "BTC-AED";
String url = "https://openapi.m2.com/market-data-server/api/public/v1/l3Orderbook?symbol=" + symbol;
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
// Handle response...
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
BufferedReader in;
if (responseCode == HttpURLConnection.HTTP_OK) {
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Print the response body
System.out.println("Response Body: " + response.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Request
const axios = require('axios');
const symbol = 'BTC-AED';
const depth = 20;
const url = `https://openapi.m2.com/market-data-server/api/public/v1/l3Orderbook?symbol=${symbol}`;
axios.get(url)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
# Request
import requests
symbol = 'BTC-AED'
depth = 20
url = f'https://openapi.m2.com/market-data-server/api/public/v1/l3Orderbook?symbol={symbol}'
response = requests.get(url)
print("Response Code:", response.status_code)
print("Response:", response.json())
Request Fields
field name | type | required | description |
---|---|---|---|
symbol | string | Y | symbol for which user want to see current orderbook |
Response
// Response
{
"version": 938313,
"transactTime": 1729735397037,
"symbol": "BTC-AED",
"asks": [
["96103.65", "1.5733698772106991", [
["3000000030780078", "14091", "1725245952371", "0.5733698772106991"],
["3000000030780080", "14092", "1725246047468", "1"]
]]
],
"bids": [
["7999", "5.45", [
["13479", "1025970", "1729756245147", "1.25"],
["13473", "1025958", "1729756239114", "1.31"],
["8793", "1007457", "1729735418111", "1.49"],
["8772", "1007415", "1729735397037", "1.4"]
]],
["7998", "1.23", [
["4488", "1000899", "1729683192822", "1.23"]
]]
]
}
Response Fields
json field name | type | description |
---|---|---|
symbol | string | symbol for which user want to see current orderbook |
transactTime | long | time generated by the server when it builds the response |
version | long | orderbook version |
bids | array | all buy orders combined with total quantity per price, available in orderbook, sorted by descending order |
asks | array | all sell orders combined with total quantity per price, available in orderbook, sorted by ascending order |
asks[0]/bids[0] | string | ask or bid price |
asks[1]/bids[1] | string | ask or bid quantity(aggregate by price) |
asks[2]/bids[2] | array | ask or bid level3 orders array(includes all order of this price) |
asks[2][0]/bids[2][0] | string | order id |
asks[2][1]/bids[2][1] | string | order seq(for sorting) |
asks[2][2]/bids[2][2] | string | order time(for sorting) |
asks[2][3]/bids[2][3] | string | order quantity |
Private Endpoints
For private endpoints, we've included a few examples demonstrating signature generation. For all other endpoints, remember to include a signature to ensure successful requests.
createTestOrder
This API is identical to createNewOrder
, but used for testing purposes to ensure that API is accessible, authentication works, and correct responses from API are sent. In reality, no order creation happens in the backend.
curl -X POST -H "Content-Type: application/json" -d '{"param1":"value1", "param2":"value2"}' https://openapi.m2.com/spot-api/api/private/v1/testorder
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.m2.com/spot-api/api/private/v1/testorder");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setDoOutput(true);
String jsonInputString = "{\"param1\":\"value1\", \"param2\":\"value2\"}";
try (OutputStream os = con.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/testorder"
data = {"param1": "value1", "param2": "value2"}
response = requests.post(url, json=data)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/testorder';
const data = {
param1: 'value1',
param2: 'value2'
};
axios.post(url, data, {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
createNewOrder
Request
// Request body
{
"account": 1,
"clOrdId": "abc123",
"side": 1,
"orderQty": 1,
"quoteOrderQty": 3000,
"price": 30000,
"symbol": "BTC-AED",
"securityId": 1,
"ordType": 1,
"execInst": "6",
"timeInForce": "1"
}
curl -X POST -H "Content-Type: application/json" -d '{
"account": 1,
"clOrdId": "abc123",
"side": 1,
"orderQty": 1,
"quoteOrderQty": 3000,
"price": 30000,
"symbol": "BTC-AED",
"securityId": 1,
"ordType": 1,
"execInst": "6",
"timeInForce": "1"
}' https://openapi.m2.com/spot-api/api/private/v1/order
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.io.IOException;
import java.time.Instant;
public class Main {
private static final String API_KEY = "YOUR_API_KEY";
private static final String SECRET = "YOUR_SECRET";
private static final String REQUEST_METHOD = "POST";
public static void main(String[] args) {
try {
String url = "https://openapi.m2.com/spot-api/api/private/v1/order";
String timestamp = String.valueOf(System.currentTimeMillis());
String requestBody = "{\"symbol\":\"BTC-AED\",\"ordType\":\"2\",\"orderQty\":1,\"price\":\"100\",\"side\":\"2\"}";
String signature = generateSignature(REQUEST_METHOD, "/spot-api/api/private/v1/order", requestBody, timestamp);
URL apiUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod(REQUEST_METHOD);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("EXCHANGE-API-TIMESTAMP", timestamp);
connection.setRequestProperty("EXCHANGE-API-KEY", API_KEY);
connection.setRequestProperty("EXCHANGE-API-SIGN", signature);
connection.setDoOutput(true);
try (OutputStream os = connection.getOutputStream()) {
byte[] input = requestBody.getBytes(StandardCharsets.UTF_8);
os.write(input);
}
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
BufferedReader in;
if (responseCode == HttpURLConnection.HTTP_OK) {
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Print the response body
System.out.println("Response Body: " + response.toString());
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
}
private static String generateSignature(String method, String path, String params, String timestamp) throws NoSuchAlgorithmException, InvalidKeyException {
String data = timestamp + method + path + params;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hmacData = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
// convert bytes to hex
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
import requests
import hashlib
import hmac
import time
import json
API_KEY = "YOUR_API_KEY"
SECRET = "YOUR_API_SECRET"
def send_post_request_for_order():
try:
url = "https://openapi.m2.com/spot-api/api/private/v1/order"
timestamp = str(int(time.time() * 1000))
request_body = {
"symbol": "BTC-AED",
"ordType": "2",
"orderQty": 1,
"price": "100",
"side": "2"
}
request_body_json = json.dumps(request_body)
signature = generate_signature("POST", "/spot-api/api/private/v1/order", request_body_json, timestamp)
headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
}
response = requests.post(url, headers=headers, data=request_body_json)
handle_response(response)
except Exception as e:
print(e)
def generate_signature(method, path, params, timestamp):
data = timestamp + method + path + params
signature = hmac.new(SECRET.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest()
return signature
def handle_response(response):
print("Response Code:", response.status_code)
print("Response Body:", response.text)
def main():
send_post_request_for_order()
if __name__ == "__main__":
main()
const axios = require('axios');
const crypto = require('crypto');
const API_KEY = "YOUR_API_KEY"
const SECRET = "YOUR_SECRET_KEY"
function sendPostRequestForOrder() {
try {
const url = "https://openapi.m2.com/spot-api/api/private/v1/order";
const timestamp = Math.floor(Date.now());
const requestBody = {
symbol: "BTC-AED",
ordType: "2",
orderQty: 1,
price: "100",
side: "2"
};
const request_body_json = JSON.stringify(requestBody);
const signature = generateSignature("POST", "/spot-api/api/private/v1/order", request_body_json, timestamp);
const headers = {
"Content-Type": "application/json",
"EXCHANGE-API-TIMESTAMP": timestamp,
"EXCHANGE-API-KEY": API_KEY,
"EXCHANGE-API-SIGN": signature
};
axios.post(url, request_body_json, { headers })
.then(response => handleResponse(response))
.catch(error => console.error(error));
} catch (error) {
console.error(error);
}
}
function generateSignature(method, path, params, timestamp) {
const data = timestamp + method + path + params;
const signature = crypto.createHmac('sha256', SECRET).update(data).digest('hex');
return signature;
}
function handleResponse(response) {
console.log("Response Code:", response.status);
console.log("Response Body:", response.data);
}
function main() {
sendPostRequestForOrder();
}
main()
Request Fields
json field name | type | Required | Description |
---|---|---|---|
account | int | N | Only 1 account(SPOT) supported |
clOrdId | string | N | Unique key per order. Should be generated & sent by the client. If empty, it's generated by the system. |
side | char | Y | 1 = Buy, 2 = Sell |
orderQty | decimal | Y | Quantity to buy/sell, decimal format like 10.52. Either orderQty or quoteOrderQty should be provided. |
quoteOrderQty | decimal | N | Quantity to buy/sell but represented in quote asset. This field is used only for market buy orders. |
price | decimal | Y | Price to buy/sell, decimal format like 150.25. Required for LIMIT order, not needed for MARKET orders. |
symbol | string | Y | Symbol to which the order belongs, like BTC-AED |
securityId | int | N | Unique internal id of particular trading pair. Either symbol or securityId should be present in the request. If both are present, validation should ensure they correspond, otherwise return a mismatch error. |
ordType | char | Y | Type of order: 1 = Market, 2 = Limit |
execInst | string | N | Execution instruction. Currently only one value = "6" is supported to initiate post-only order. |
timeInForce | char | N | Time when to execute order. Default is 1 and currently only 1 is supported to initiate Good-till-cancel (GTC) |
Response
// Response body
{
"orderId": 12345,
"symbol": "BTC-AED",
"clOrdId": "abc123",
"transactTime": 1690894932589
}
Response Fields
json field name | type | Description |
---|---|---|
orderId | long | Generated by the system unique order identifier |
symbol | string | Value that was sent during request |
clOrdId | string | Value that was sent during request. If it was empty, a unique value would be generated by the system |
transactTime | long | Time in UTC when order was created (this is creation time, actual order execution may be some time after even for market order) |
cancelOrder
This is an async API, where we just register the request to cancel an order, but to understand if the order is cancelled or not, the user can use either websocket or getOrder API call.
// Request body
{
"account":1,
"orderId":123,
"clOrdId":"cancel_abc123",
"origClOrdId":"abc123"
}
curl -X POST -H "Content-Type: application/json" -d '{"orderId":"YOUR_ORDER_ID"}' https://openapi.m2.com/spot-api/api/private/v1/order/cancel
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.m2.com/spot-api/api/private/v1/order/cancel");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setDoOutput(true);
String jsonInputString = "{\"orderId\":\"YOUR_ORDER_ID\"}";
try (OutputStream os = con.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/order/cancel"
data = {"orderId": "YOUR_ORDER_ID"}
response = requests.post(url, json=data)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/order/cancel';
const data = {
orderId: 'YOUR_ORDER_ID'
};
axios.post(url, data, {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
Request
json field name | type | required | description |
---|---|---|---|
account | int | N | unique ID for user account |
orderId | long | Y | Actually each request should include either orderId or origClOrdId. If both are provided then a check should be done to make sure that they match |
clOrdId | string | N | uniquely-generated client orderId (if not present would be generated by the server) |
origClOrdId | string | N | original clOrdId that was used during order creation |
Response
// Response
{
"orderId":123,
"clOrdId":"cancel_abc123",
"origClOrdId":"abc123"
}
json field name | type | description |
---|---|---|
orderId | long | ID of order |
clOrdId | string | uniquely-generated client orderId (if not present would be generated by the server) |
origClOrdId | string | original clOrdId that was used during order creation |
massCancel
Cancel all open orders for a specific trading pair like BTC-AED
Request
You can use this API to either:
- Cancel all orders for a specific pair by sending the symbol
- Cancel all orders across all pairs by not sending a symbol (just send an empty body
{}
)
json field name | type | required | description |
---|---|---|---|
clOrdId | string | N | uniquely-generated client orderId (if not present would be generated by the server) |
symbol | string | N | security to which the order is belong, like BTC-AED |
securityId | int | N | unique internal id of particular trading pair. If both are present, validation should be made to ensure that they correspond, otherwise return mismatch error |
// Request body
{
"clOrdId": "cancelall_abc123",
"symbol": "BTC-AED",
"securityId": 1
}
curl -X POST -H "Content-Type: application/json" -d '{
"clOrdId": "cancelall_abc123",
"symbol": "BTC-AED",
"securityId": 1
}' https://openapi.m2.com/spot-api/api/private/v1/order/masscancel
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.m2.com/spot-api/api/private/v1/order/masscancel");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setDoOutput(true);
String jsonInputString = "{" +
"\"clOrdId\": \"cancelall_abc123\"," +
"\"symbol\": \"BTC-AED\"," +
"\"securityId\": 1" +
"}";
try (OutputStream os = con.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/order/masscancel"
data = {
"clOrdId": "cancelall_abc123",
"symbol": "BTC-AED",
"securityId": 1
}
response = requests.post(url, json=data)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/order/masscancel';
const data = {
clOrdId: 'cancelall_abc123',
symbol: 'BTC-AED',
securityId: 1
};
axios.post(url, data, {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
Response
// Response
{
"clOrdId": "cancelall_abc123",
"symbol": "BTC-AED",
"securityId": 1
}
json field name | type | description |
---|---|---|
clOrdId | string | uniquely-generated client orderId |
symbol | string | security to which the order belongs |
securityId | int | unique internal id of the particular trading pair |
getOrders
We use a single endpoint for 3 types of requests:
- getSingleOrder: Returns info for a single order only.
- getOpenOrders: Returns only open orders.
- getCompletedOrders: Gets only “completed” orders (those that are either cancelled or completely filled, so those that are not live inside the active orderbook).
Request
json field name | type | required | description |
---|---|---|---|
type | string | Y | denotes what search type to use SINGLE - get a single order by orderId/clOrdId, one of these 2 ids should be present in the request OPEN - get only open orders (those with status new or partiallyFilled) COMPLETED - get all orders |
orderId | long | N | used when we want to search for a single order by its orderId |
clOrdId | string | N | used when we want to search for a single order by its clOrdId - since this field is enforced to be unique only by customer, you may get many orders with the same clOrdId |
symbol | string | N | you can search all orders by a specific symbol BTC-AED Not required for this API. |
securityId | int | N | you can search all orders by a specific symbol, passing the ID of this symbol. If both symbol & securityId are sent, a check must be done to ensure that they match |
lastRowId | long | N | used for pagination in combination with size you send it and the next page starts from this value |
size | int | N | used for pagination in combination with page when you want to list multiple pages Default value: 500 and max value is 1000 |
startDate | long | N | Start timestamp in UTC from which time you want to search orders. The startDate define a time slot within the last 30 days. Default is 30 days ago from the current time. |
endDate | long | N | Start timestamp in UTC from which time you want to search orders. The endDate define a time slot within the last 30 days. Default is 30 days ago from the current time. |
// Request params
{
"type": "SINGLE",
"orderId": 123,
"clOrdId": "abc123",
"symbol": "BTC-AED",
"securityId": 1,
"lastRowId": 1,
"size": 100,
"filter": "side,1",
"startDate": 1690894000000,
"endDate": 1690894932589
}
curl -X GET 'https://openapi.m2.com/spot-api/api/private/v1/orders?type=SINGLE&orderId=123&clOrdId=abc123&symbol=BTC-AED&securityId=1&lastRowId=123&size=100&filter=side,1&startDate=1690894000000&endDate=1690894932589'
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
String url = "https://openapi.m2.com/spot-api/api/private/v1/orders?type=SINGLE&orderId=123&clOrdId=abc123&symbol=BTC-AED&securityId=1&lastRowId=123&size=100&filter=side,1&startDate=1690894000000&endDate=1690894932589";
URL apiUrl = new URL(url);
HttpURLConnection con = (HttpURLConnection) apiUrl.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/orders"
params = {
"type": "SINGLE",
"orderId": 123,
"clOrdId": "abc123",
"symbol": "BTC-AED",
"securityId": 1,
"lastRowId": 1,
"size": 100,
"filter": "side,1",
"startDate": 1690894000000,
"endDate": 1690894932589
}
response = requests.get(url, params=params)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/orders';
const params = {
type: 'SINGLE',
orderId: 123,
clOrdId: 'abc123',
symbol: 'BTC-AED',
securityId: 1,
lastRowId: 1,
size: 100,
filter: 'side,1',
startDate: 1690894000000,
endDate: 1690894932589
};
axios.get(url, { params })
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
Response
// Response
{
"data": List<ExecutionReportResponse>,
"pagination": Pagination
}
ExecutionReportResponse
extends ExecutionReport
{
"rowId": 1,
"type": "ORDER"
}
ExecutionReport
{
"account": 1,
"orderId": 123,
"execId": 321,
"clOrdId": "abc123",
"side": 1,
"orderQty": 1,
"quoteOrderQty": 1000,
"cumQty": 0.3,
"leavesQty": 0.7,
"price": 30000,
"stopPx": 0,
"avgPx": 29900,
"lastPx": 1,
"lastQty": 1,
"symbol": "BTC-AED",
"securityId": 1,
"ordType": 1,
"timeInForce": "1",
"ordStatus": 1,
"execType": 1,
"transactTime": 1690894932589,
"ordRejReason": 99,
"ordRejText": "balance_insufficient",
"ordCxlText": "self_match_prevention",
"execInst": "6"
}
Pagination
{
"currentPage": 1,
"size": 200,
"startRowId": 0,
"lastRowId": 200
}
* For ExecutionReport
, in case of reject we send rejectReason
but no reason is sent for cancellation.
ExecutionReport
json field name | type | description |
---|---|---|
type | string | type of result is it order or execution Values: ORDER - for /orders API endpoint TRADE - for /trades API endpoint |
rowId | long | unique sequence number of each row used for the lastRowId request for pagination |
account | int | since we have only 1 account now (SPOT), we can either omit it or require the user to send the userId. But in the future when we add sub-accounts, this field would come in handy |
orderId | long | unique ID of the order |
execId | long | unique ID of execution (one order can have multiple executions) |
clOrdId | string | unique key per order. Should be generated & sent by the client. If empty - generated by the system like “userId-timestamp” |
side | char | order side Supported values: 1 = Buy 2 = Sell |
orderQty | decimal | total quantity to buy/sell, decimal format like 10.52 |
quoteOrderQty | decimal | quantity measured in “quote” asset. Used only for market buy orders |
cumQty | decimal | already executed quantity |
leavesQty | decimal | not executed quantity |
price | decimal | price to buy/sell, decimal format like 150.25 |
stopPx | decimal | decimal price, required if ordType is stop/loss |
avgPx | decimal | average price per multiple executions (since an order can be executed multiple times with different prices, we should always calculate the average price) |
lastPx | decimal | price of the latest execution |
lastQty | decimal | qty of the latest execution don’t confuse lastPx/lastQty with price/orderQty, first for the last trade where the latter for the order itself. Since an order can be partially filled and at a better price, these values |
symbol | string | security to which the order belongs, like BTC-AED |
securityId | int | unique internal ID of a particular trading pair. Either symbol or securityId should be present in the request. If both are present, validation should be made to ensure that they correspond, otherwise a mismatch error is returned |
ordType | char | Type of order Supported values: 1 = Market 2 = Limit |
timeInForce | char | time when to execute the order |
ordStatus | char | Last up-to-date status of the order Supported values: 0 = New 1 = Partially filled 2 = Filled 4 = Canceled C = Expired 8 = Reject |
execType | char | similar to ordStatus but shows the type of the last execution |
transactTime | long | time of the last transaction (since an order may be in different states like partially executed, filled, or canceled, each phase would have its own time, but here we should store the time of the last phase) |
ordRejReason | int | reject reason as an int value according to FIX |
ordRejText | string | human-readable text explanation of the reject reason |
ordCxlText | string | reason for a cancel. In our system, we define 2 types of errors (reject, when an order doesn’t go to the orderbook and is rejected straight, and cancel - when an order gets into the orderbook but is cancelled) |
execInst | string | Currently possible value is only “6”, which means that such an order was postOnly. This value is relevant only for ORDER. For trades, it would be null |
Pagination
json field name | type | description |
---|---|---|
currentPage | int | number of the current page being returned |
size | int | number of items per page |
startRowId | int | start row ID for the current page |
lastRowId | int | last row ID for the current page |
getTrades
This API extends getOrders because there would be a single record per order, yet a single order can have multiple trades (for example, an order can have 2 partial fills and a last cancel - so totally 3 trades for a single order).
GET /spot-api/api/private/v1/trades
This returns paginated results if there are many records.
Request
// Request params
{
"symbol": "BTC-AED",
"securityId": 1,
"lastRowId": 123,
"size": 100
}
curl -X GET https://openapi.m2.com/spot-api/api/private/v1/trades
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.m2.com/spot-api/api/private/v1/trades");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/trades"
response = requests.get(url)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/trades';
axios.get(url)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
Response
ExecutionTradeReportResponse
extends ExecutionReportResponse
with following additional fields:
json field name | type | description |
---|---|---|
fee | String | fee for this trade |
feeAsset | String | fee settle asset for this trade |
vat | String | vat for this trade |
vatAsset | String | vat settle asset for this trade |
// Response
{
"data": List<ExecutionTradeReportResponse>,
"pagination": Pagination
}
The response consists of two main parts:
- data: A list of execution report responses representing the trades.
- pagination: Information about pagination.
Request
json field name | type | required | description |
---|---|---|---|
symbol | string | N | You can search all orders by a specific symbol like BTC-AED. Either symbol or securityId is required for this API. |
securityId | int | N | You can search all orders by a specific symbol, passing the ID of this symbol. If both symbol & securityId are sent, a check must be done to ensure that they match. |
size | int | N | Number of items per page. Default value: 500. |
lastRowId | long | N | Used for pagination in combination with size. You send it and the next page starts from this value. |
getAccount
Here we can leave the same object as the current web-API returns.
Request
// Request params
{
"account": 123
}
curl -X GET https://openapi.m2.com/spot-api/api/private/v1/account
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.m2.com/spot-api/api/private/v1/account");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/account"
response = requests.get(url)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/account';
axios.get(url)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
Response
// Response
{
"email": "[email protected]",
"fullname": "john doe",
"kycStatus": "LITE",
"canTrade": true,
"canLogin": true,
"canWithdraw": true,
"makerFee": 0.0009,
"takerFee": 0.0015
}
The response includes the following fields:
- email: Email of the user.
- fullName: Full name of the user.
- kycStatus: KYC status for the user.
- canTrade: Boolean indicating whether the user can trade.
- canLogin: Boolean indicating whether the user can login.
- canWithdraw: Boolean indicating whether the user can withdraw.
- makerFee: Fee for makers, not supported.
- takerFee: Fee for takers, not supported.
Request
json field name | type | required | description |
---|---|---|---|
account | int | N | Unique account ID. Since we have only a single account at the moment, this field can be empty. |
Response
json field name | type | description |
---|---|---|
string | Email of the user. | |
fullName | string | Full name of the user. |
kycStatus | string | KYC status for the user. |
canLogin | boolean | True if user can login and False if user is not allowed to login |
canTrade | boolean | True if user can trade. |
canWithdraw | boolean | True if user can withdraw. |
makerFee** | decimal | Not supported. |
takerFee** | decimal | Not supported. |
**Note: Fields with double asterisks are optional.
getPositions
GET /spot-api/api/private/v1/positions
Request
// Request params
{
"account": 123,
"symbol": "BTC-AED",
"securityId": "1",
"securityType": "ASSET",
"accountType": "SPOT"
}
curl -X GET https://openapi.m2.com/spot-api/api/private/v1/positions
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.m2.com/spot-api/api/private/v1/positions");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/positions"
response = requests.get(url)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/positions';
axios.get(url)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
This request retrieves user positions.
Response
// Response
[
{
"symbol": "BTC",
"securityId": 1,
"securityType": "ASSET",
"availableQuantity": 1,
"lockedQuantity": 1
}
]
The response returns an array of position objects with the following fields:
- symbol: Text symbol of the instrument of the position.
- securityId: Unique ID of the instrument.
- availableQuantity: Balance available for trading.
- lockedQuantity: Balance locked.
- securityType: (Optional) See securityType from /instruments API.
Request
json field name | type | required | description |
---|---|---|---|
account | int | N | Unique ID of the account. Until we have subaccounts, userId should be used. |
symbol | string | N | Text-based symbol, used as a filter parameter. If applied, only 1 position for this symbol would be returned. |
securityId | int | N | Either one securityId or securityType can be used for filtering position requests. |
securityType | string | N | See securityType from /instruments API. |
accountType | string | N | For which account type you want to get positions: SPOT: Default type, returns all positions for the spot account. MAIN: Returns positions for the main account. |
Response
json field name | type | description |
---|---|---|
symbol | string | Text symbol of the instrument of the position. |
securityId | int | Unique ID of the instrument. |
securityType** | string | (Optional) See securityType from /instruments API. |
availableQuantity | decimal | Balance available for trading. |
lockedQuantity | decimal | Balance locked. |
**Note: Fields with double asterisks are optional.
getPositions with wallet info
GET /spot-api/api/private/v1/wallet/positions
Request
// Request params
{
"account": 123,
"symbol": "BTC-AED",
"securityId": 1,
"securityType": "ASSET",
"accountType": "SPOT",
"walletCode": 0
}
curl -X GET https://openapi.m2.com/spot-api/api/private/v1/wallet/positions
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpRequest {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.m2.com/spot-api/api/private/v1/wallet/positions");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Code: " + responseCode);
System.out.println("Response: " + response.toString());
}
}
import requests
url = "https://openapi.m2.com/spot-api/api/private/v1/wallet/positions"
response = requests.get(url)
print("Response Code:", response.status_code)
print("Response:", response.json())
const axios = require('axios');
const url = 'https://openapi.m2.com/spot-api/api/private/v1/wallet/positions';
axios.get(url)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
This request retrieves user positions (both spot & open positions in case of perpetuals) with wallet info.
Response
// Response
[
{
"walletCode": 0,
"walletName": "global_account",
"assets": [
{
"symbol": "BTC",
"securityId": 1,
"securityType": "ASSET",
"availableQuantity": 1,
"lockedQuantity": 1
}
]
}
]
The response returns an array of position objects with the following fields:
- walletCode: Wallet code, 0:global_account. 1:uae_account.
- walletName: Text wallet of the instrument of the position.
- symbol: Text symbol of the instrument of the position.
- securityId: Unique ID of the instrument.
- availableQuantity: Balance available for trading.
- lockedQuantity: Balance locked.
- securityType: (Optional) See securityType from /instruments API.
Request
json field name | type | required | description |
---|---|---|---|
account | int | N | Unique ID of the account. Until we have subaccounts, userId should be used. |
symbol | string | N | Text-based symbol, used as a filter parameter. If applied, only 1 position for this symbol would be returned. |
securityId | int | N | Either one securityId or securityType can be used for filtering position requests. |
securityType | string | N | See securityType from /instruments API. |
accountType | string | N | For which account type you want to get positions: SPOT: Default type, returns all positions for the spot account. MAIN: Returns positions for the main account. |
walletCode | int | N | For which wallet you want to get positions: 0:global_account. 1:uae_account |
Response
json field name | type | description |
---|---|---|
walletCode | int | Wallet code, 0:global_account. 1:uae_account. |
walletName | string | Text wallet of the instrument of the position. |
symbol | string | Text symbol of the instrument of the position. |
securityId | int | Unique ID of the instrument. |
securityType** | string | (Optional) See securityType from /instruments API. |
availableQuantity | decimal | Balance available for trading. |
lockedQuantity | decimal | Balance locked. |
**Note: Fields with double asterisks are optional.
WebSocket API
WebSocket API designed to provide real-time data streaming for enhanced trading experiences. This API offers seamless integration with our existing infrastructure and supports various functionalities.
Usage Areas
The WebSocket API primarily focuses on two key areas:
- Order Management: Enables users to receive seamless updates on the various stages of their orders.
- Notifications: Delivers real-time streaming data on market events and state changes, including account position updates and order executions.
Heartbeat Functionality
To maintain a stable connection, clients are required to send a heartbeat message at least once every three minutes. Failure to do so will result in the closure of the connection. Here's the format for the heartbeat message:
{"id":"unique_id","type":"PING"}
For each ping message, a corresponding pong message will be received.
Authentication
Authentication within the WebSocket API is seamlessly integrated. Users need to provide the following parameters for successful authorization:
- apikey: The API key generated by the user for trading purposes.
- timestamp: Current timestamp, ensuring it's not older than five minutes.
- signature: A signature generated by the user, using HMAC signature with their API secret key.
Error Codes
The WebSocket API returns specific error codes for different scenarios:
- 400: Indicates missing or invalid parameters.
- 401: Indicates an invalid signature.
- 500: Indicates the unavailability of the authentication server.
Public Endpoints
Orderbook Updates
Provides full snapshot updates of the orderbook.
Subscription Request
// Orderbook Updates Request
{"method":"SUBSCRIBE","id":"unique_id","params":["spot_depth5@BTC-AED"]}
use params to subscrip depth you want, support depth: 5 10 20
Orderbook Updates - Real-time
Provides real-time depth data flow updates of the orderbook.
Subscription Request:
// Orderbook Updates - Real-time Request
{"method":"SUBSCRIBE","id":"unique_id","params":["spot_depth@BTC-AED"]}
Response
// Response
{
"id": "9e8ce974d81d488da4aaf8fc94e39e45",
"type": "MESSAGE",
"topic": "spot_depth",
"subject": "BTC-AED",
"sn": 0,
"body": {
"e": "depthUpdate",
"E": 1718358058983,
"s": "BTC-AED",
"U": 657,
"u": 659,
"a": [["3","2.005"]
],
"b": [["10","2.002"]
]
}
}
Calibration procedure:
- After receiving the websocket depth data flow, cache the data.
- Initiate a depth request to get the depth snapshot data of order, Rest api is: /market-data-server/api/public/v1/orderbook?symbol=BTC-AED&depth=20, Both u and U initial value is: lastUpdateId.
- Playback the cached depth data flow to depth snapshot data.
- Apply the new depth data flow to the local depth snapshot, we need to ensure that U(new)<=u+1(old) and u(new) > u(old). Update the local depth snapshot data based on u/U according to the price and size. If the size=0, update the u/U and remove the price of which the size is 0 out of depth. For other cases, please update the price.
The Change attribute of depth data is a string value of "price,size", namely: ["price", "size"].
Please note: 1. size refers to the latest size corresponding to price. When the size is 0, the corresponding price needs to be deleted from the order book.
- the rest depth:/market-data-server/api/public/v1/orderbook?symbol=BTC-AED&depth=500 param "depth" maximum is 500, it's means if the receiving websocket depth data flow price may not in 500 level orderbook, but you still need to update the u/U of local depth snapshot data, and out range price you can choose loss it.
L3 Orderbook Updates - Real-time
Provides real-time level3 depth data flow updates of the orderbook.
Subscription Request:
// Orderbook Updates - Real-time Request
{"method":"SUBSCRIBE","id":"unique_id","params":["spot_depth_l3@BTC-AED"]}
Response
// Response
{
"id": "2068a92fa9734dae9734105f0518ba26",
"type": "MESSAGE",
"topic": "spot_depth_l3",
"subject": "BTC-AED",
"sn": 0,
"body": {
"startVersion": 917659,
"endVersion": 917659,
"symbol": "BTC-AED",
"asks": [],
"bids": [
[
"7951",
"2.61",
[
[
"11771",
"1019191",
"1729748594947",
"0.0"
]
]
]
],
"midPrice": "7951"
}
}
Calibration procedure:
- After receiving the websocket depth data flow, cache the data.
- Initiate a full orders snapshot request to get the full orders snapshot data
- Playback the cached increment orders data flow to full orders snapshot data.
- Apply the new increment orders data flow to the local full orders snapshot, we need to ensure that version start(new)<=version end(old)+1 and version end(new) > version end(old). Update the local full orders snapshot data based on version start/version end according to the orderId and size. If the size=0, update the version start/version end and remove the orderId of which the size is 0 out of orderbook. For other cases, please update the order(size).
Please note: 1. size refers to the latest size corresponding to orderid. When the size is 0, the corresponding orderid needs to be deleted from the order book. 2. orders sorting rules: order time + seq 3. websocket may loss data,so if version discontinuous,client need to rebuild order book,rebuild way: clear local orderbook and redo: Calibration procedure.
Trade Updates
Delivers real-time updates on trades.
Subscription Request:
// Trade Updates Request
{"method":"SUBSCRIBE","id":"unique_id","params":["spot_trade@BTC-AED"]}
Private Endpoints
User Positions
Notifies about any changes in user positions, including deposits, withdrawals, or trades.
Subscription Request:
// User Positions Request
{"method":"SUBSCRIBE","id":"unique_id","params":["api_user_positions@all"]}
Response
// User Positions Response
{
"id": "80c034223ee34f4e8ec01c871b65aaff",
"type": "MESSAGE",
"topic": "api_user_positions",
"subject": "all",
"sn": 0,
"body": {
"symbol": "BTC",
"securityId": 0,
"securityType": 0,
"availableQuantity": 993,
"lockedQuantity": 3
}
}
Response Fields
Field Name | Type | Description |
---|---|---|
symbol | string | Unique symbol representing a trading pair. |
securityId | int | unique integer ID of instrument |
securityType | string | type of position (used for filtering) |
availableQuantity | decimal | Balance available for trading. |
lockedQuantity | decimal | Balance locked. |
User Positions(Recommended)
Notifies about any changes in user positions, including deposits, withdrawals, or trades.
Subscription Request:
{"method":"SUBSCRIBE","id":"unique_id","params":["COMMON@ACCOUNT"]}
Response
{
"id": "9cd3a3110aa74a89a4a4cd899d4a9dbc",
"type": "MESSAGE",
"topic": "COMMON",
"subject": "ACCOUNT",
"sn": 0,
"body": {
"e": "balanceUpdate", // event type
"E": 1718796759745, // event time
"a": "BTC", // asset
"d": "100", // balance change
"T": 1718796759745, // clear time
"f": "10", // available balance
"l": "0", // locked balance
"w": 0 // wallet code, 0:global_account 1:uae_account
}
}
Response Fields
Field Name | Type | Description |
---|---|---|
e | string | Event type. |
E | long | Event time. |
a | string | Asset |
d | string | Balance change. |
f | string | Available balance. |
l | string | Locked balance. |
T | long | Clear time. |
w | int | Wallet code, 0:global_account 1:uae_account. |
User Orders
Provides updates on all order interactions and cancel reject notifications.
Subscription Request
// User Orders Request
{"method":"SUBSCRIBE","id":"unique_id","params":["api_user_orders@all"]}
ExecutionReport Response
the normal execution report response
// ExecutionReport Response
{
"id": "35e7fad4297c4fddae1d51bee2a03add",
"type": "MESSAGE",
"topic": "api_user_orders",
"subject": "all",
"sn": 0,
"body": {
"account": 99313426,
"orderId": 2000000011890072,
"execId": 0,
"clOrdId": "api_99313426_1697888777740",
"side": "2",
"orderQty": 1,
"quoteOrderQty": 0,
"cumQty": 0,
"leavesQty": 1,
"price": 100,
"lastPx": 0,
"symbol": "BTC-AED",
"securityId": 3,
"ordType": "2",
"timeInForce": "1",
"ordStatus": "0",
"execType": "0",
"transactTime": 1697888781022,
"ordRejReason": 0,
"e": "executionReport"
}
}
Response Fields
Field Name | Type | Description |
---|---|---|
account | string | Unique ID of the account. Until we have subaccounts, userId should be used. |
orderId | int | Generated by the system unique order identifier |
execId | string | unique ID of execution (one order can have multiple executions) |
clOrdId | string | unique key per order. Should be generated & sent by the client. If empty - generated by the system like “userId-timestamp” |
side | char | order side Supported values: 1 = Buy 2 = Sell |
orderQty | decimal | total quantity to buy/sell, decimal format like 10.52 |
quoteOrderQty | decimal | quantity measured in “quote” asset. Used only for market buy orders |
cumQty | decimal | already executed quantity |
leavesQty | decimal | not executed quantity |
price | decimal | price to buy/sell, decimal format like 150.25 |
lastPx | decimal | price of the latest execution |
lastQty | decimal | qty of the latest execution don’t confuse lastPx/lastQty with price/orderQty, first for the last trade where the latter for the order itself. Since an order can be partially filled and at a better price, these values |
symbol | string | security to which the order belongs, like BTC-AED |
securityId | int | unique internal ID of a particular trading pair. Either symbol or securityId should be present in the request. If both are present, validation should be made to ensure that they correspond, otherwise a mismatch error is returned |
ordType | char | Type of order Supported values: 1 = Market 2 = Limit |
timeInForce | char | time when to execute the order |
ordStatus | char | Last up-to-date status of the order Supported values: 0 = New 1 = Partially filled 2 = Filled 4 = Canceled C = Expired 8 = Reject |
execType | char | similar to ordStatus but shows the type of the last execution |
transactTime | long | time of the last transaction (since an order may be in different states like partially executed, filled, or canceled, each phase would have its own time, but here we should store the time of the last phase) |
ordRejReason | int | reject reason as an int value according to FIX |
ordRejText | string | human-readable text explanation of the reject reason |
ordCxlText | string | reason for a cancel. In our system, we define 2 types of errors (reject, when an order doesn’t go to the orderbook and is rejected straight, and cancel - when an order gets into the orderbook but is cancelled) |
OrderCancelReject Response
order cancel or reject response
// OrderCancelReject Response
{
"id": "35e7fad4297c4fddae1d51bee2a03add",
"type": "MESSAGE",
"topic": "api_user_orders",
"subject": "all",
"sn": 0,
"body": {
"account": 123,
"orderId": 123,
"clOrdId": "abc",
"origClOrdId": "abc",
"cxlRejReason": 1,
"text": "failed",
"e": "executionReport"
}
}
Response Fields
Field Name | Type | Description |
---|---|---|
account | string | Unique ID of the account. Until we have subaccounts, userId should be used. |
orderId | int | Generated by the system unique order identifier |
clOrdId | string | Unique key per order. Should be generated & sent by the client. If empty, it's generated by the system. |
cxlRejReason | int | 1:order not found in match-engie 2:other reason |
text | string | system cancel reason message |
Example Connection
Here's an example demonstrating how to connect to private WebSocket channels using HMAC SHA256 signature:
API_KEY=YOUR_API_KEY
SECRET=YOUR_SECRET_KEY
SIG=hmac(API_KEY + timestamp, SECRET)
wscat -c "wss://stream.m2exchange.com/endpoint?apikey=$API_KEY×tamp=YOUR_TIMESTAMP&signature=$SIG"
Upon successful connection, you will receive a welcome message. You can then subscribe to the desired topics for real-time updates:
{"method":"SUBSCRIBE","id":"123","params":["spot_depth5@BTC-AED","spot_trade@BTC-AED","api_user_orders@all","api_user_positions@all"]}
For each subscription, you will receive an acknowledgment message (ACK
).
Errors
Definitions errors in the REST API and Websocket is as follow
REST API Errors
There are 3 types of orders:
Auth error:
- httpStatus=401 (by apiGateway) - invalid signature
- httpStatus=403 (by tradeSpot) - signature is valid, but user tries to submit request on behalf of not-his-own account
4xx errors. httpStatus=4xx, json body with error details
200 errors. httpStatus=200, json body with error details
JSON Body Example
4xx errors
{
"code": "100200",
"msg": "business error",
"userMsg": "Instrument not found: symbol=BNB-AED"
}
200 errors
{
"code": "500",
"msg": "symbol_not_open",
"data": null
}
List of 4xx errors:
HTTP Status | userMsg | error explanation |
---|---|---|
400 | Max allowed length for clOrdId=36 | Max size that string for clOrdId field can be is 36 characters |
400 | Invalid field for market buy order: orderQty | When you send market buy order you should include quantity in quote asset with a field quoteOrderQty. For example for BTC/USDT pair if you want to buy bitcoin for 1000usdt you should add quoteOrderQty=1000 |
400 | Field missing for market buy order: quoteOrderQty | When you send market buy order, field quoteOrderQty should be provided with the request body |
400 | Field missing: orderQty | When you send market sell order, field orderQty should be provided with the request body |
400 | Invalid field for market order: price | When you send market order, you shouldn’t include field price into request body |
400 | Invalid pair | When you send request to create new order, you can only send it for pairs with valid symbol. You can check the pair when you fetch instruments |
400 | Instrument is not a pair: securityType=ASSET | You should send request to a correct trading pair. If you send request with symbol=USDT you will get this error, cause USDT is ASSET not a PAIR |
400 | Instrument not found: symbol=BTC-ABC | If you send request to a PAIR that doesn’t exist (you can take a look at all existing pairs using instruments API), you will get this error, cause we don’t have pair BTC-ABC |
List of 200 errors (code would always be 500, and data would always be null):
msg | Error Explanation | |
---|---|---|
1 | symbol_not_open | The trade pair is not allowed to trade; Please contact tech support team. |
2 | user_not_found | User session not found, you can try to log in again. |
3 | user_kyc_not_passed | You have to be KYC verified. If you do believe you have a valid KYC verified, please contact CX team. |
4 | user_invalid | Your profile status is identified as invalid. Please contact CX team. |
5 | no_trade_permission | You have no permission to place an order. Please contact CX team. |
6 | side_is_required | Order “side” is required, either buy or sell. |
7 | symbol_is_required | Order symbol is required. |
8 | type_is_required | Order type is required. |
9 | limit_order_price_is_required | For a limit order, you must specify the price. |
10 | limit_order_price_should_positive | For a limit order, the price must be positive. |
11 | limit_order_quantity_is_required | For a limit order, you must specify the quantity. |
12 | limit_order_quantity_should_positive | For a limit order, the quantity must be positive. |
13 | not_support_order_type | Order type is not supported. We only support order types: LIMIT, MARKET. |
14 | clientOid_invalid | For open-api in the web/app should just ignore. |
15 | market_order_need_no_price | The market order should NOT have a price. |
16 | buy_order_amount_is_required | The market buy order must have an amount of quote. |
17 | buy_order_amount_should_be_positive | The market buy order amount of quote must be positive. |
18 | sell_order_quantity_is_required | The market sell order must have a quantity of base pass through. |
19 | buy_order_need_not_quantity | The market buy order must have a quantity of base pass through. |
20 | market_order_should_not_provide_both_quantity_amount | The market order can only have one quantity (base) and amount (quote). |
21 | market_order_should_provide_quantity_or_amount | The market order must have one quantity (base) and amount (quote). |
22 | order_quantity_should_be_positive | Order quantity cannot be negative. |
23 | invalid_amount_precision | Invalid order amount (quote) precision. |
24 | quantity_too_small | Order quantity (base) is too small. |
25 | invalid_quantity_precision | Invalid order quantity (base) precision. |
26 | invalid_min_price_precision | Invalid precision of price. |
27 | order_min_price_should_zero_or_positive | Min price precision should be no less than 0. |
28 | minQty_too_small | MinQty is too small. |
29 | amount_should_be_positive | Amount should be positive if present. |
30 | quantity_should_be_positive | Quantity should be positive if present. |
31 | order_max_price_should_zero_or_positive | Max price should be no less than 0. |
32 | order_should_provide_either_quantity_or_amount | Order should have either Price of Base OR Amount of Quote. |
33 | order_should_not_null_both_quantity_and_amount | Order must have have either Price of Base OR Amount of Quote, but can not have both be null. |
34 | market_order_should_provide_quantity_or_amount | Market Order should have either Price of Base OR Amount of Quote. |
35 | market_order_should_not_provide_both_quantity_amount | Market Order should not have both Price of Base and Amount of Quote. |
36 | market_order_need_no_price | For market orders, there should be no price pass through. |
37 | balance_insufficient | User balance is insufficient. |
38 | quantity_too_small | The order quantity is too small. |
39 | quantity_not_match_increment | The price incremental is incorrect. |
40 | amount_too_small | The order amount is too small. |
41 | amount_not_match_increment | The order amount incremental is incorrect. |
42 | request_timeout | Request timeout. |
43 | exceed_user_max_limit | The user’s number of open order reaches the maximum cap of one user. |
44 | exceed_orderbook_max_limit | The number of trades in the order book reaches the match engine maximum cap。 |
45 | orderId_duplicated | Duplicate order. |
46 | market_order_not_support_cancel | User can't cancel a market order. |
47 | trade_paused | The trade was paused trading. |
48 | price_can_not_match | Price band issue |
49 | trade_cancel_only | The pair status is "cancel only," which means the user can only cancel their orders and cannot place any new orders for this pair. |
50 | symbol_not_open | The pair is not available for trade. |
51 | coin_not_found | The quote or base coin is not valid in the exchange. |
52 | user_symbol_entities_not_match | The user’s entity doesn’t match the pair’s entity. It may be Bahamas user wanna trade ADGM pair, which is not legalized. |
53 | no_trading_allowed | The pair doesn’t support this order. |
54 | pair_not_found | The specified pair was not found. |
55 | user_entity_region_not_found | Can't find the user’s entity region, need to contact CX team to verify KYC status. |
56 | pair_entity_not_found | Can't find the pair’s entity, which means the entity is missing for the pair config. Need contact tech support team. |
Websocket Errors
Such errors returned through websocket with status=rejected
- balance_insufficient - user has not enough balance in account to initiate the trade
- post_only_match_reject - postOnly LIMIT order was rejected, cause it has crossing price
- exceed_user_max_limit - user is not allowed to have more than 100 open orders across all orderbooks
- exceed_orderbook_max_limit - orderbook per symbol can’t have more than 200 open orders (both bids & asks combined). If this limit exceeds, the orderbook can’t accept any new orders from any user.
Feel free to reach out to support if you have any questions or need further assistance!