Authentication

HMAC-SHA512, nonce, subaccount header, and TOTP for the Bit2Me gateway.

Private endpoints on https://gateway.bit2me.com require an API key and the matching secret. You create both in Get API keys. The secret is shown once; it is what you HMAC with. Never share a key or secret.

There is no sandbox host. Wallet, Pro, Partner, Embed, and gateway all use production URLs (gateway.bit2me.com, embed.bit2me.com). Integration testing uses your live keys with the smallest scope and an IP allowlist.

If requests fail with 401, sync the machine clock with NTP first. The documented nonce window is 5 seconds of clock skew against Bit2Me UTC. Check status.bit2me.com before you assume a platform outage. If this persists, open a support ticket.

Headers

HeaderRequiredDescription
x-api-key / X-API-KEYYes (private)Public API key
x-nonceYes (signed)Unix time in milliseconds (Date.now()). Clock-skew window: 5 seconds
api-signatureYes (signed)HMAC described below
X-SUBACCOUNT-IDWhen acting for an end userSubaccount userId. HTTP names are case-insensitive; samples use this spelling
x-totpSensitive operations6-digit 2FA code
x-totp-typeWith x-totpgauth, sms, or email. Always gauth for subaccounts

Validate the server TLS certificate and abort if validation fails.

HMAC algorithm

  1. Build the message:

    • No body: {nonce}:{path}
    • JSON body: {nonce}:{path}:{compactJson}

    {path} includes the query string, e.g. /v1/trading/wallet/balance?symbols=BTC.

  2. SHA-256 the message (binary digest).

  3. HMAC-SHA512 using your secret, over that digest.

  4. Base64-encode the HMAC. That value is api-signature.

The body used to sign must be the exact request body string:

  • JavaScript: JSON.stringify(body) (no spaces).
  • Python: json.dumps(body, separators=(",", ":")).

Do not mix pretty-printed JSON into the signature. Do not POST {} on no-payload calls: an empty object is still a body and would be signed as {}. That includes POST /v1/signin/apikey when you are not sending userId — omit the body (and sign {nonce}:{path}), or send a real JSON object and sign that object.

const crypto = require("crypto");

function getClientMessageToSign(nonce, url, body) {
  const hasBody = !!body && Object.keys(body).length > 0;
  return hasBody ? `${nonce}:${url}:${JSON.stringify(body)}` : `${nonce}:${url}`;
}

function getMessageSignature(message, secret) {
  const hashDigest = crypto.createHash("sha256").update(message).digest("binary");
  return crypto.createHmac("sha512", secret).update(hashDigest, "binary").digest("base64");
}

const nonce = Date.now();
const path = "/v1/trading/wallet/balance";
const signature = getMessageSignature(getClientMessageToSign(nonce, path), process.env.BIT2ME_API_SECRET);
import hashlib, hmac, json, base64

def sign(nonce: str, path: str, secret: str, body=None) -> str:
    if body is None:
        message = f"{nonce}:{path}".encode()
    else:
        compact = json.dumps(body, separators=(",", ":"))
        message = f"{nonce}:{path}:{compact}".encode()
    digest = hashlib.sha256(message).digest()
    return base64.b64encode(hmac.new(secret.encode(), digest, hashlib.sha512).digest()).decode()
# Replace SIGNATURE after computing HMAC locally.
curl -sS "https://gateway.bit2me.com/v1/trading/wallet/balance" \
  -H "x-api-key: $BIT2ME_API_KEY" \
  -H "api-signature: $SIGNATURE" \
  -H "x-nonce: $NONCE"

Official Node samples for REST and WebSocket signing: bit2me-devs/trading-spot-samples (node/auth). The older bit2me-api-node-tool repository is discontinued — do not treat it as an SDK.

Known-answer fixture

Use this to unit-test your signer. If your output does not match, fix concatenation or encoding before you hit production.

The fixture nonce is HMAC-only. Live requests must send milliseconds (Date.now()), not this value.

FieldValue
Secretexample_secret_do_not_use_in_production
Nonce1687155308
Path/v1/trading/wallet/balance
Body(none)
Message1687155308:/v1/trading/wallet/balance
api-signatureIgazRn9MPHcJjAfhrVHzI46U9iIDb7qbXYcTIN+9vrktJ6M4rET7WH+wasF+/8Thp9tzjkWaFCL8ASSzlY4D8g==

POST example (JSON.stringify / Python separators=(",", ":") — no spaces):

FieldValue
Path/v1/trading/order
Body{"side":"buy","symbol":"BTC/EUR","price":"50000","amount":"0.01","orderType":"limit"}
Message1687155308:/v1/trading/order:{"side":"buy","symbol":"BTC/EUR","price":"50000","amount":"0.01","orderType":"limit"}
api-signatureTnwJpXmBNukFWneZrjQk+SCRk9Ck13B2oK7kSpnKEpdQajo9l9pn5sUnwIQcsguFmFBHDGwS2aOjXlMnKSIyXQ==

Which operations are signed

OpenAPI marks private operations with ApiKeyAuth. That is the HMAC pair (x-api-key + api-signature + x-nonce), not “API key only”.

SignedNot signed
Every private Wallet / Partner / Pro REST call, including POST /v1/signin/apikeyPublic Pro market data (GET /v2/trading/market-config, candles, book, tickers)
Widget session: POST /v1/signin/apikey with { "userId": "<subaccount>" }POST /v1/signin/embed — different contract (token exchange). Do not send your API key there

The REST playground signs these for you, see Playground. A working first call: Quickstart (GET /v1/wallet/pocket).

Invalid credentials (403)

Some gateway routes, including HMAC sign-in and Pro Wallet funding, return 403 invalid credentials for a bad key or a bad signature. The body does not tell you which. Same response for an invented key.

Before you open a ticket:

  1. Confirm you are on https://gateway.bit2me.com (no sandbox URL).
  2. You are calling POST /v1/signin/apikey, not /v1/signin/embed.
  3. You did not POST {} unless that object is also in the HMAC message.
  4. The secret matches the key (copy both from the dashboard; trim .env whitespace).
  5. Pass the known-answer fixture. If the fixture fails, do not hit production.
  6. x-nonce is milliseconds (Date.now()), within 5 seconds of UTC.

401 is still the usual HMAC / clock failure on most Wallet routes. See Fix HMAC 401 and Errors.

Pair formats

Do not mix these on the same call:

SurfaceExample
Pro REST / Pro WebSocketBTC/EUR
MCP toolsBTC-EUR
Wallet buy / sellpair: "BTC/EUR", or source and destination pocket UUIDs

TOTP

Some operations require 2FA:

  • x-totp — code
  • x-totp-type — gauth | sms | email

For subaccount operations, x-totp-type is always gauth.

Enroll subaccount 2FA with POST /v1/account/subaccount/sfa (subaccountUserId in JSON) then PUT /v1/account/subaccount/sfa (subaccountUserId + totp in JSON). Parent enroll is GET /v3/signin/sfa, then POST /v1/account/settings/sfa with the received code in x-totp / x-totp-type, then PUT /v1/account/settings/sfa. Withdrawal 2FA still uses x-totp headers. Details: Two-factor authentication.

Playground

Every REST reference page can send the request. Open Authorization, enter your API key and secret once, and every endpoint uses them until you reload the tab:

  • The request is signed in your browser. The secret is kept in memory only and is never stored or sent.
  • It calls the gateway shown in Server URL with your real account (production, on the public site). Prefer a Read key. Write requests (POST, PUT, PATCH, DELETE) stay blocked until you tick Allow write requests.
  • Requests go straight from your browser to that gateway. It is fixed per site and cannot be changed.

In your own integration, keep signing on your backend: never ship the secret to a browser.

Key scopes

When you create a key you can request:

  • Read
  • Trade — create or cancel orders
  • Withdraw — withdrawals that bypass two-factor authentication

Prefer IP allowlists on Trade and Withdraw keys. Do not issue a Withdraw-scoped key for subaccount remittance if you need TOTP on every send — that scope is documented as bypassing two-factor authentication.

Rotate a key

  1. Open Get API keys.
  2. Create a new key (and secret). Store the secret outside the app logs.
  3. Point your backend at the new pair.
  4. Revoke the old key in the same dashboard.

There is no published dual-key grace window. Treat revoke as immediate. If a secret leaked, revoke first, then rotate.

Gateway WebSocket

REST HMAC does not sign the notification socket. After open on wss://ws.bit2me.com/v1:

  • JWT: POST /v1/signin/apikey (HMAC) → send { "type": "authenticate", "payload": { "token": "<accessToken.token>" } }
  • Or API key: { "type": "authenticate", "payload": { "apikey": ["<api-key>"] } }

Failed auth closes the socket with 4000. Full examples: Gateway notifications.

Spot Trading API uses a different URL and envelope (event: authenticate + REST token, 1 minute). See Pro WebSocket.

On this page