Skip to content

Authentication ​

Every /v1/** endpoint is authenticated with an HMAC-SHA256 request signature. There are no sessions or bearer tokens.

Headers ​

HeaderDescription
X-Api-KeyAPI key
X-TimestampCurrent time in Unix milliseconds. Rejected if more than ±30 s from server time (REQUEST_EXPIRED)
X-NonceRandom string of 16–64 characters; must not repeat for the same key within 5 minutes (NONCE_REPLAYED). Hex of 16 random bytes is recommended
X-SignatureThe signature (below), lowercase hex
Content-Typeapplication/json when there is a body

String to sign ​

Join these 6 parts with \n (line feed, 0x0A), without a trailing newline:

text
METHOD
PATH
CANONICAL_QUERY
TIMESTAMP
NONCE
hex(sha256(BODY))
PartRule
METHODUppercase, e.g. GET, POST
PATHURL path without host or query string, exactly as sent (percent-encoded), e.g. /v1/wallets/u_10001/addresses
CANONICAL_QUERYSort parameters by key, then by value for repeated keys; RFC 3986-encode keys and values (only A-Z a-z 0-9 - _ . ~ stay literal, space is %20, not +); join with &. Empty string when there is no query
TIMESTAMPSame as X-Timestamp
NONCESame as X-Nonce
hex(sha256(BODY))Lowercase hex SHA-256 of the raw body bytes. Without a body (e.g. GET) it is the hash of the empty string, e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
text
X-Signature = hex(HMAC-SHA256(API secret, string to sign))

The two most common mistakes

  1. Sign the exact bytes you send. Serialize the JSON to a string once, hash that string and send that same string as the body; don't sign one serialization and let your HTTP library produce another.
  2. Send the canonical query string itself as the URL query, so what you sign and what you send can never differ in order or encoding.

Worked example ​

API secret test-secret, X-Timestamp: 1762650000000, X-Nonce: 0123456789abcdef0123:

http
GET /v1/deposits?user_id=u%201&chain_code=TRON&status=PENDING&status=CONFIRMED&note=a%2Bb%21

Canonical query:

text
chain_code=TRON&note=a%2Bb%21&status=CONFIRMED&status=PENDING&user_id=u%201

String to sign:

text
GET
/v1/deposits
chain_code=TRON&note=a%2Bb%21&status=CONFIRMED&status=PENDING&user_id=u%201
1762650000000
0123456789abcdef0123
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Signature: d474bb4c8ff89a37f214d8c39c3d17a502d54b64c1822024a86480c248bcbc86

With a body (POST /v1/withdraws, same timestamp and nonce):

json
{"user_id":"u1","chain_code":"TRON","token_code":"USDT","to_address":"TXYZ","amount":"12.5","biz_id":"order-1"}

Signature: 381b9e20fc9b2e0cd97ecb003686acd472db596757b674e56df9e038d708867c

Use these two vectors to test your implementation.

Scopes and IP allowlist ​

Each API key has scopes; the scope each endpoint needs is listed in the API reference:

ScopeAllows
readAll queries, resending events
depositCreating deposit addresses, rescans
withdrawCreating withdrawals

A missing scope returns PERMISSION_DENIED; a source IP outside the key's allowlist returns IP_NOT_ALLOWED. Use a dedicated withdraw key for withdrawals and allow only your servers' egress IPs.

Authentication errors ​

reasonHTTPCause
MERCHANT_AUTH_FAILED401Missing signature headers, unknown or disabled key, or disabled merchant
SIGNATURE_INVALID401Wrong signature — compare your string to sign with the example above, part by part
REQUEST_EXPIRED401Timestamp missing, not in milliseconds, or more than 30 s off (check NTP)
NONCE_REPLAYED401Nonce reused, or not 16–64 characters long
IP_NOT_ALLOWED403Source IP not in the allowlist
PERMISSION_DENIED403The key lacks the scope this endpoint requires