> ## Documentation Index
> Fetch the complete documentation index at: https://docs.klara-ai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Signatures

> Verify that webhook payloads are genuinely from Klara using HMAC-SHA256 signatures

Every webhook request includes two headers for signature verification:

| Header              | Description                                                  |
| ------------------- | ------------------------------------------------------------ |
| `X-Klara-Signature` | `sha256=<hex-digest>` — HMAC-SHA256 signature of the payload |
| `X-Klara-Timestamp` | Unix timestamp (seconds) when the webhook was sent           |

## Verification steps

1. Extract the timestamp and signature from headers
2. Reject timestamps older than 5 minutes (replay protection)
3. Compute `HMAC-SHA256(secret, "{timestamp}.{raw_body}")`
4. Compare the computed signature with the received one using constant-time comparison

<Warning>
  Always use a **constant-time comparison** function (e.g., `crypto.timingSafeEqual` in Node.js) to prevent timing attacks.
</Warning>

## Implementation examples

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from 'crypto';

  const TIMESTAMP_TOLERANCE = 300; // 5 minutes in seconds

  function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
    const timestamp = parseInt(timestampHeader, 10);

    // 1. Reject stale timestamps
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - timestamp) > TIMESTAMP_TOLERANCE) {
      throw new Error('Webhook timestamp too old');
    }

    // 2. Compute expected signature
    const signedContent = `${timestamp}.${rawBody}`;
    const expected = crypto
      .createHmac('sha256', secret)
      .update(signedContent)
      .digest('hex');

    // 3. Constant-time comparison
    const received = signatureHeader.replace('sha256=', '');
    const isValid = crypto.timingSafeEqual(
      Buffer.from(received),
      Buffer.from(expected)
    );

    if (!isValid) {
      throw new Error('Invalid webhook signature');
    }

    return true;
  }

  // Express example
  app.post('/webhooks/klara', express.raw({ type: 'application/json' }), (req, res) => {
    try {
      verifyWebhookSignature(
        req.body.toString(),
        req.headers['x-klara-signature'],
        req.headers['x-klara-timestamp'],
        process.env.KLARA_WEBHOOK_SECRET
      );

      const event = JSON.parse(req.body);
      // Process event...

      res.status(200).send('OK');
    } catch (err) {
      res.status(401).send('Invalid signature');
    }
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  TIMESTAMP_TOLERANCE = 300  # 5 minutes

  def verify_webhook_signature(raw_body: bytes, signature_header: str, timestamp_header: str, secret: str) -> bool:
      timestamp = int(timestamp_header)

      # 1. Reject stale timestamps
      now = int(time.time())
      if abs(now - timestamp) > TIMESTAMP_TOLERANCE:
          raise ValueError("Webhook timestamp too old")

      # 2. Compute expected signature
      signed_content = f"{timestamp}.{raw_body.decode()}"
      expected = hmac.new(
          secret.encode(),
          signed_content.encode(),
          hashlib.sha256
      ).hexdigest()

      # 3. Constant-time comparison
      received = signature_header.replace("sha256=", "")
      if not hmac.compare_digest(received, expected):
          raise ValueError("Invalid webhook signature")

      return True

  # Flask example
  @app.route("/webhooks/klara", methods=["POST"])
  def handle_webhook():
      try:
          verify_webhook_signature(
              request.get_data(),
              request.headers["X-Klara-Signature"],
              request.headers["X-Klara-Timestamp"],
              os.environ["KLARA_WEBHOOK_SECRET"],
          )
      except ValueError:
          return "Invalid signature", 401

      event = request.get_json()
      # Process event...
      return "OK", 200
  ```

  ```ruby Ruby theme={null}
  require 'openssl'

  TIMESTAMP_TOLERANCE = 300 # 5 minutes

  def verify_webhook_signature(raw_body, signature_header, timestamp_header, secret)
    timestamp = timestamp_header.to_i

    # 1. Reject stale timestamps
    now = Time.now.to_i
    raise "Webhook timestamp too old" if (now - timestamp).abs > TIMESTAMP_TOLERANCE

    # 2. Compute expected signature
    signed_content = "#{timestamp}.#{raw_body}"
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_content)

    # 3. Constant-time comparison
    received = signature_header.sub("sha256=", "")
    raise "Invalid webhook signature" unless Rack::Utils.secure_compare(received, expected)

    true
  end
  ```
</CodeGroup>

<Tip>
  Make sure you use the **raw request body** (not a parsed/re-serialized version) when computing the signature. Re-serializing JSON can change key ordering or whitespace, which will cause verification to fail.
</Tip>

## Testing signatures

Use the **Send test** button on the webhook settings page to send a test event to your endpoint. Check that your signature verification passes before subscribing to real events.
