Authentication
Every request to /v1/* is signed with HMAC-SHA256 using your API secret. Keep the secret on your server.
Required headers
| Header | Value |
|---|---|
X-Key-Id | Your API key id, for example ak_3f9c… |
X-Timestamp | Current Unix time in milliseconds (13 digits) |
X-Signature | Hex-encoded HMAC-SHA256 of the string to sign (64 characters) |
Content-Type | application/json when the request has a body |
String to sign
Join four values with a newline (\n):
{X-Timestamp}
{METHOD}
{path including query string}
{raw request body}- METHOD is uppercase:
GETorPOST. - Path is exactly what you send, including the query string, for example
/v1/quote?chain=tron&token=USDT&direction=in&amount=50000000. - Body is the exact JSON bytes you send. For
GET, use an empty string (the string to sign then ends with a newline). - Key is your API secret exactly as issued, as UTF-8 text. Do not base64-decode it.
Signing a request
import crypto from "node:crypto"; const BASE = process.env.ICN_BASE_URL; // given to you at onboarding const KEY_ID = process.env.ICN_KEY_ID; // ak_... const SECRET = process.env.ICN_SECRET; // use the string exactly as issued export async function icn(method, path, body) { const raw = body === undefined ? "" : JSON.stringify(body); const ts = Date.now().toString(); // milliseconds const signature = crypto .createHmac("sha256", SECRET) .update(`${ts}\n${method}\n${path}\n${raw}`) .digest("hex"); const res = await fetch(BASE + path, { method, headers: { "X-Key-Id": KEY_ID, "X-Timestamp": ts, "X-Signature": signature, ...(raw && { "Content-Type": "application/json" }), }, body: raw || undefined, // send exactly the bytes you signed }); return { status: res.status, body: await res.json() }; }
import hashlib, hmac, json, os, time import requests BASE = os.environ["ICN_BASE_URL"] KEY_ID = os.environ["ICN_KEY_ID"] # ak_... SECRET = os.environ["ICN_SECRET"] # use the string exactly as issued def icn(method, path, body=None): raw = "" if body is None else json.dumps(body, separators=(",", ":")) ts = str(int(time.time() * 1000)) # milliseconds msg = f"{ts}\n{method}\n{path}\n{raw}".encode() signature = hmac.new(SECRET.encode(), msg, hashlib.sha256).hexdigest() headers = {"X-Key-Id": KEY_ID, "X-Timestamp": ts, "X-Signature": signature} if raw: headers["Content-Type"] = "application/json" res = requests.request(method, BASE + path, data=raw.encode() or None, headers=headers) return res.status_code, res.json()
# Sign: timestamp \n METHOD \n path(+query) \n body TS=$(date +%s%3N) BODY='{"chain":"tron","player_ref":"player_1024"}' SIG=$(printf '%s\n%s\n%s\n%s' "$TS" POST /v1/addresses "$BODY" \ | openssl dgst -sha256 -hmac "$ICN_SECRET" -hex | sed 's/^.* //') curl -X POST "$ICN_BASE_URL/v1/addresses" \ -H "X-Key-Id: $ICN_KEY_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" \ -H "Content-Type: application/json" \ -d "$BODY"
Rules
- The timestamp must be within 5 minutes of our clock. Keep your servers on NTP.
- Each signature can be used once. To retry, sign again with a new timestamp.
- Requests are accepted only from the IP addresses (CIDRs) registered for your key.
- Request bodies are limited to 64 KB.
Every authentication failure returns the same response, so it never reveals which check failed:
401 Unauthorized
{
"error": { "code": "unauthorized", "message": "authentication failed" }
}If you get a 401, check in this order: clock drift, the exact path and body you signed, the secret, and your server's outgoing IP.