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 ...×tamp=<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
timestampis required: Unix time in milliseconds.recvWindowis optional: how many milliseconds your timestamp may differ from the server clock. Default5000, maximum30000. A request outside the window is rejected with-1021.GETandDELETEcarry parameters in the query string.POSTcarries them in the body asapplication/x-www-form-urlencoded. If aPOSThas both a query string and a body, the message isquery + bodywith nothing between.signaturemay 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×tamp=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×tamp=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
| HTTP | code | Cause |
|---|---|---|
| 401 | -2015 | Missing X-MBX-APIKEY, revoked or unknown key, or the signature does not match. |
| 400 | -1021 | timestamp outside recvWindow. |
| 400 | -1102 | timestamp missing or not an integer. |
| 400 | -1131 | recvWindow is zero, negative, non-numeric or above 30000. |
| 400 | -2010 | The 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.
Updated 10 days ago