Auth
Request signing
All requests use HMAC-SHA256 over a canonical JSON body digest. The algorithm is identical for both endpoints — only the key header name differs.
Signing flow
1
Canonical JSON
Sort all object keys lexicographically at every nesting level. Preserve array order. Stringify each element recursively. This ensures the body hash is deterministic regardless of key insertion order.
2
Body digest
Compute a UTF-8 SHA-256 hex hash of the canonical JSON string:
sha256hex(canonicalJSON(body))3
Signing string
Concatenate trimmed timestamp + newline + body digest:
trim(X-Buzz-Timestamp) + "\n" + sha256hex(canonicalJSON(body))4
HMAC signature
Compute HMAC-SHA256 over the signing string using your campaign secret. Output as lowercase hex:
hex( HMAC-SHA256(secret, signingString) ). The optional prefix v1= is accepted and stripped before comparison.5
Headers to send
Include
X-Buzz-Timestamp (Unix seconds integer), X-Buzz-Signature (the hex HMAC), and the appropriate key header (X-Buzz-Key-Id or X-Buzz-Key).Node.js helpers
Sign the same plain object you pass to
JSON.stringify(). Do not mutate the body after signing, and avoid middleware that alters the body before sending.const crypto = require('crypto');
function canonicalStringify(value) {
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return '[' + value.map(canonicalStringify).join(',') + ']';
const keys = Object.keys(value).sort();
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalStringify(value[k])).join(',') + '}';
}
function bodyDigestHex(body) {
const s = canonicalStringify(body && typeof body === 'object' ? body : {});
return crypto.createHash('sha256').update(s, 'utf8').digest('hex');
}
function signRequest(secret, timestampSec, bodyObject) {
const ts = String(timestampSec).trim();
const signingString = ts + '\n' + bodyDigestHex(bodyObject);
return crypto.createHmac('sha256', secret).update(signingString, 'utf8').digest('hex');
}Python
import hashlib, hmac, json
def canonical(value):
if value is None or not isinstance(value, (dict, list)):
return json.dumps(value, separators=(',', ':'), ensure_ascii=False)
if isinstance(value, list):
return '[' + ','.join(canonical(v) for v in value) + ']'
items = sorted(value.items())
return '{' + ','.join(json.dumps(k) + ':' + canonical(v) for k, v in items) + '}'
def sign(secret: str, ts: int, body) -> str:
digest = hashlib.sha256(canonical(body).encode('utf-8')).hexdigest()
signing_string = f'{ts}\n{digest}'
return hmac.new(secret.encode('utf-8'), signing_string.encode('utf-8'), hashlib.sha256).hexdigest()Debugging signature failures
If you get a 401, the signature debugger shows you exactly which step diverged: canonical JSON, body digest, or HMAC. Paste your secret + body + timestamp and compare byte-for-byte.
Common gotchas:
- Sending timestamp in milliseconds instead of seconds.
- Hashing the un-canonicalized JSON your HTTP client serialized.
- Using uppercase hex in the signature.
- Extra whitespace between the timestamp and newline in the signing string.