| Header | Required | Value |
|---|---|---|
X-MG-API-KEY | Yes | Your public key, for example mg_7f82ab91c4d0e5f6 |
X-MG-TIMESTAMP | Yes | Unix time in milliseconds at the moment you sign |
X-MG-RECV-WINDOW | No | How long the request stays valid, in milliseconds. Default 5000, maximum 60000 |
X-MG-SIGNATURE | Yes | The lowercase hex HMAC-SHA256 digest |
Origin | Yes | The single domain your key is bound to, for example https://yourdomain.com |
Content-Type: application/json whenever you send a body, and Idempotency-Key on every POST and DELETE.\n), in exactly this order:HTTP_METHOD
REQUEST_PATH
QUERY_STRING
BODY_HASH
TIMESTAMP
RECV_WINDOW| Field | Definition |
|---|---|
HTTP_METHOD | Uppercase: GET, POST or DELETE |
REQUEST_PATH | The path only. No host, no query string. For example /v1/orders |
QUERY_STRING | The raw query exactly as you send it, without the leading ?. Empty string when there is none |
BODY_HASH | Lowercase hex SHA-256 of the raw request body bytes. With no body, use the SHA-256 of the empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
TIMESTAMP | The X-MG-TIMESTAMP value, copied exactly |
RECV_WINDOW | The X-MG-RECV-WINDOW value, copied exactly. If you do not send the header, this field is the empty string |
X-MG-SIGNATURE = hex( HMAC-SHA256( key = apiSecret, message = canonicalPayload ) )X-MG-RECV-WINDOW means signing an empty field, not signing 5000. The rule is literally "sign what you send".Content-Type: application/json is mandatory when there is a body. The server only reads raw bytes for requests it recognizes as JSON. Without this header your body is never read, the server hashes the empty string, and you get INVALID_SIGNATURE even though your signing code is perfect. This is the most confusing failure in the whole API.INVALID_TIMESTAMP.scheme://host[:port]. The default port is dropped, so https://x.example and https://x.example:443 are the same. Everything else differs: http:// is not https://, and https://api.x.example is not https://x.example.Origin nor Referer is refused. "Cannot tell" never means "allow".localhost is the one deliberate exception, so you can develop locally.The Origin header is not signed, and it is not authentication
It is a misconfiguration control. It stops a key pasted into the wrong project or the wrong deployment from working, and it makes traffic traceable to your site. Any non-browser client can send whatever Originit likes, so it is not a defense against someone who already holds your key. Your signature, the freshness window and the rate limit are what protect the credential.Practically: send it, keep it identical to what you registered, and do not add it to the canonical payload.
401 INVALID_API_KEY403 API_KEY_DISABLED403 INVALID_ORIGIN401 INVALID_TIMESTAMP401 REPLAYED_REQUEST401 INVALID_SIGNATURE403 API_PERMISSION_DENIED403 KYC_REQUIRED|serverTime - timestamp| must be less than or equal to recvWindow, and recvWindow itself may not exceed 60000 ms. Two consequences follow:401 REPLAYED_REQUEST. When you retry a failed request, always sign it again with a fresh timestamp. Never resend the identical signed bytes.REPLAYED_REQUEST rather than INVALID_SIGNATURE. Re-sign every attempt and you will never see this.401 INVALID_SIGNATURE on every call.apiSecret = mgs_EXAMPLEONLYdoNotUse_0123456789abcdefghij
X-MG-TIMESTAMP = 1755500000000
X-MG-RECV-WINDOW = 5000GET /v1/wallets/balance, no query, no bodycanonical = "GET\n/v1/wallets/balance\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n1755500000000\n5000"
signature = 0845ad6a032a297fcc8700949f691ba926da6203dfd4c5edc79249924c277285GET /v1/orders?limit=10canonical = "GET\n/v1/orders\nlimit=10\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n1755500000000\n5000"
signature = 42486da25871ee591aa91ae6d68a27945e6d3da50a6373f590bb4e601a3ecb82POST /v1/orders with a bodybody = {"symbol":"BTC-USDT","side":"BUY","type":"LIMIT","quantity":"0.001","price":"60000"}
bodyHash = ab789dd3cdd8accdd479cf45699c53831a953436d3f018171b6b42de3e76d1a1
canonical = "POST\n/v1/orders\n\nab789dd3cdd8accdd479cf45699c53831a953436d3f018171b6b42de3e76d1a1\n1755500000000\n5000"
signature = 655ad61a4dc2a1c8bf437313deba665a03c044b1698d545dae46a1ae128e37c3Content-Type header is wrong. If A fails, check that you are hashing the empty string for BODY_HASH rather than leaving that field blank.Origin appears in none of these vectors because it is not signed. A client can reproduce all three signatures perfectly and still be refused 403 INVALID_ORIGIN if it forgets the header against the live API.api_secret, and injects all five headers. It skips anything under /v1/market/, because those routes are public.{{variables}} in the path, the query and the body before hashing, so the bytes it signs are the bytes Postman transmits. It also generates a fresh Idempotency-Key on every POST and DELETE.Apidog and other tools: Apidog does not run Postman sandbox scripts identically. If you import this collection there, either port the script into Apidog's own pre-request script feature, or compute the signature yourself using the rules on this page.