# Security (/webhooks/security)

<!-- agent-signals: reading_time_min: 2 · est_tokens: 1239 · updated: 2026-07-30 -->
Related: [Overview](/webhooks/overview.md), [Event Types](/webhooks/events.md), [Testing](/webhooks/testing.md)

Verify that every webhook request actually came from Firecrawl by checking its HMAC-SHA256 signature. This stops attackers from spoofing payloads and lets you trust the data before acting on it.

## Secret Key [#secret-key]

Your webhook secret is available in the [Advanced tab](https://www.firecrawl.dev/app/settings?tab=advanced) of your account settings. Each account has a unique secret used to sign all webhook requests.

<Warning>
  Keep your webhook secret secure and never expose it publicly. If you believe your secret has been compromised, regenerate it immediately from your account settings.
</Warning>

## Signature Verification [#signature-verification]

Each webhook request includes an `X-Firecrawl-Signature` header:

```
X-Firecrawl-Signature: sha256=abc123def456...
```

### How to Verify [#how-to-verify]

1. Extract the signature from the `X-Firecrawl-Signature` header
2. Get the raw request body (before parsing)
3. Compute HMAC-SHA256 using your secret key
4. Compare signatures using a timing-safe function

### Implementation [#implementation]

<CodeGroup>
  <CodeBlockTabs defaultValue="Node/Express" groupId="node-express+python-flask">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="Node/Express">
        Node/Express
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python/Flask">
        Python/Flask
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="Node/Express">
      ```js  
      import crypto from 'crypto';
      import express from 'express';

      const app = express();

      // Use raw body parser for signature verification
      app.use('/webhook/firecrawl', express.raw({ type: 'application/json' }));

      app.post('/webhook/firecrawl', (req, res) => {
        const signature = req.get('X-Firecrawl-Signature');
        const webhookSecret = process.env.FIRECRAWL_WEBHOOK_SECRET;
        
        if (!signature || !webhookSecret) {
          return res.status(401).send('Unauthorized');
        }
        
        // Extract hash from signature header
        const [algorithm, hash] = signature.split('=');
        if (algorithm !== 'sha256') {
          return res.status(401).send('Invalid signature algorithm');
        }
        
        // Compute expected signature
        const expectedSignature = crypto
          .createHmac('sha256', webhookSecret)
          .update(req.body)
          .digest('hex');
        
        // Verify signature using timing-safe comparison
        if (!crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(expectedSignature, 'hex'))) {
          return res.status(401).send('Invalid signature');
        }
        
        // Parse and process verified webhook
        const event = JSON.parse(req.body);
        console.log('Verified Firecrawl webhook:', event);
        
        res.status(200).send('ok');
      });

      app.listen(3000, () => console.log('Listening on 3000'));
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python/Flask">
      ```python  
      import hmac
      import hashlib
      from flask import Flask, request, abort

      app = Flask(__name__)

      WEBHOOK_SECRET = 'your-webhook-secret-here'  # Get from Firecrawl dashboard

      @app.post('/webhook/firecrawl')
      def webhook():
          signature = request.headers.get('X-Firecrawl-Signature')
          
          if not signature:
              abort(401, 'Missing signature header')
          
          # Extract hash from signature header
          try:
              algorithm, hash_value = signature.split('=', 1)
              if algorithm != 'sha256':
                  abort(401, 'Invalid signature algorithm')
          except ValueError:
              abort(401, 'Invalid signature format')
          
          # Compute expected signature
          expected_signature = hmac.new(
              WEBHOOK_SECRET.encode('utf-8'),
              request.data,
              hashlib.sha256
          ).hexdigest()
          
          # Verify signature using timing-safe comparison
          if not hmac.compare_digest(hash_value, expected_signature):
              abort(401, 'Invalid signature')
          
          # Parse and process verified webhook
          event = request.get_json(force=True)
          print('Verified Firecrawl webhook:', event)
          
          return 'ok', 200

      if __name__ == '__main__':
          app.run(port=3000)
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Best Practices [#best-practices]

* **Verify every request.** Always check the signature before processing a webhook payload. Reject any request that fails verification with a `401` status.
* **Use timing-safe comparisons.** Standard string comparison can leak timing information. Use `crypto.timingSafeEqual()` in Node.js or `hmac.compare_digest()` in Python.
* **Serve your endpoint over HTTPS.** This ensures webhook payloads are encrypted in transit.
