Skip to main content
Every webhook Baanx sends is signed with HMAC-SHA256. Verifying this signature ensures the request is genuine and hasn’t been tampered with.

Your API Key

Each webhook endpoint is assigned a unique API key by Baanx. This key is the shared secret used to generate and verify signatures.
Store your API key securely in a secrets manager or environment variable. Never log it, commit it to source control, or expose it in client-side code.
Key properties:
  • Generated and managed by Baanx — not client-generated
  • Unique per webhook endpoint
  • Can be rotated on request (or via the Rotate Key API)
  • Custom keys can be configured in special cases
Example key format:
whk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Request Headers

Every webhook request includes these headers:
HeaderDescription
Content-TypeAlways application/json
X-TimestampUnix timestamp (seconds, UTC) when the request was sent
X-SignatureHMAC-SHA256 signature as a lowercase hex string

How the Signature is Calculated

Baanx constructs the signature as follows:
signature_payload = X-Timestamp + "." + JSON.stringify(body)
signature         = HMAC-SHA256(api_key, signature_payload)
X-Signature       = hex_encode(signature)  // lowercase
To verify a request, you reproduce this calculation using the same inputs and compare the result to the X-Signature header.
Use your raw request body (as a string, before JSON parsing) when computing the signature. Parsing and re-serialising the body can change whitespace and key ordering, causing verification to fail.

Verification Steps

1

Extract headers

Read X-Timestamp and X-Signature from the incoming request headers.
2

Check the timestamp

Reject the request if the timestamp is more than 300 seconds (5 minutes) away from the current server time. This prevents replay attacks.
3

Build the signature payload

Concatenate the timestamp, a literal ., and the raw JSON body string:
payload = timestamp + "." + rawBody
4

Compute the expected signature

Calculate HMAC-SHA256 over the payload using your API key, then hex-encode the result (lowercase).
5

Compare signatures

Use a timing-safe comparison to compare the computed signature against X-Signature. Never use === — it is vulnerable to timing attacks.

Code Examples

import crypto from 'crypto';

function verifyWebhookSignature(
  rawBody: string,
  timestamp: string,
  signature: string,
  apiKey: string
): boolean {
  // Reject stale requests (> 5 minutes old)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    return false;
  }

  const payload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', apiKey)
    .update(payload)
    .digest('hex');

  // Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express.js usage — use raw body middleware
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody  = req.body.toString('utf8');
  const timestamp = req.headers['x-timestamp'] as string;
  const signature = req.headers['x-signature'] as string;

  if (!verifyWebhookSignature(rawBody, timestamp, signature, process.env.WEBHOOK_API_KEY!)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(rawBody);
  // Process event...
  res.status(204).send();
});

Replay Protection

The X-Timestamp header protects against replay attacks — where an attacker captures a legitimate webhook request and re-sends it later. Your endpoint should reject any request where the timestamp differs from the current server time by more than 5 minutes (300 seconds):
| now - X-Timestamp | > 300  →  reject with 401
Ensure your server clock is synchronised (e.g., via NTP) so legitimate requests are not incorrectly rejected due to clock skew.

Common Verification Mistakes

If you parse the raw JSON into an object and then re-serialise it to compute the signature, the result may differ from the original raw string (different whitespace, key ordering). Always use the raw request body string from the HTTP request.
Using === to compare signatures leaks information through response time differences. Always use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python).
Without timestamp validation, a captured webhook request could be replayed indefinitely. Always reject requests where | now - timestamp | > 300.

Next Steps