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.
signing.js
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

signing.py
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.
← Previous
Overview
Next →
In-app webhooks