The WebSocket API uses the same authentication mechanism as the REST (Web) API: every message you send must be signed. Unlike a public feed, there is no separate "log in" step — each subscribe, info, and ping message carries your credentials and a freshly computed signature. An unsigned or incorrectly signed message is rejected.
Credentials you need
You will use three values from your zerohash API key (visible in the dashboard when logged in):
| Credential | Placeholder | Where it goes | Notes |
|---|---|---|---|
| Public key | API_PUBLIC_KEY | The key field of every message | Safe to send |
| Passphrase | API_PASSPHRASE | The passphrase field of every message | Safe to send |
| Private / secret key | API_PRIVATE_KEY | Used to compute the signature only | Base64-encoded, and never sent over the wire |
How the signature works
The signature is an HMAC-SHA256 hash of a payload string, keyed by your base64-decoded private key, with the result base64-encoded.
Step 1 — Build the payload string
Concatenate these four components in this exact order (no separators):
payload = timestamp + method + route + json_body
| Component | Value |
|---|---|
timestamp | Current Unix epoch time in seconds, as a string (e.g. "1720000000") |
method | The literal string "POST" |
route | The literal string "/" |
json_body | The compact JSON string of the message body — no extra whitespace |
Step 2 — Compute the HMAC
- UTF-8 encode the payload string to bytes.
- Base64-decode your
API_PRIVATE_KEY— this is the HMAC key. - Compute
HMAC-SHA256(key, payload_bytes). - Base64-encode the resulting digest → this is your
signature.
The values you sign must exactly match the values you send.
- Use the same
timestampin the payload and in the message'stimestampfield.- The
json_bodyyou sign must be byte-for-byte identical to thebodyyou send — same key order, same whitespace. A different serialization produces a different signature and the request will be rejected.
Websocket Authentication
The message envelope
Every message you send — regardless of feed — shares the same envelope:
| messageType | string | The action: subscribe, info, or ping |
| topic | string | balances, prices, or available-symbols (context-dependent) |
| body | string | Payload / filter object (required; may be on the balances feed) |
| key | string | Your API_PUBLIC_KEY |
| passphrase | string | Your API_PASSPHRASE |
| timestamp | string | Unix epoch seconds — the same value used to compute the signature |
| signature | string | Base64-encoded HMAC-SHA256 signature |
Example signed message:
{
"messageType": "subscribe",
"topic": "balances",
"key": "API_PUBLIC_KEY",
"passphrase": "API_PASSPHRASE",
"timestamp": "1720000000",
"signature": "base64_encoded_hmac",
"body": { "filter": {} }
}| Symptom | Likely cause |
|---|---|
| Message rejected / auth error | json_body used for signing differs from the body sent (whitespace or key order), or timestamp mismatch between payload and field |
| Intermittent rejections | Clock drift — ensure timestamp is current system time in seconds, not milliseconds |
| Works in REST but not WS | Remember the private key must be base64-decoded before use as the HMAC key |
Next steps
- Initial Subscription (https://docs.zerohash.com/reference/initial-subscription) — start the balances feed
- Subscribing to a Symbol (https://docs.zerohash.com/reference/subscribing-to-a-symbol) — start the prices feed
- Maintaining Subscription (https://docs.zerohash.com/reference/maintaining-subscription) — the 20-second heartbeat