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

# Webhook Security

> Verify webhook signatures to ensure events are from Stable Genius

# Webhook Security

Every webhook request includes a signature so you can verify it originated from Stable Genius and wasn't tampered with in transit.

## Signature Header

Each webhook includes a `X-StableGenius-Signature` header containing an HMAC-SHA256 signature:

```
X-StableGenius-Signature: sha256=a1b2c3d4e5f6...
X-StableGenius-Timestamp: 1711929612
```

## Verification Steps

<Steps>
  <Step title="Extract headers">
    Read the `X-StableGenius-Signature` and `X-StableGenius-Timestamp` headers from the request.
  </Step>

  <Step title="Prepare the signed payload">
    Concatenate the timestamp and the raw request body with a period: `{timestamp}.{body}`
  </Step>

  <Step title="Compute the expected signature">
    HMAC-SHA256 the signed payload using your webhook signing secret (found in the dashboard under **Settings → Webhooks**).
  </Step>

  <Step title="Compare signatures">
    Compare your computed signature with the one in the header. Use a constant-time comparison to prevent timing attacks.
  </Step>

  <Step title="Check timestamp freshness">
    Reject events with timestamps older than 5 minutes to prevent replay attacks.
  </Step>
</Steps>

## Implementation Examples

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(req, signingSecret) {
    const signature = req.headers['x-stablegenius-signature'];
    const timestamp = req.headers['x-stablegenius-timestamp'];
    const body = req.rawBody; // Raw request body as string

    // Check timestamp freshness (5 minute tolerance)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp)) > 300) {
      throw new Error('Webhook timestamp too old');
    }

    // Compute expected signature
    const signedPayload = `${timestamp}.${body}`;
    const expected = 'sha256=' + crypto
      .createHmac('sha256', signingSecret)
      .update(signedPayload)
      .digest('hex');

    // Constant-time comparison
    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      throw new Error('Invalid webhook signature');
    }

    return true;
  }
  ```

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

  def verify_webhook_signature(payload: bytes, signature: str, timestamp: str, signing_secret: str):
      # Check timestamp freshness (5 minute tolerance)
      now = int(time.time())
      if abs(now - int(timestamp)) > 300:
          raise ValueError("Webhook timestamp too old")

      # Compute expected signature
      signed_payload = f"{timestamp}.{payload.decode()}"
      expected = "sha256=" + hmac.new(
          signing_secret.encode(),
          signed_payload.encode(),
          hashlib.sha256
      ).hexdigest()

      # Constant-time comparison
      if not hmac.compare_digest(signature, expected):
          raise ValueError("Invalid webhook signature")

      return True
  ```
</CodeGroup>

## Signing Secret

Your webhook signing secret is available in the [dashboard](https://app.stablegenius.co) under **Settings → Webhooks**. Each webhook endpoint has its own signing secret.

<Warning>
  Treat your webhook signing secret like an API key. Don't commit it to source control. Store it in environment variables or a secrets manager.
</Warning>

## Replay Protection

The timestamp header prevents replay attacks. If an attacker intercepts a webhook payload and resends it later, the timestamp check will reject it. Always verify that the timestamp is within 5 minutes of the current time.

## IP Allowlisting

For additional security, you can restrict your webhook endpoint to only accept requests from Stable Genius IP addresses. Contact us for the current list of egress IPs.
