API request signing¶
Private API calls can be authenticated with an API key instead of a bearer token. Send these headers with every signed request:
| Header | Value |
|---|---|
X-Api-Key |
Public API key |
X-Timestamp |
Current Unix timestamp in milliseconds |
X-Signature |
Base64-encoded request signature |
Keep the secret key returned when the API key is created secure. Never send it to the API or expose it in client-side code.
If the key has an IP whitelist, the request must originate from an IP address on that list. The gateway determines the IP from X-Forwarded-For (and similar headers). If no IP is provided, the request may be rejected before the signature is verified.
Create the canonical payload¶
Join the following four fields with exactly three newline characters (\n):
The values are normalized as follows:
METHODis the uppercase HTTP method.PATHis the URL path only, including its leading slash, BUT WITHOUT/apiprefix. Do not include the scheme, host, fragment, or query string.TIMESTAMPis the exact value sent inX-Timestamp.- For
GETrequests,BODYis an empty string. The canonical payload therefore ends with the newline that separatesTIMESTAMPfromBODY. - For all other methods,
BODYis the exact request body sent over HTTP. Whitespace, line breaks, and JSON object key order are preserved. The body is not parsed, compacted, or otherwise normalized. - If a non-
GETrequest has no body,BODYis an empty string.
For example, this request:
POST /api/spot/orders?source=client
X-Timestamp: 1785114000123
{
"symbol": "BTC_USDT",
"side": "buy"
}
produces this canonical payload:
Calculate the signature¶
- Calculate the SHA-256 digest of the UTF-8 encoded canonical payload.
- Calculate HMAC-SHA-256 over that digest, using the API secret as the HMAC key.
- Encode the resulting HMAC bytes with standard Base64 encoding.
In formula form:
Send the result as the X-Signature header.
Node.js example¶
import crypto from "node:crypto";
function prepareSignedRequest(
apiKey,
apiSecret,
method,
signingPath,
requestBody = "",
) {
const normalizedMethod = method.toUpperCase();
const timestamp = Date.now().toString();
const canonicalBody = normalizedMethod === "GET" ? "" : requestBody;
const canonicalPayload = [
normalizedMethod,
signingPath,
timestamp,
canonicalBody,
].join("\n");
const payloadDigest = crypto
.createHash("sha256")
.update(canonicalPayload, "utf8")
.digest();
const signature = crypto
.createHmac("sha256", apiSecret)
.update(payloadDigest)
.digest("base64");
return {
headers: {
"Content-Type": "application/json",
"X-Api-Key": apiKey,
"X-Timestamp": timestamp,
"X-Signature": signature,
},
body: normalizedMethod === "GET" ? undefined : canonicalBody,
};
}
const url = "https://exchange.example/api/spot/orders";
const signingPath = "/spot/orders";
const body = JSON.stringify({
symbol: "BTC_USDT",
side: "buy",
});
const request = prepareSignedRequest(
process.env.EXCHANGE_API_KEY,
process.env.EXCHANGE_API_SECRET,
"POST",
signingPath,
body,
);
const response = await fetch(url, {
method: "POST",
headers: request.headers,
body: request.body,
});
The body sent over HTTP must match the body used to build the canonical payload. In particular, avoid serializing an object again after calculating the signature.
Example¶
for canonical payload (NOTE: no newline after final })
Expected sha256 digest in hexadecimal form is 89cda91be4579801835fd528e83cc8dcf954a57cdca0be7186b1d851665a277d
Use secret test-hmac-secret-32bytes-long!!
It produces following signature
hexadecimal: 5ccd4e9fd895701a2e0f8c5fe267f5f976e0ad6704d01b5701ff87d409c5e86d
base64: XM1On9iVcBouD4xf4mf1+XbgrWcE0BtXAf+H1AnF6G0=