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

# Signature Verification

> Verify webhook signatures to ensure requests are from Monk

# Signature Verification

Monk signs all webhook payloads so you can verify they originated from us. Always verify signatures before processing webhook data.

## How Signatures Work

Each webhook includes an `X-Monk-Signature` header with this format:

```
t=1706540400,v1=5d41402abc4b2a76b9719d911017c592
```

| Part | Description                                               |
| ---- | --------------------------------------------------------- |
| `t`  | Unix timestamp (seconds) when the signature was generated |
| `v1` | HMAC-SHA256 signature                                     |

The signature is computed over:

```
{timestamp}.{JSON payload}
```

## Verification Steps

1. Extract `t` (timestamp) and `v1` (signature) from the header
2. Check that the timestamp is within your tolerance window (e.g., 5 minutes)
3. Compute the expected signature: `HMAC-SHA256(secret, "{t}.{payload}")`
4. Compare signatures using constant-time comparison

<Warning>
  **Prevent replay attacks**: Reject webhooks where the timestamp (`t`) is older
  than your tolerance window (recommended: 300 seconds).
</Warning>

## Code Examples

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

  function verifyWebhookSignature(
    payload, // Raw request body as string
    signatureHeader, // X-Monk-Signature header value
    secret, // Your webhook signing secret
    toleranceSeconds = 300
  ) {
    // Parse the signature header
    const parts = signatureHeader.split(',');
    const timestamp = parseInt(
      parts.find(p => p.startsWith('t='))?.slice(2) || '0'
    );
    const signature = parts.find(p => p.startsWith('v1='))?.slice(3) || '';

    // Check timestamp tolerance (prevent replay attacks)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - timestamp) > toleranceSeconds) {
      throw new Error('Webhook timestamp outside tolerance window');
    }

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

    // Constant-time comparison
    const signatureBuffer = Buffer.from(signature, 'utf8');
    const expectedBuffer = Buffer.from(expected, 'utf8');

    if (signatureBuffer.length !== expectedBuffer.length) {
      return false;
    }

    return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
  }

  // Usage with Express.js
  app.post(
    '/webhooks/monk',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      const payload = req.body.toString();
      const signature = req.headers['x-monk-signature'];

      try {
        if (
          !verifyWebhookSignature(
            payload,
            signature,
            process.env.MONK_WEBHOOK_SECRET
          )
        ) {
          return res.status(401).send('Invalid signature');
        }

        const data = JSON.parse(payload);
        const event = req.headers['x-monk-event'];

        // Process the webhook
        console.log(`Received ${event}:`, data);

        res.status(200).send('OK');
      } catch (err) {
        console.error('Webhook error:', err.message);
        res.status(400).send('Webhook error');
      }
    }
  );
  ```

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

  def verify_webhook_signature(
      payload: str,           # Raw request body
      signature_header: str,  # X-Monk-Signature header
      secret: str,            # Your webhook signing secret
      tolerance_seconds: int = 300
  ) -> bool:
      # Parse the signature header
      parts = dict(p.split('=', 1) for p in signature_header.split(','))
      timestamp = int(parts.get('t', '0'))
      signature = parts.get('v1', '')

      # Check timestamp tolerance
      now = int(time.time())
      if abs(now - timestamp) > tolerance_seconds:
          raise ValueError('Webhook timestamp outside tolerance window')

      # Compute expected signature
      signed_payload = f'{timestamp}.{payload}'
      expected = hmac.new(
          secret.encode('utf-8'),
          signed_payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      # Constant-time comparison
      return hmac.compare_digest(signature, expected)


  # Usage with Flask
  from flask import Flask, request

  app = Flask(__name__)

  @app.route('/webhooks/monk', methods=['POST'])
  def handle_webhook():
      payload = request.get_data(as_text=True)
      signature = request.headers.get('X-Monk-Signature', '')

      try:
          if not verify_webhook_signature(
              payload,
              signature,
              os.environ['MONK_WEBHOOK_SECRET']
          ):
              return 'Invalid signature', 401

          data = request.json
          event = request.headers.get('X-Monk-Event')

          # Process the webhook
          print(f'Received {event}:', data)

          return 'OK', 200
      except Exception as e:
          print(f'Webhook error: {e}')
          return 'Webhook error', 400
  ```

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

  def verify_webhook_signature(payload, signature_header, secret, tolerance_seconds = 300)
    # Parse the signature header
    parts = signature_header.split(',').map { |p| p.split('=', 2) }.to_h
    timestamp = parts['t'].to_i
    signature = parts['v1'] || ''

    # Check timestamp tolerance
    now = Time.now.to_i
    if (now - timestamp).abs > tolerance_seconds
      raise 'Webhook timestamp outside tolerance window'
    end

    # Compute expected signature
    signed_payload = "#{timestamp}.#{payload}"
    expected = OpenSSL::HMAC.hexdigest('sha256', secret, signed_payload)

    # Constant-time comparison
    ActiveSupport::SecurityUtils.secure_compare(signature, expected)
  end

  # Usage with Sinatra
  require 'sinatra'

  post '/webhooks/monk' do
    payload = request.body.read
    signature = request.env['HTTP_X_MONK_SIGNATURE']

    begin
      unless verify_webhook_signature(payload, signature, ENV['MONK_WEBHOOK_SECRET'])
        halt 401, 'Invalid signature'
      end

      data = JSON.parse(payload)
      event = request.env['HTTP_X_MONK_EVENT']

      # Process the webhook
      puts "Received #{event}: #{data}"

      status 200
      'OK'
    rescue => e
      puts "Webhook error: #{e.message}"
      halt 400, 'Webhook error'
    end
  end
  ```
</CodeGroup>

## Common Errors

### Invalid Signature

The computed signature doesn't match. Common causes:

* Using the wrong signing secret (each endpoint has a unique secret)
* Parsing or modifying the payload before verification
* Encoding issues with the payload

<Tip>Always verify against the **raw request body** before JSON parsing.</Tip>

### Timestamp Too Old

The webhook timestamp is outside your tolerance window. This could indicate:

* A replay attack attempt
* Significant clock skew between your server and Monk
* A delayed retry being delivered

```json theme={null}
{
  "error": "Webhook timestamp outside tolerance window"
}
```

### Missing Signature Header

The `X-Monk-Signature` header is missing. Ensure:

* You're receiving the request at the correct endpoint
* Your proxy/load balancer isn't stripping headers

## Testing Webhooks

### Local Development

Use a tunneling service to test webhooks locally:

1. Start your local server
2. Create a tunnel: `ngrok http 3000`
3. Add the tunnel URL as a webhook endpoint in Monk
4. Trigger events in your Monk sandbox

### Webhook Logs (Coming Soon)

View delivery attempts and responses in **Settings → Webhooks → Logs** to debug issues.
