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

# Webhook Security

> Secure your webhook endpoints with signature verification and best practices — with gu1 webhook events for real-time downstream integration.

## Overview

Securing your webhook endpoints is critical to ensure that webhook requests are coming from Gu1 and not from malicious actors. This guide covers how to verify webhook signatures, implement security best practices, and avoid common security mistakes.

## Signature Verification

Gu1 signs all webhook requests with an HMAC SHA-256 signature using your webhook secret. The signature is sent in the `X-Webhook-Signature` header, allowing you to verify that the request is authentic.

### How Signature Verification Works

1. **Gu1 generates a signature**: When sending a webhook, Gu1 creates an HMAC SHA-256 hash of the raw request body using your webhook secret
2. **Signature is sent in header**: The signature is included in the `X-Webhook-Signature` header
3. **Your server recalculates**: Your endpoint recalculates the signature using the same secret and raw body
4. **Compare signatures**: If the signatures match, the webhook is authentic

<Warning>
  Always verify webhook signatures in production. Without verification, anyone can send fake webhooks to your endpoint and potentially compromise your system.
</Warning>

## Signature Verification Examples

### Node.js (Express)

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

const app = express();

// IMPORTANT: Store raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/gu1', async (req, res) => {
  try {
    // Get signature from header
    const signature = req.headers['x-webhook-signature'];
    const webhookSecret = process.env.GU1_WEBHOOK_SECRET;

    // Verify signature
    if (!verifySignature(req.rawBody, signature, webhookSecret)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Process webhook
    const { event, timestamp, payload } = req.body;
    await handleWebhook(event, payload);

    // Return 200 to acknowledge receipt
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: error.message });
  }
});

/**
 * Verify HMAC SHA-256 signature
 */
function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

app.listen(3000);
```

<Note>
  **Critical**: You must verify the signature using the **raw request body** before it's parsed as JSON. If you verify against the parsed JSON body (e.g., `JSON.stringify(req.body)`), the signature will not match because JSON formatting may differ.
</Note>

### Python (Flask)

```python Python theme={null}
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
import logging

app = Flask(__name__)

@app.route('/webhooks/gu1', methods=['POST'])
def gu1_webhook():
    try:
        # Get signature from header
        signature = request.headers.get('X-Webhook-Signature')
        webhook_secret = os.getenv('GU1_WEBHOOK_SECRET')

        # Get raw body for signature verification
        raw_body = request.get_data(as_text=True)

        # Verify signature
        if not verify_signature(raw_body, signature, webhook_secret):
            logging.error('Invalid webhook signature')
            return jsonify({'error': 'Invalid signature'}), 401

        # Parse payload
        payload = request.json
        event = payload.get('event')
        data = payload.get('payload')

        logging.info(f'Received webhook: {event}')

        # Process the webhook
        handle_webhook(event, data)

        return jsonify({
            'success': True,
            'message': 'Webhook received'
        }), 200

    except Exception as e:
        logging.error(f'Webhook error: {e}')
        return jsonify({
            'success': False,
            'error': str(e)
        }), 500

def verify_signature(raw_body: str, signature: str, secret: str) -> bool:
    """Verify HMAC SHA-256 signature"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        raw_body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

def handle_webhook(event: str, data: dict):
    # Process webhook based on event type
    pass

if __name__ == '__main__':
    app.run(port=3000)
```

<Tip>
  Use `hmac.compare_digest()` instead of `==` for comparing signatures in Python. This function performs a timing-safe comparison that prevents timing attacks.
</Tip>

### Go (Gin)

```go Go theme={null}
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

type WebhookPayload struct {
    Event     string                 `json:"event"`
    Timestamp string                 `json:"timestamp"`
    Payload   map[string]interface{} `json:"payload"`
}

func main() {
    r := gin.Default()
    r.POST("/webhooks/gu1", handleWebhook)
    r.Run(":3000")
}

func handleWebhook(c *gin.Context) {
    // Read raw body for signature verification
    rawBody, err := ioutil.ReadAll(c.Request.Body)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to read body"})
        return
    }

    // Verify signature
    signature := c.GetHeader("X-Webhook-Signature")
    webhookSecret := os.Getenv("GU1_WEBHOOK_SECRET")

    if !verifySignature(rawBody, signature, webhookSecret) {
        log.Println("Invalid webhook signature")
        c.JSON(401, gin.H{"error": "Invalid signature"})
        return
    }

    // Parse payload
    var payload WebhookPayload
    if err := json.Unmarshal(rawBody, &payload); err != nil {
        c.JSON(400, gin.H{"error": "Invalid JSON"})
        return
    }

    log.Printf("Received webhook: %s", payload.Event)

    // Process webhook
    handleWebhookEvent(payload)

    c.JSON(200, gin.H{
        "success": true,
        "message": "Webhook received",
    })
}

func verifySignature(rawBody []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func handleWebhookEvent(payload WebhookPayload) {
    // Process webhook based on event type
}
```

## Raw Body vs Parsed JSON

A common mistake is to verify the signature using the parsed JSON object instead of the raw request body. This will always fail because JSON formatting can differ.

<CodeGroup>
  ```javascript Wrong - Will Fail theme={null}
  // WRONG: Verifying parsed body
  app.post('/webhooks', express.json(), (req, res) => {
    const signature = req.headers['x-webhook-signature'];

    // This will NOT work - JSON.stringify may format differently
    const body = JSON.stringify(req.body);
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(body)
      .digest('hex');

    if (signature === expectedSignature) {
      // Will always fail
    }
  });
  ```

  ```javascript Correct - Will Work theme={null}
  // CORRECT: Verifying raw body
  app.use(express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString('utf8');
    }
  }));

  app.post('/webhooks', (req, res) => {
    const signature = req.headers['x-webhook-signature'];

    // This works - using raw body
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(req.rawBody)
      .digest('hex');

    if (signature === expectedSignature) {
      // Signature verified successfully
    }
  });
  ```
</CodeGroup>

### What Gu1 signs (and what it does not)

Gu1 computes `HMAC-SHA256(secret, raw_request_body)` and sends the hex digest in `X-Webhook-Signature`.

| Included in the signature                             | Not included                                                                  |
| ----------------------------------------------------- | ----------------------------------------------------------------------------- |
| Exact bytes of the HTTP POST body (UTF-8 JSON string) | `X-Webhook-Timestamp`, `X-Webhook-ID`, `X-Webhook-Event`, or any other header |
|                                                       | Query parameters or URL path                                                  |
|                                                       | Fields added/removed by your middleware after the body is read                |

<Note>
  Use a **timing-safe** comparison when checking the signature (for example `crypto.timingSafeEqual` in Node.js). See the security best practices section below.
</Note>

### Webhook history in the dashboard

The **Webhook Monitor** shows the payload for debugging and support. That view is **not** the exact byte string that was signed at delivery time. Pretty-printing, copying from the UI, or running `JSON.stringify()` on a parsed object can produce a different string and make a correct delivery look invalid.

To debug a failed delivery, compare the `X-Webhook-Signature` header your endpoint received with an HMAC computed over the **raw body of that same HTTP request**. Do not re-verify from the dashboard JSON alone.

### Intermittent signature failures

If verification works for some events but returns `401 Invalid signature` for others **with the same webhook secret and endpoint**, the usual causes are:

1. **Parsed JSON re-stringified** — `JSON.stringify(req.body)` after `express.json()` (or equivalent) does not reliably reproduce Gu1's body bytes. Different payloads (key order, nested shape, number formatting) can make this fail only sometimes.
2. **Middleware that mutates the body** — deduplicating arrays, stripping `null` fields, sorting keys, or normalizing strings before verification changes the signed input.
3. **Wrong secret** — secret regenerated in the dashboard while an old value remains in your environment.
4. **Missing signature header** — if the webhook has no secret configured, Gu1 may omit `X-Webhook-Signature`; treating a missing header as an invalid signature is expected.

<Warning>
  Signature verification is **optional** but recommended. Gu1 still delivers webhooks if you do not verify; a `401` response is returned by **your** server when your verification logic rejects the request.
</Warning>

## Idempotency Patterns

Webhooks may be delivered more than once due to network issues, timeouts, or retries. Implement idempotency to ensure you process each webhook only once.

### Database-Based Idempotency

Store processed webhook IDs in your database:

```javascript Node.js theme={null}
async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Check if already processed
  const alreadyProcessed = await db.webhookLog.findUnique({
    where: { webhookId }
  });

  if (alreadyProcessed) {
    console.log('Webhook already processed, skipping');
    return; // Skip duplicate
  }

  // Process webhook
  await processWebhookEvent(webhook);

  // Mark as processed
  await db.webhookLog.create({
    data: {
      webhookId,
      event: webhook.event,
      processedAt: new Date(),
      payload: webhook
    }
  });
}
```

### Cache-Based Idempotency

For high-volume webhooks, use a cache like Redis:

```javascript Node.js with Redis theme={null}
const Redis = require('ioredis');
const redis = new Redis();

async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Try to set the key with NX (only if not exists)
  const result = await redis.set(
    `webhook:${webhookId}`,
    '1',
    'EX', 86400, // Expire after 24 hours
    'NX'
  );

  if (!result) {
    console.log('Webhook already processed, skipping');
    return; // Already processed
  }

  // Process webhook
  await processWebhookEvent(webhook);
}
```

```python Python with Redis theme={null}
import redis
import json

redis_client = redis.Redis(host='localhost', port=6379, db=0)

def handle_webhook(webhook):
    webhook_id = f"{webhook['payload']['entityId']}_{webhook['event']}_{webhook['timestamp']}"

    # Try to set the key with NX (only if not exists)
    result = redis_client.set(
        f"webhook:{webhook_id}",
        "1",
        ex=86400,  # Expire after 24 hours
        nx=True
    )

    if not result:
        print('Webhook already processed, skipping')
        return  # Already processed

    # Process webhook
    process_webhook_event(webhook)
```

## Security Best Practices

<AccordionGroup>
  <Accordion title="1. Always Verify Signatures">
    Never skip signature verification in production environments. This is your primary defense against fake webhooks.

    ```javascript theme={null}
    // Always check the signature
    if (!verifySignature(req.rawBody, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    ```
  </Accordion>

  <Accordion title="2. Use HTTPS Only">
    Configure your webhook endpoints to use HTTPS only. Reject HTTP requests:

    ```javascript theme={null}
    app.post('/webhooks', (req, res) => {
      // Check if request is HTTPS
      if (req.protocol !== 'https') {
        return res.status(403).json({ error: 'HTTPS required' });
      }

      // Process webhook...
    });
    ```
  </Accordion>

  <Accordion title="3. Validate Event Types">
    Only process event types you're expecting:

    ```javascript theme={null}
    const ALLOWED_EVENTS = [
      'entity.created',
      'entity.updated',
      'entity.status_changed',
      'kyc.validation_approved'
    ];

    if (!ALLOWED_EVENTS.includes(webhook.event)) {
      console.warn('Unknown event type:', webhook.event);
      return res.status(400).json({ error: 'Unknown event type' });
    }
    ```
  </Accordion>

  <Accordion title="4. Implement Rate Limiting">
    Protect your endpoint from abuse with rate limiting:

    ```javascript theme={null}
    const rateLimit = require('express-rate-limit');

    const webhookLimiter = rateLimit({
      windowMs: 1 * 60 * 1000, // 1 minute
      max: 100, // Limit each IP to 100 requests per minute
      message: 'Too many webhook requests'
    });

    app.post('/webhooks', webhookLimiter, handleWebhook);
    ```
  </Accordion>

  <Accordion title="5. Store Webhook Secrets Securely">
    Never hardcode webhook secrets. Use environment variables or secret management:

    ```javascript theme={null}
    // Good - environment variable
    const secret = process.env.GU1_WEBHOOK_SECRET;

    // Bad - hardcoded
    const secret = 'abc123'; // Never do this!
    ```

    For production, use a secret manager:

    * AWS Secrets Manager
    * HashiCorp Vault
    * Azure Key Vault
    * Google Secret Manager
  </Accordion>

  <Accordion title="6. Validate Payload Structure">
    Validate the webhook payload structure before processing:

    ```javascript theme={null}
    function isValidEntityPayload(payload) {
      return (
        payload &&
        payload.entity &&
        payload.entity.id &&
        payload.entity.type
      );
    }

    if (!isValidEntityPayload(webhook.payload)) {
      return res.status(400).json({ error: 'Invalid payload structure' });
    }
    ```
  </Accordion>

  <Accordion title="7. Use Timing-Safe Comparisons">
    When comparing signatures, use timing-safe comparison functions to prevent timing attacks:

    ```javascript theme={null}
    const crypto = require('crypto');

    function verifySignature(rawBody, signature, secret) {
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(rawBody)
        .digest('hex');

      // Timing-safe comparison
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      );
    }
    ```
  </Accordion>

  <Accordion title="8. Implement Webhook Logs">
    Log all webhook attempts for auditing and debugging:

    ```javascript theme={null}
    async function logWebhook(webhook, success, error = null) {
      await db.webhookLog.create({
        data: {
          event: webhook.event,
          timestamp: webhook.timestamp,
          success,
          error: error?.message,
          payload: webhook,
          processedAt: new Date()
        }
      });
    }
    ```
  </Accordion>

  <Accordion title="9. Return 200 Quickly">
    Respond with 200 status code quickly to prevent retries. Process heavy work asynchronously:

    ```javascript theme={null}
    app.post('/webhooks', async (req, res) => {
      // Verify signature first
      if (!verifySignature(req.rawBody, signature, secret)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      // Acknowledge immediately
      res.status(200).json({ received: true });

      // Process asynchronously
      processWebhook(req.body).catch(console.error);
    });
    ```
  </Accordion>

  <Accordion title="10. Implement Retry Logic">
    If webhook processing fails, store it for retry:

    ```javascript theme={null}
    try {
      await processWebhook(webhook);
    } catch (error) {
      // Store failed webhook for retry
      await db.failedWebhook.create({
        data: {
          webhook,
          error: error.message,
          retryCount: 0,
          nextRetryAt: new Date(Date.now() + 5 * 60 * 1000) // Retry in 5 minutes
        }
      });

      // Still return 200 to prevent Gu1 from retrying
      res.status(200).json({ success: false });
    }
    ```
  </Accordion>
</AccordionGroup>

## Common Security Mistakes to Avoid

<Warning>
  Avoid these common security mistakes that can compromise your webhook endpoints:
</Warning>

### 1. Skipping Signature Verification

```javascript theme={null}
// DON'T DO THIS - No signature verification
app.post('/webhooks', (req, res) => {
  // Processing webhook without verification
  handleWebhook(req.body);
  res.status(200).send('OK');
});
```

**Risk**: Anyone can send fake webhooks to your endpoint.

### 2. Verifying Parsed JSON Instead of Raw Body

```javascript theme={null}
// DON'T DO THIS - Verifying parsed body
app.post('/webhooks', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const body = JSON.stringify(req.body); // Wrong!

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  // Will always fail
});
```

**Risk**: Signature verification will always fail.

### 3. Using HTTP Instead of HTTPS

```javascript theme={null}
// DON'T DO THIS - Accepting HTTP
http://yourapp.com/webhooks
```

**Risk**: Webhook payloads can be intercepted in transit.

### 4. Hardcoding Secrets

```javascript theme={null}
// DON'T DO THIS - Hardcoded secret
const secret = 'my-webhook-secret-123';
```

**Risk**: Secrets exposed in version control or logs.

### 5. Not Implementing Idempotency

```javascript theme={null}
// DON'T DO THIS - Processing duplicates
app.post('/webhooks', (req, res) => {
  // No check for duplicates
  await createUser(req.body.entity);
  res.status(200).send('OK');
});
```

**Risk**: Duplicate webhooks will create duplicate records.

### 6. Exposing Errors to Clients

```javascript theme={null}
// DON'T DO THIS - Exposing internal errors
app.post('/webhooks', async (req, res) => {
  try {
    await processWebhook(req.body);
  } catch (error) {
    res.status(500).json({
      error: error.stack, // Exposing stack trace
      query: error.query  // Exposing database query
    });
  }
});
```

**Risk**: Internal information leakage to attackers.

### 7. Not Validating Event Types

```javascript theme={null}
// DON'T DO THIS - Processing unknown events
app.post('/webhooks', (req, res) => {
  // Accepting any event type
  handleAnyEvent(req.body.event, req.body.payload);
});
```

**Risk**: Attackers can send arbitrary event types.

### 8. Using Weak Secrets

```javascript theme={null}
// DON'T DO THIS - Weak secret
const secret = '123456';
```

**Risk**: Secrets can be brute-forced.

**Solution**: Use strong, randomly generated secrets (at least 32 characters).

## Testing Webhook Security

### Test Invalid Signatures

```bash theme={null}
# Test with invalid signature
curl -X POST https://yourapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: invalid-signature" \
  -d '{"event":"entity.created","payload":{}}'

# Should return 401 Unauthorized
```

### Test Replay Attacks

Send the same webhook twice and verify idempotency:

```bash theme={null}
# Send webhook once
curl -X POST https://yourapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Send same webhook again
curl -X POST https://yourapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Second request should be ignored (idempotency)
```

## Monitoring and Alerting

Set up monitoring for webhook security:

```javascript theme={null}
// Track webhook metrics
const metrics = {
  totalReceived: 0,
  signatureVerified: 0,
  signatureFailed: 0,
  processed: 0,
  failed: 0
};

app.post('/webhooks', (req, res) => {
  metrics.totalReceived++;

  const signature = req.headers['x-webhook-signature'];
  if (!verifySignature(req.rawBody, signature, secret)) {
    metrics.signatureFailed++;

    // Alert if too many signature failures
    if (metrics.signatureFailed > 10) {
      alertSecurityTeam('High webhook signature failure rate');
    }

    return res.status(401).json({ error: 'Invalid signature' });
  }

  metrics.signatureVerified++;
  // Process webhook...
});
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhook Configuration" icon="gear" href="/en/webhooks/configuration">
    Learn how to configure webhooks
  </Card>

  <Card title="Entity Events" icon="user" href="/en/webhooks/events/entity-events">
    Handle entity lifecycle events
  </Card>

  <Card title="KYC Events" icon="id-card" href="/en/webhooks/events/kyc-events">
    Process KYC verification events
  </Card>

  <Card title="Rule Events" icon="gavel" href="/en/webhooks/events/rule-events">
    Respond to compliance rules
  </Card>
</CardGroup>
