Authentication

How to sign requests using HMAC-SHA256, with examples in Python, Java, and C++

All LTP API requests must be signed using HMAC-SHA256. This page explains the signing algorithm, required headers, and provides complete code examples in Python, C++, and Java.


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

Note: For signing, use raw values without URL-encoding. For GET requests, use URL-encoded values when appending parameters to the query string.

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.

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

    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 = requests.get(url, params=params, headers=headers, timeout=timeout)
        else:
            body = json.dumps(params)
            if method == POST:
                response = requests.post(url, data=body, headers=headers, timeout=timeout)
            elif method == PUT:
                response = requests.put(url, data=body, headers=headers, timeout=timeout)
            elif method == DELETE:
                response = requests.delete(url, data=body, headers=headers, timeout=timeout)
        return response.json()


class ClientAPI(Client):
    # -- Trading --

    def place_order(self, sym, side, ord_type, order_qty, limit_price,
                    client_order_id="", **kwargs):
        params = {
            "sym": sym,
            "side": side,
            "orderType": ord_type,
            "orderQty": order_qty,
            "limitPrice": limit_price,
        }
        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)

    # -- Transfers --

    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)

    # -- Account --

    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)

    # View balances
    print(client.get_assets())

    # Place order
    order_resp = client.place_order(
        sym="BINANCE_PERP_BTC_USDT", side="BUY",
        ord_type="LIMIT", order_qty="0.003", limit_price="90000",
    )
    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))

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";

    private static final HttpClient httpClient = HttpClient.newHttpClient();

    // -- Signing --

    /** Current Unix timestamp in seconds. */
    private static String gmtNow() {
        return String.valueOf(Instant.now().getEpochSecond());
    }

    /**
     * Build the param string for signing (raw values, no URL-encoding).
     * TreeMap guarantees alphabetical key order.
     */
    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();
    }

    /**
     * Build the URL-encoded query string for GET requests.
     */
    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();
    }

    /** Compute HMAC-SHA256 and return lowercase hex string. */
    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();
    }

    // -- HTTP helpers --

    private static HttpRequest.Builder baseRequest(String nonce, String sign) {
        return HttpRequest.newBuilder()
                .timeout(Duration.ofSeconds(5))
                .header("Content-Type",  "application/json")
                .header("nonce",         nonce)
                .header("signature",     sign)
                .header("X-MBX-APIKEY", API_KEY);
    }

    // -- Trading --

    public static String placeOrder(String sym, String side, String orderType,
                                    String orderQty, String limitPrice) 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);

        String nonce = gmtNow();
        String sign  = getSign(params, nonce);

        HttpRequest req = baseRequest(nonce, sign)
                .POST(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(params)))
                .uri(URI.create(HOST + "/api/v1/trading/order"))
                .build();

        return httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body();
    }

    public static String queryOrder(String orderId) throws Exception {
        SortedMap<String, Object> params = new TreeMap<>();
        params.put("orderId", orderId);

        String nonce = gmtNow();
        String sign  = getSign(params, nonce);
        String url   = HOST + "/api/v1/trading/order?" + getPayload(params);

        HttpRequest req = baseRequest(nonce, sign)
                .GET()
                .uri(URI.create(url))
                .build();

        return httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body();
    }

    public static String cancelOrder(String orderId) throws Exception {
        SortedMap<String, Object> params = new TreeMap<>();
        params.put("orderId", orderId);

        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 + "/api/v1/trading/order"))
                .build();

        return httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body();
    }

    // -- Entry point --

    public static void main(String[] args) throws Exception {
        String result = placeOrder("OKX_SPOT_ETH_USDT", "BUY", "LIMIT", "0.0001", "3001");
        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";

// -- Helpers --

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();
}

/**
 * Build sorted param string (std::map is ordered alphabetically by key).
 * Used for both signing and GET query strings.
 */
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();
}

// -- HTTP --

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) {
    CURL* curl = curl_easy_init();
    std::string response;
    if (!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_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);

    if (method == "POST" || method == "PUT" || method == "DELETE") {
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str());
        if (!body.empty())
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
    }

    curl_easy_perform(curl);
    curl_slist_free_all(chunk);
    curl_easy_cleanup(curl);
    return response;
}

/**
 * Sign parameters and build authentication headers.
 * Returns {headers, queryString} where queryString is used for GET URLs.
 */
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);              // key=val&key=val
    std::string message     = queryString + "&" + nonce;       // append timestamp for signing only
    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};
}

// -- Trading --

void placeOrder() {
    std::map<std::string, std::string> params = {
        {"sym",        "BINANCE_SPOT_SOL_USDT"},
        {"side",       "BUY"},
        {"orderType",  "LIMIT"},
        {"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);
    // GET: only queryString in URL (no timestamp)
    std::cout << httpRequest(HOST + "/api/v1/trading/order?" + queryString,
                             "GET", "", 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() {
    placeOrder();
    queryOrder("your_order_id");
    cancelOrder("your_order_id");
    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

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.

For the full error code reference, see Error Codes.