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:
| 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
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
| 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
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:
| 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.
For the full error code reference, see Error Codes.
