# Verifying Signatures

Every webhook request includes an HMAC-SHA256 signature in the `x-zevpay-signature` header. Verify this signature to ensure the request is genuinely from ZevPay and hasn't been tampered with.

## How it works

1. ZevPay computes an HMAC-SHA256 hash of the request body using your **webhook secret**
2. The hash is sent as a hex string in the `x-zevpay-signature` header
3. You recompute the hash on your end and compare it to the header value

## Your webhook secret

Find your webhook secret in the ZevPay Dashboard under **Settings → API Keys**. It's shared between the public and secret key in each pair.

::: warning Keep it secret
Never expose your webhook secret in client-side code, public repositories, or logs.
:::

## Verification examples

::: code-group

```js [Node.js]
const crypto = require('crypto');

function verifyWebhookSignature(req, webhookSecret) {
  const signature = req.headers['x-zevpay-signature'];
  if (!signature) return false;

  const body = JSON.stringify(req.body);
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(body)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express.js example
app.post('/webhooks/zevpay', express.json(), (req, res) => {
  const isValid = verifyWebhookSignature(req, process.env.ZEVPAY_WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process the event
  const { event, data } = req.body;
  console.log(`Received ${event}:`, data.reference);

  res.status(200).send('OK');
});
```

```python [Python]
import hmac
import hashlib
import json

def verify_webhook_signature(payload_body, signature, webhook_secret):
    expected = hmac.new(
        webhook_secret.encode('utf-8'),
        payload_body.encode('utf-8'),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

# Flask example
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/zevpay', methods=['POST'])
def webhook():
    signature = request.headers.get('x-zevpay-signature', '')
    is_valid = verify_webhook_signature(
        request.get_data(as_text=True),
        signature,
        os.environ['ZEVPAY_WEBHOOK_SECRET'],
    )

    if not is_valid:
        return jsonify({'error': 'Invalid signature'}), 401

    payload = request.get_json()
    print(f"Received {payload['event']}: {payload['data']['reference']}")

    return 'OK', 200
```

```php [PHP]
<?php

function verifyWebhookSignature($payload, $signature, $webhookSecret) {
    $expected = hash_hmac('sha256', $payload, $webhookSecret);
    return hash_equals($expected, $signature);
}

// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ZEVPAY_SIGNATURE'] ?? '';
$webhookSecret = getenv('ZEVPAY_WEBHOOK_SECRET');

if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    echo 'Invalid signature';
    exit;
}

$event = json_decode($payload, true);
error_log("Received {$event['event']}: {$event['data']['reference']}");

http_response_code(200);
echo 'OK';
```

```go [Go]
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
	"os"
)

func verifySignature(body []byte, signature, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	signature := r.Header.Get("x-zevpay-signature")
	secret := os.Getenv("ZEVPAY_WEBHOOK_SECRET")

	if !verifySignature(body, signature, secret) {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}

	// Process event...
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}
```

:::

## Important notes

### Use timing-safe comparison

Always use a constant-time comparison function (e.g., `crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python, `hash_equals` in PHP) to prevent timing attacks.

### Parse the raw body

The signature is computed over the **raw JSON string** of the request body. If your framework parses the body before you access it, make sure to re-serialize it identically, or access the raw body directly.

```js
// Express.js — ensure raw body is available
app.use('/webhooks/zevpay', express.raw({ type: 'application/json' }));

app.post('/webhooks/zevpay', (req, res) => {
  const rawBody = req.body.toString();
  // Use rawBody for signature verification
});
```

### Test with your webhook secret

Use your **test mode** webhook secret during development. Test and live mode use separate secrets.
