Skip to main content
Every request — the ones you send to the Customer API and the ones we send to your wallet callbacks — carries an HMAC-SHA512 signature.

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 three parts are joined with a vertical bar |. The separator after timestamp is always present, even when data is empty.
Sign the bytes you actually send. Serialize your JSON once, sign that buffer, send that buffer. Re-serializing between signing and sending — a pretty-printer, a middleware, a different key order — changes the bytes and breaks the signature. The same applies to the query string: sign the encoded string you put on the wire, not a re-encoding of it.

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 key secretKey and is reproducible with the snippets on this page.
Use this one first. If your implementation reproduces it, the algorithm is right.

Reference implementations

Each sign 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.
Add the IP allowlist in front of this. Signature verification proves the message is authentic; the allowlist keeps unauthenticated traffic away from your wallet in the first place.

Troubleshooting

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.
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.
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.
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.
The secret key never leaves your server. It is not a bearer token: never place it in a URL, a browser, a mobile app, a log line or a stack trace.