Skip to content

Message Integrity (HMAC-SHA256)

Message Integrity (HMAC-SHA256)

Every wallet callback TS sends (/getbalance, /opentrade, /closetrade) is signed. You verify the signature; if it fails, you reject the request. The only unsigned call is /authenticate — there the single-use launch token is the credential.

Wire format

POST /opentrade HTTP/1.1
Content-Type: application/json
X-Sig-Version: v2
X-Timestamp: 1715630400
X-Nonce: 3a7c9e1b4f2d8a5e0c1b9d6f3a8e5c2b
X-Signature: 8f3a4d…a91d

{"token":"…","amount":"10","currency":"USD","externalTradeType":"tap","externalTradeId":"12345","data":[…]}

The signature lives in headers; the body carries no integrity field.

Headers

HeaderFormatMeaning
X-Sig-Versionliteral string v2Opaque version tag. Future scheme bumps will change this.
X-Timestampunix seconds, ASCII decimalWhen the request was signed. You reject if more than ±60 s from your clock.
X-Nonce32 lowercase hex charsbin2hex(random_bytes(16)). Per-request, unique. You store it ≥180 s and reject duplicates.
X-Signaturelowercase hex (64 chars)hex(HMAC-SHA256(secret, canonical))

Canonical string

Five lines, newline-separated:

METHOD\nPATH\nTIMESTAMP\nNONCE\nSHA256_hex(BODY)

Concrete for the example above:

POST
/opentrade
1715630400
3a7c9e1b4f2d8a5e0c1b9d6f3a8e5c2b
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  • METHOD — uppercase HTTP method (POST).
  • PATH — request path only (no query string, no host).
  • TIMESTAMP — same value as X-Timestamp.
  • NONCE — same value as X-Nonce.
  • SHA256_hex(BODY) — lowercase hex SHA-256 of the raw bytes the partner received.

Critical: hash the raw body bytes

Do NOT decode and re-encode the JSON before hashing. json_encode output differs across runtimes (float precision, unicode escape flags, key order under sorting helpers) — any drift breaks the signature silently.

  • PHP: file_get_contents('php://input')
  • Node (Express): grab req body via express.raw({type: '*/*'}) before any JSON middleware
  • Python (Flask): request.get_data() (cache=True so subsequent reads work)
  • Go: read req.Body and stash a copy before any decoder consumes it

Replay protection

ParameterValueNotes
Timestamp skew±60 sReject if abs(now − X-Timestamp) > 60. NTP-sync both sides.
Nonce TTL≥180 sMust be ≥ 2× skew so replayed timestamps can't survive their nonce expiring.
Nonce bit-width128 bits32 hex chars.
Nonce storeRedis (recommended)Per-partner; isolated per shard if you shard.

Sample verifier (Node.js)

js
const crypto = require('crypto')

function verify(req, secret) {
 const v = req.headers['x-sig-version']
 const ts = parseInt(req.headers['x-timestamp'], 10)
 const nonce = req.headers['x-nonce']
 const sig = req.headers['x-signature']
 if (v !== 'v2') throw new Error('unsupported version')
 if (Math.abs(Date.now()/1000 - ts) > 60) throw new Error('skew')
 if (!/^[0-9a-f]{32}$/.test(nonce)) throw new Error('bad nonce')
 if (await nonceWasSeen(nonce)) throw new Error('replay')
 await rememberNonce(nonce, 180)

 const bodyHash = crypto.createHash('sha256').update(req.rawBody).digest('hex')
 const canonical = `POST\n${req.path}\n${ts}\n${nonce}\n${bodyHash}`
 const expected = crypto.createHmac('sha256', secret).update(canonical).digest('hex')
 if (!crypto.timingSafeEqual(Buffer.from(sig,'hex'), Buffer.from(expected,'hex'))) {
 throw new Error('bad signature')
 }
}