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:

HeaderValue
X-MBX-APIKEYYour API access key
nonceCurrent Unix timestamp in seconds
signatureHMAC-SHA256 hex digest (see below)
Content-Typeapplication/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 returns 2000 API verification failed:

  • Sort is mandatory. Signing exchangeType=BINANCE&coin=BTC instead of coin=BTC&exchangeType=BINANCE is 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.

📘

Your nonce must be within 30 seconds of server time in either direction. Outside that window the request is rejected with 7000 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

MethodParameters
GETAppended to the URL as a query string
POST / PUT / DELETESent 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 openssl

WebSocket 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.

RESTWebSocket
Credentials sent viaHeaders — X-MBX-APIKEY, nonce, signatureA 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 signOnce per requestOnce per connection

Endpoints

EndpointURLLogin
Market Datawss://md.liquiditytech.com/marketdata/v2/publicOptional — raises rate limits
User Datawss://wss.liquiditytech.com/v1/privateRequired
Algo Orderswss://wss.liquiditytech.com/v1/private-algoRequired

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"
  }
}
FieldTypeRequiredDescription
actionStringYesMust be login.
args.apiKeyStringYesYour API key.
args.timestampStringYesCurrent 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.signStringYesHMAC-SHA256 signature (see below).
args.onlyTradeBooleanNoUser 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:

EndpointSuccess responsecode 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:

SituationResponse
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" }
⚠️

event is login on both success and failure — it is not an outcome flag. Always branch on code: 0 (Integer) on the private endpoints, "200000" (String) on Market Data. See Error Codes for the full 601xxx list.

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:

EndpointIdle timeoutClient pingRecommended interval
Market Data60 seconds{ "ping": 1769050973831 } (JSON)Every 20 seconds
User Data30 secondsping (raw string)Every 10 seconds
Algo Orders30 secondsping (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:

CodeMeaning
2000API verification failed — signature mismatch, missing signature header, or missing nonce header
7000Nonce is invalid — timestamp too far from server time, check your system clock
100018API key not found — the X-MBX-APIKEY value does not match any existing key
2001IP address not on the API key's whitelist
2002API authorization invalid
100041API key has been frozen

Troubleshooting tips:

  • 2000 is the most common error. Check that: (1) the signature and nonce headers are both present, (2) the HMAC is computed with the correct SECRET_KEY, and (3) the parameter sort order matches alphabetical key order.
  • 7000 means the nonce timestamp differs too much from the server clock. Ensure your system clock is synchronized (e.g. via NTP).
  • 100018 means the X-MBX-APIKEY header 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.


Did this page help you?