> ## 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

<Warning>
  This page documents a frozen v1 API. It receives only critical correctness or
  security corrections. For new integrations, use the [current API](/api-docs/overview)
  and follow the linked [migration guide](/api-docs/legacy/overview#migration-guides).
</Warning>

## 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.

paywise sends the body as compact JSON with sorted keys. Compute the hash over the raw bytes exactly as received: parsing and re-serializing the JSON produces different bytes and a signature mismatch.

## 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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.