Authentication

Binance-style HMAC signing, so stock clients work unchanged.

Signed endpoints use exactly Binance's REST signing scheme. CCXT, python-binance, binance-connector and every other Binance client sign correctly without modification.

The scheme

Header   X-MBX-APIKEY: <api key>
Params   ...&timestamp=<ms>&recvWindow=<ms, optional>&signature=<hex>

message   = query string + request body, in the order you sent them,
            with the `signature` parameter removed
signature = HMAC-SHA256(secret, message), lowercase hex
  • timestamp is required: Unix time in milliseconds.
  • recvWindow is optional: how many milliseconds your timestamp may differ from the server clock. Default 5000, maximum 30000. A request outside the window is rejected with -1021.
  • GET and DELETE carry parameters in the query string.
  • POST carries them in the body as application/x-www-form-urlencoded. If a POST has both a query string and a body, the message is query + body with nothing between.
  • signature may be the last parameter in the query or the body.

The message is the raw parameter string. Do not sort, re-encode or URL-decode it after signing. The server verifies the bytes it receives.

Example

secret    = <your API secret>
message   = symbol=BTCUSDT&side=BUY&type=MARKET&quantity=0.001&timestamp=1759500000000&recvWindow=5000
signature = HMAC-SHA256(secret, message)

POST /api/v3/order
X-MBX-APIKEY: wlt_bot_...
Content-Type: application/x-www-form-urlencoded

symbol=BTCUSDT&side=BUY&type=MARKET&quantity=0.001&timestamp=1759500000000&recvWindow=5000&signature=<hex>

The same parameter string in a GET query signs to the same value.

Node.js

import crypto from 'node:crypto';

function signed(params, secret) {
  const query = new URLSearchParams({ ...params, timestamp: Date.now(), recvWindow: 5000 }).toString();
  const signature = crypto.createHmac('sha256', secret).update(query).digest('hex');
  return `${query}&signature=${signature}`;
}

const body = signed({ symbol: 'BTCUSDT', side: 'BUY', type: 'MARKET', quantity: '0.001' }, SECRET);
const res = await fetch('https://bot.mywellat.com/api/v3/order', {
  method: 'POST',
  headers: { 'X-MBX-APIKEY': API_KEY, 'Content-Type': 'application/x-www-form-urlencoded' },
  body,
});

Python

import hashlib, hmac, time, urllib.parse, requests

def signed(params, secret):
    params = {**params, 'timestamp': int(time.time() * 1000), 'recvWindow': 5000}
    query = urllib.parse.urlencode(params)
    sig = hmac.new(secret.encode(), query.encode(), hashlib.sha256).hexdigest()
    return f'{query}&signature={sig}'

r = requests.get(
    'https://bot.mywellat.com/api/v3/account?' + signed({}, SECRET),
    headers={'X-MBX-APIKEY': API_KEY},
)

Clock skew

If your host clock drifts you will see -1021. Either raise recvWindow (up to 30 s) or read the server time from GET /api/v3/time and offset your timestamps. CCXT does this with options.adjustForTimeDifference = true.

Public endpoints

Market data endpoints need no key, no timestamp and no signature. Sending the header anyway is harmless.

Errors you can get from authentication

HTTPcodeCause
401-2015Missing X-MBX-APIKEY, revoked or unknown key, or the signature does not match.
400-1021timestamp outside recvWindow.
400-1102timestamp missing or not an integer.
400-1131recvWindow is zero, negative, non-numeric or above 30000.
400-2010The key is bound to no strategy, to a strategy of the other family, or the request is a write against a strategy that is not LIVE.

A -2015 on a request you are sure is signed correctly usually means the message bytes differ from what you signed: a client that re-sorts parameters, URL-encodes twice, or sends a JSON body instead of a form body.

Security notes

  • The secret is shown once and never stored by Wellat. Rotate by revoking the key and creating a new one.
  • One key is bound to one strategy. A leaked key exposes that strategy's trading, nothing else: no withdrawals, no other accounts.
  • Wellat verifies the signature twice, at the public gateway and again in the trading engine. There is no way to bypass it with a header.

Did this page help you?