The algorithm
X-Zd-Signature = hex( HMAC-SHA512( apiPath + "|" + timestamp + "|" + data, secretKey ) )The three parts
string
required
The path of the request, with no host, no query string and no fragment. For
https://api.zerodash.studio/api/v1/games?x=1 that is /api/v1/games.string
required
Unix time in milliseconds. The exact same string goes into the
X-Zd-Timestamp header — sign what you send, byte for byte. A timestamp more than 5 minutes old is rejected.string
required
Depends on the request:
|. The separator after timestamp is always present, even when data is empty.
Headers
application/json is the only accepted content type for request bodies. Nothing else will be processed.
Worked examples
Every value below was produced with the secret keysecretKey and is reproducible with the snippets on this page.
- Canonical vector
- GET with query
- POST with body
- GET without query
Reference implementations
Eachsign function below returns the X-Zd-Signature value. The same function verifies inbound callbacks: recompute over the received path, timestamp and body, then compare in constant time.
Verifying inbound callbacks
Do this before parsing the body or touching the wallet.Read the raw body
Capture the untouched bytes. Most frameworks let a JSON body parser consume the stream first — if yours does, buffer the raw payload before the parser runs, or you will verify a re-serialization and fail every time.
Check the timestamp
Reject anything where
|now − X-Zd-Timestamp| > 5 minutes. This is what stops a captured request from being replayed later.Recompute and compare
Rebuild
path|timestamp|data with the path as you registered it — including any prefix such as /zerodash/v1 — and compare against X-Zd-Signature in constant time.Reject with 401
On any mismatch, stop. Do not create a transaction, do not move money.
Troubleshooting
401 on every request
401 on every request
Compare the exact string you signed against the one on the wire. The usual causes, in order of frequency: the host slipped into
apiPath; the query string still has its ?; the trailing | was dropped when data is empty; the body was re-serialized after signing; the timestamp you signed differs from the header.Works on GET, fails on POST
Works on GET, fails on POST
Your HTTP client is serializing the object itself instead of sending the buffer you signed. Build the body string once, sign it, then send it as a raw string or byte array with
Content-Type: application/json.Works locally, fails behind a proxy
Works locally, fails behind a proxy
A proxy or gateway is rewriting the path (stripping a prefix, adding a trailing slash) or the body (re-encoding, gzip). Sign the path as the origin server sees it, and verify the callback path as you registered it with Zero-Dash.
Intermittent 401s
Intermittent 401s
Clock drift. Your timestamp must be within 5 minutes of ours. Run NTP; a container with a drifting clock will fail a small, growing fraction of requests.