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

# Signature Verification

> Verify webhook signatures to ensure requests are authentic

## Why Verify Signatures?

Every webhook delivery includes an `X-Paywise-Signature` header containing an HMAC SHA-256 signature of the request body. You should always verify this signature to ensure that:

1. The request was sent by paywise (not a third party)
2. The payload has not been tampered with in transit

<Warning>
  Never process webhook events without verifying the signature first. Skipping verification exposes your application to forged requests.
</Warning>

## Signature Format

The signature header uses the following format:

```
X-Paywise-Signature: sha256=<hex_digest>
```

The `<hex_digest>` is the HMAC SHA-256 hash of the raw request body, computed using your endpoint's secret key.

## Verification Steps

<Steps>
  <Step title="Extract the Signature">
    Read the `X-Paywise-Signature` header and strip the `sha256=` prefix.
  </Step>

  <Step title="Compute the Expected Signature">
    Calculate the HMAC SHA-256 hash of the raw request body using your secret key.
  </Step>

  <Step title="Compare">
    Use a constant-time comparison function to compare the received and expected signatures. Return HTTP 401 if they don't match.
  </Step>
</Steps>

## Code Examples

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook(request, secret_key):
      """Verify the webhook signature. Returns True if valid."""
      signature_header = request.headers.get('X-Paywise-Signature', '')
      if not signature_header.startswith('sha256='):
          return False

      received_signature = signature_header[7:]  # Strip 'sha256=' prefix
      expected_signature = hmac.new(
          secret_key.encode('utf-8'),
          request.body,
          hashlib.sha256
      ).hexdigest()

      return hmac.compare_digest(received_signature, expected_signature)


  # Usage in a Django view:
  def webhook_handler(request):
      SECRET_KEY = "your-secret-key-here"

      if not verify_webhook(request, SECRET_KEY):
          return HttpResponse(status=401)

      payload = json.loads(request.body)
      event_type = payload['event']

      # Process the event...
      return HttpResponse(status=200)
  ```

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

  function verifyWebhook(req, secretKey) {
    const signature = req.headers['x-paywise-signature'] || '';
    if (!signature.startsWith('sha256=')) return false;

    const received = signature.slice(7);
    const expected = crypto
      .createHmac('sha256', secretKey)
      .update(req.body)  // raw body as Buffer
      .digest('hex');

    return crypto.timingSafeEqual(
      Buffer.from(received),
      Buffer.from(expected)
    );
  }

  // Usage in an Express app:
  app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
    const SECRET_KEY = 'your-secret-key-here';

    if (!verifyWebhook(req, SECRET_KEY)) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(req.body);
    const eventType = payload.event;

    // Process the event...
    res.status(200).send('OK');
  });
  ```

  ```php PHP theme={null}
  function verifyWebhook(string $payload, string $signatureHeader, string $secretKey): bool
  {
      if (strpos($signatureHeader, 'sha256=') !== 0) {
          return false;
      }

      $received = substr($signatureHeader, 7);
      $expected = hash_hmac('sha256', $payload, $secretKey);

      return hash_equals($expected, $received);
  }

  // Usage:
  $payload = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_PAYWISE_SIGNATURE'] ?? '';
  $secretKey = 'your-secret-key-here';

  if (!verifyWebhook($payload, $signature, $secretKey)) {
      http_response_code(401);
      exit('Invalid signature');
  }

  $data = json_decode($payload, true);
  // Process the event...
  http_response_code(200);
  ```
</CodeGroup>

## Secret Key

* The secret key is generated automatically when you create a webhook endpoint
* It is displayed **only once** at creation time — save it immediately
* The key is a 44-character cryptographically secure random string
* If you lose your secret key, delete the endpoint and create a new one

<Tip>
  Store your webhook secret key in environment variables or a secrets manager — never hard-code it in your application source code.
</Tip>
