Skip to content

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):

METHOD
PATH
TIMESTAMP
BODY

The values are normalized as follows:

  • METHOD is the uppercase HTTP method.
  • PATH is the URL path only, including its leading slash, BUT WITHOUT /api prefix. Do not include the scheme, host, fragment, or query string.
  • TIMESTAMP is the exact value sent in X-Timestamp.
  • For GET requests, BODY is an empty string. The canonical payload therefore ends with the newline that separates TIMESTAMP from BODY.
  • For all other methods, BODY is 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-GET request has no body, BODY is 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:

POST
/spot/orders
1785114000123
{
  "symbol": "BTC_USDT",
  "side": "buy"
}

Calculate the signature

  1. Calculate the SHA-256 digest of the UTF-8 encoded canonical payload.
  2. Calculate HMAC-SHA-256 over that digest, using the API secret as the HMAC key.
  3. Encode the resulting HMAC bytes with standard Base64 encoding.

In formula form:

signature = Base64(HMAC-SHA256(apiSecret, SHA256(canonicalPayload)))

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

POST
/spot/orders
1785114000123
{
  "symbol": "BTC_USDT",
  "side": "buy"
}

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=