Authentication
All LTP API requests are authenticated with HMAC-SHA256. REST and WebSocket use the same hash function but sign different messages and pass credentials in different ways — if you are connecting over WebSocket, skip to WebSocket Authentication.
This page covers the REST signing algorithm and required headers, with complete code examples in Python, Java, and C++, followed by WebSocket login.
Code examples demonstrate a keep-alive mechanism for persistent connections, reducing connection overhead and latency.
Required Headers
Every REST request must include the following headers:
| Header | Value |
|---|---|
X-MBX-APIKEY | Your API access key |
nonce | Current Unix timestamp in seconds |
signature | HMAC-SHA256 hex digest (see below) |
Content-Type | application/json |
Signing Algorithm
Step 1 — Build the parameter string
Collect all request parameters. Sort them alphabetically by key and join as:
key1=value1&key2=value2&key3=value3
Both rules below are enforced by the server — getting either wrong returns2000 API verification failed:
- Sort is mandatory. Signing
exchangeType=BINANCE&coin=BTCinstead ofcoin=BTC&exchangeType=BINANCEis rejected.- Sign raw values, never URL-encoded ones. Percent-encoding a value before hashing it is rejected. Encoding is only for the query string you send — that part the server does not check.
If the request has no parameters, use an empty string.
Step 2 — Append the timestamp
Get the current Unix timestamp in whole seconds and append it:
{param_string}&{timestamp}
For a request with no parameters this becomes &1712345678.
Yournoncemust be within 30 seconds of server time in either direction. Outside that window the request is rejected with7000 Nonce is invalid. Keep your system clock synchronized via NTP.
Step 3 — Compute the signature
signature = HMAC-SHA256(secret_key, message).hexdigest()
Step 4 — Set headers
Send the same timestamp as nonce and the hex digest as signature.
GET vs POST
| Method | Parameters |
|---|---|
GET | Appended to the URL as a query string |
POST / PUT / DELETE | Sent as a JSON body |
In both cases, the same parameters are used to compute the signature.
Code Examples
Python
#!/usr/bin/python
# -*- encoding: utf-8 -*-
import json
import requests
import hmac
import time
import hashlib
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
def get_header(api_key: str, secret: str, params: dict = None) -> dict:
if params is None:
params = {}
nonce = get_timestamp()
message = (
"&".join(f"{k}={params[k]}" for k in sorted(params.keys()))
+ "&"
+ str(nonce)
)
sign = hmac.new(
secret.encode("utf-8"),
message.encode("utf-8"),
digestmod=hashlib.sha256,
).hexdigest()
return {
"Content-Type": "application/json",
"X-MBX-APIKEY": api_key,
"signature": sign,
"nonce": str(nonce),
}
def get_timestamp() -> int:
return int(time.time())
class Client:
def __init__(self, api_url: str, api_key: str, secret_key: str):
self.API_KEY = api_key
self.API_SECRET_KEY = secret_key
self.domain = api_url
self.session = requests.Session()
# Use keep-alive to maintain persistent connections and reduce network connection overhead (time cost).
self.session.headers.update({"Connection": "keep-alive"})
def _request(self, method: str, path: str, params: dict, timeout: int = 10):
headers = get_header(self.API_KEY, self.API_SECRET_KEY, params)
url = self.domain + path
if method == GET:
response = self.session.get(url, params=params, headers=headers, timeout=timeout)
else:
body = json.dumps(params)
if method == POST:
response = self.session.post(url, data=body, headers=headers, timeout=timeout)
elif method == PUT:
response = self.session.put(url, data=body, headers=headers, timeout=timeout)
elif method == DELETE:
response = self.session.delete(url, data=body, headers=headers, timeout=timeout)
return response.json()
class ClientAPI(Client):
def place_order(self, sym, side, ord_type, order_qty, limit_price,
time_in_force="GTC", client_order_id="", **kwargs):
params = {
"sym": sym,
"side": side,
"orderType": ord_type,
"orderQty": order_qty,
"limitPrice": limit_price,
"timeInForce": time_in_force,
}
if client_order_id:
params["clientOrderId"] = client_order_id
params.update(kwargs)
return self._request(POST, "/api/v1/trading/order", params)
def get_order(self, order_id: str = "", client_order_id: str = ""):
params = {}
if order_id:
params["orderId"] = order_id
if client_order_id:
params["clientOrderId"] = client_order_id
return self._request(GET, "/api/v1/trading/order", params)
def replace_order(self, order_id: str, replace_qty: str, replace_price: str):
params = {
"orderId": order_id,
"replaceQty": replace_qty,
"replacePrice": replace_price,
}
return self._request(PUT, "/api/v1/trading/order", params)
def cancel_order(self, order_id: str = "", client_order_id: str = ""):
params = {}
if order_id:
params["orderId"] = order_id
if client_order_id:
params["clientOrderId"] = client_order_id
return self._request(DELETE, "/api/v1/trading/order", params)
def transfer(self, from_trade_account_id, to_trade_account_id,
from_account_type, to_account_type, currency, network,
amount, rapid_transfer="false", client_order_id=""):
params = {
"fromTradeAccountId": from_trade_account_id,
"toTradeAccountId": to_trade_account_id,
"fromAccountType": from_account_type,
"toAccountType": to_account_type,
"currency": currency,
"network": network,
"amount": amount,
"rapidTransfer": rapid_transfer,
}
if client_order_id:
params["clientOrderId"] = client_order_id
return self._request(POST, "/api/v1/transfer/apply", params)
def get_transfer(self, transfer_id="", client_order_id=""):
params = {}
if transfer_id:
params["transferId"] = transfer_id
if client_order_id:
params["clientOrderId"] = client_order_id
return self._request(GET, "/api/v1/transfer/get", params)
def get_assets(self):
return self._request(GET, "/api/v1/user/asset", {})
if __name__ == "__main__":
API_KEY = ""
SECRET_KEY = ""
client = ClientAPI("https://api.liquiditytech.com", API_KEY, SECRET_KEY)
print(client.get_assets())
order_resp = client.place_order(
sym="BINANCE_PERP_BTC_USDT", side="BUY",
ord_type="LIMIT", order_qty="0.003", limit_price="90000",
)
if order_resp.get("code") in (200, 200000):
order_id = order_resp["data"]["orderId"]
print(client.get_order(order_id))
print(client.replace_order(order_id, "0.003", "90001"))
print(client.cancel_order(order_id))
else:
print("Place order failed:", order_resp)Java
Dependencies: com.alibaba.fastjson (JSON serialization), JDK 11+.
import com.alibaba.fastjson.JSON;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.Formatter;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
public class LtpApiClient {
private static final String API_KEY = "your_api_key";
private static final String SECRET_KEY = "your_secret_key";
private static final String HOST = "https://api.liquiditytech.com";
static {
System.setProperty("jdk.httpclient.keepalive.timeout", "30");
}
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(5))
.build();
private static String gmtNow() {
return String.valueOf(Instant.now().getEpochSecond());
}
private static String getPayloadForSign(Map<String, Object> params) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
return sb.toString();
}
private static String getPayload(Map<String, Object> params) throws Exception {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8"));
}
return sb.toString();
}
private static String getSign(Map<String, Object> params, String nonce) throws Exception {
String message = getPayloadForSign(params) + "&" + nonce;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET_KEY.getBytes("UTF-8"), "HmacSHA256"));
byte[] raw = mac.doFinal(message.getBytes("UTF-8"));
Formatter fmt = new Formatter();
for (byte b : raw) fmt.format("%02x", b);
return fmt.toString();
}
private static HttpRequest.Builder baseRequest(String nonce, String sign) {
return HttpRequest.newBuilder()
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
// Use keep-alive to maintain persistent connections and reduce network connection overhead (time cost).
.header("Connection", "keep-alive")
.header("nonce", nonce)
.header("signature", sign)
.header("X-MBX-APIKEY", API_KEY);
}
private static String doGet(String path, SortedMap<String, Object> params) throws Exception {
String nonce = gmtNow();
String sign = getSign(params, nonce);
String url = HOST + path;
if (!params.isEmpty()) url += "?" + getPayload(params);
HttpRequest req = baseRequest(nonce, sign).GET().uri(URI.create(url)).build();
return httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
private static String doPost(String path, SortedMap<String, Object> params) throws Exception {
String nonce = gmtNow();
String sign = getSign(params, nonce);
HttpRequest req = baseRequest(nonce, sign)
.POST(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(params)))
.uri(URI.create(HOST + path)).build();
return httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
private static String doPut(String path, SortedMap<String, Object> params) throws Exception {
String nonce = gmtNow();
String sign = getSign(params, nonce);
HttpRequest req = baseRequest(nonce, sign)
.PUT(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(params)))
.uri(URI.create(HOST + path)).build();
return httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
private static String doDelete(String path, SortedMap<String, Object> params) throws Exception {
String nonce = gmtNow();
String sign = getSign(params, nonce);
HttpRequest req = baseRequest(nonce, sign)
.method("DELETE", HttpRequest.BodyPublishers.ofString(JSON.toJSONString(params)))
.uri(URI.create(HOST + path)).build();
return httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
public static String placeOrder(String sym, String side, String orderType,
String orderQty, String limitPrice,
String timeInForce) throws Exception {
SortedMap<String, Object> params = new TreeMap<>();
params.put("sym", sym);
params.put("side", side);
params.put("orderType", orderType);
params.put("orderQty", orderQty);
params.put("limitPrice", limitPrice);
params.put("timeInForce", timeInForce);
return doPost("/api/v1/trading/order", params);
}
public static String queryOrder(String orderId) throws Exception {
SortedMap<String, Object> params = new TreeMap<>();
params.put("orderId", orderId);
return doGet("/api/v1/trading/order", params);
}
public static String replaceOrder(String orderId, String replaceQty,
String replacePrice) throws Exception {
SortedMap<String, Object> params = new TreeMap<>();
params.put("orderId", orderId);
params.put("replaceQty", replaceQty);
params.put("replacePrice", replacePrice);
return doPut("/api/v1/trading/order", params);
}
public static String cancelOrder(String orderId) throws Exception {
SortedMap<String, Object> params = new TreeMap<>();
params.put("orderId", orderId);
return doDelete("/api/v1/trading/order", params);
}
public static void main(String[] args) throws Exception {
System.out.println(doGet("/api/v1/trading/account", new TreeMap<>()));
String result = placeOrder("OKX_SPOT_ETH_USDT", "BUY", "LIMIT",
"0.0001", "3001", "GTC");
System.out.println(result);
}
}C++
Dependencies: OpenSSL, libcurl, nlohmann/json.
#include <iostream>
#include <string>
#include <map>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <vector>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <openssl/hmac.h>
using json = nlohmann::json;
const std::string HOST = "https://api.liquiditytech.com";
const std::string API_KEY = "your_api_key";
const std::string SECRET_KEY = "your_secret_key";
static CURL* g_curl = nullptr;
std::string gmtNow() {
auto now = std::chrono::system_clock::now();
return std::to_string(
std::chrono::duration_cast<std::chrono::seconds>(
now.time_since_epoch()).count());
}
std::string hmacSHA256(const std::string& data, const std::string& key) {
unsigned int len = 32;
std::vector<unsigned char> result(len);
HMAC(EVP_sha256(),
key.c_str(), static_cast<int>(key.length()),
reinterpret_cast<const unsigned char*>(data.c_str()), data.length(),
result.data(), &len);
std::ostringstream oss;
for (unsigned int i = 0; i < len; ++i)
oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(result[i]);
return oss.str();
}
std::string getPayload(const std::map<std::string, std::string>& params) {
std::ostringstream oss;
bool first = true;
for (const auto& [k, v] : params) {
if (!first) oss << "&";
oss << k << "=" << v;
first = false;
}
return oss.str();
}
size_t writeCallback(void* contents, size_t size, size_t nmemb, std::string* out) {
out->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
std::string httpRequest(const std::string& url,
const std::string& method,
const std::string& body,
const std::map<std::string, std::string>& headers) {
std::string response;
if (!g_curl) return response;
struct curl_slist* chunk = nullptr;
for (const auto& [k, v] : headers)
chunk = curl_slist_append(chunk, (k + ": " + v).c_str());
curl_easy_reset(g_curl);
curl_easy_setopt(g_curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(g_curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(g_curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(g_curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(g_curl, CURLOPT_TIMEOUT, 10L);
// Use keep-alive to maintain persistent connections and reduce network connection overhead (time cost).
curl_easy_setopt(g_curl, CURLOPT_TCP_KEEPALIVE, 1L);
curl_easy_setopt(g_curl, CURLOPT_TCP_KEEPIDLE, 30L);
curl_easy_setopt(g_curl, CURLOPT_TCP_KEEPINTVL, 10L);
if (method == "POST" || method == "PUT" || method == "DELETE") {
curl_easy_setopt(g_curl, CURLOPT_CUSTOMREQUEST, method.c_str());
if (!body.empty())
curl_easy_setopt(g_curl, CURLOPT_POSTFIELDS, body.c_str());
}
curl_easy_perform(g_curl);
curl_slist_free_all(chunk);
return response;
}
std::pair<std::map<std::string, std::string>, std::string> signRequest(
const std::map<std::string, std::string>& params) {
std::string nonce = gmtNow();
std::string queryString = getPayload(params);
std::string message = queryString + "&" + nonce;
std::string sign = hmacSHA256(message, SECRET_KEY);
std::map<std::string, std::string> headers = {
{"Content-Type", "application/json"},
{"nonce", nonce},
{"signature", sign},
{"X-MBX-APIKEY", API_KEY},
};
return {headers, queryString};
}
void placeOrder() {
std::map<std::string, std::string> params = {
{"sym", "BINANCE_SPOT_SOL_USDT"},
{"side", "BUY"},
{"orderType", "LIMIT"},
{"timeInForce", "GTC"},
{"orderQty", "0.1"},
{"limitPrice", "110"},
};
auto [headers, _] = signRequest(params);
json body(params);
std::cout << httpRequest(HOST + "/api/v1/trading/order",
"POST", body.dump(), headers) << std::endl;
}
void queryOrder(const std::string& orderId) {
std::map<std::string, std::string> params = {{"orderId", orderId}};
auto [headers, queryString] = signRequest(params);
std::cout << httpRequest(HOST + "/api/v1/trading/order?" + queryString,
"GET", "", headers) << std::endl;
}
void replaceOrder(const std::string& orderId) {
std::map<std::string, std::string> params = {
{"orderId", orderId},
{"replaceQty", "0.1"},
{"replacePrice", "111"},
};
auto [headers, _] = signRequest(params);
json body(params);
std::cout << httpRequest(HOST + "/api/v1/trading/order",
"PUT", body.dump(), headers) << std::endl;
}
void cancelOrder(const std::string& orderId) {
std::map<std::string, std::string> params = {{"orderId", orderId}};
auto [headers, _] = signRequest(params);
json body(params);
std::cout << httpRequest(HOST + "/api/v1/trading/order",
"DELETE", body.dump(), headers) << std::endl;
}
int main() {
curl_global_init(CURL_GLOBAL_ALL);
g_curl = curl_easy_init();
auto [headers, _] = signRequest({});
std::cout << httpRequest(HOST + "/api/v1/trading/account",
"GET", "", headers) << std::endl;
placeOrder();
curl_easy_cleanup(g_curl);
curl_global_cleanup();
return 0;
}CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(ltp_api_client)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)
find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED)
add_executable(ltp_api_client main.cpp)
target_link_libraries(ltp_api_client PRIVATE
nlohmann_json::nlohmann_json
OpenSSL::SSL OpenSSL::Crypto
CURL::libcurl)Install dependencies:
# Ubuntu
sudo apt install libcurl4-openssl-dev libssl-dev
# macOS
brew install curl opensslWebSocket Authentication
WebSocket authentication works differently from REST. Instead of signing your request parameters and passing credentials in headers, you send a login message over the open connection and sign a fixed string.
| REST | WebSocket | |
|---|---|---|
| Credentials sent via | Headers — X-MBX-APIKEY, nonce, signature | A login message on the open connection |
| Message that gets signed | {sorted_params}&{timestamp} — different for every request | {timestamp}GET/users/self/verify — always the same shape |
| How often you sign | Once per request | Once per connection |
Endpoints
| Endpoint | URL | Login |
|---|---|---|
| Market Data | wss://md.liquiditytech.com/marketdata/v2/public | Optional — raises rate limits |
| User Data | wss://wss.liquiditytech.com/v1/private | Required |
| Algo Orders | wss://wss.liquiditytech.com/v1/private-algo | Required |
On the two private endpoints the server pushes nothing until login succeeds. On Market Data you can subscribe immediately without logging in, but authenticating raises your limits from 5 to 40 connections per IP, and from 5 to 50 trading pairs per connection.
Login message
Send this as soon as the connection opens — on Market Data, send it before you subscribe:
{
"action": "login",
"args": {
"apiKey": "your_api_key",
"timestamp": "1538054050",
"sign": "your_signature"
}
}| Field | Type | Required | Description |
|---|---|---|---|
action | String | Yes | Must be login. |
args.apiKey | String | Yes | Your API key. |
args.timestamp | String | Yes | Current Unix timestamp in whole seconds (10 digits). Must be the same value used to build the signature. The two private endpoints reject 13-digit millisecond values with 601016; Market Data accepts either. |
args.sign | String | Yes | HMAC-SHA256 signature (see below). |
args.onlyTrade | Boolean | No | User Data endpoint only. Default false; set true to use the connection for order actions only, with no push data. |
Signature
sign = HMAC_SHA256(secret_key, timestamp + "GET" + "/users/self/verify").hexdigest()
The signed message is your timestamp concatenated with the literal strings GET and /users/self/verify. That path is a fixed constant, not an endpoint you call.
import hmac, hashlib, time
timestamp = str(int(time.time()))
message = timestamp + "GET" + "/users/self/verify"
sign = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()The same signature scheme works on all three endpoints.
Login response
The success payload is not identical across endpoints — the code type differs, so check the shape for the endpoint you are connecting to:
| Endpoint | Success response | code type |
|---|---|---|
| Market Data | { "id": null, "event": "login", "code": "200000", "msg": "success" } | String |
| User Data | { "event": "login", "code": 0, "msg": "" } | Integer |
| Algo Orders | { "event": "login", "code": 0, "msg": "" } | Integer |
Failures use the same envelope, with event still set to login:
| Situation | Response |
|---|---|
| Bad signature | { "event": "login", "code": 601009, "msg": "Login failed" } |
| Timestamp out of range, or millisecond value sent | { "event": "login", "code": 601016, "msg": "Login failed, invalid nonce" } |
eventisloginon both success and failure — it is not an outcome flag. Always branch oncode:0(Integer) on the private endpoints,"200000"(String) on Market Data. See Error Codes for the full601xxxlist.
Login rate limit: 1 request per second per API key.
Staying connected
An authenticated connection still drops when it goes idle, and both the timeout and the ping format differ by endpoint:
| Endpoint | Idle timeout | Client ping | Recommended interval |
|---|---|---|---|
| Market Data | 60 seconds | { "ping": 1769050973831 } (JSON) | Every 20 seconds |
| User Data | 30 seconds | ping (raw string) | Every 10 seconds |
| Algo Orders | 30 seconds | ping (raw string) | Under 30 seconds |
The server replies pong (or { "pong": ... } on Market Data). Implement automatic reconnection — network latency can delay a ping sent close to the timeout.
Full connection-handling guidance is on each overview page: Market Data · User Data · Algo Orders.
Authentication Errors
Common authentication-related error codes:
| Code | Meaning |
|---|---|
2000 | API verification failed — signature mismatch, missing signature header, or missing nonce header |
7000 | Nonce is invalid — timestamp too far from server time, check your system clock |
100018 | API key not found — the X-MBX-APIKEY value does not match any existing key |
2001 | IP address not on the API key's whitelist |
2002 | API authorization invalid |
100041 | API key has been frozen |
Troubleshooting tips:
2000is the most common error. Check that: (1) thesignatureandnonceheaders are both present, (2) the HMAC is computed with the correctSECRET_KEY, and (3) the parameter sort order matches alphabetical key order.7000means thenoncetimestamp differs too much from the server clock. Ensure your system clock is synchronized (e.g. via NTP).100018means theX-MBX-APIKEYheader is missing, empty, or contains a key that does not exist.
WebSocket login failures are returned on the connection itself rather than as an HTTP response — 601009 for a bad signature, 601016 for an out-of-range timestamp. See Login response above.
For the full error code reference, see Error Codes.
Updated 13 days ago
