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

# Verifying Webhooks

> Learn how to verify webhook signatures to ensure authenticity and security

## Overview

Webhook signature verification ensures that incoming webhook requests are genuinely from EngageXPay and have not been tampered with. This process uses HMAC (Hash-based Message Authentication Code) to validate each webhook payload.

## How It Works

1. **Extract the signature**: Retrieve the signature value from the webhook request header
2. **Recalculate the signature**: Use your secret key and the same hash algorithm to compute the HMAC signature for the received payload
3. **Compare signatures**: Verify that your computed signature matches the one provided in the header

## Verification Steps

Follow these steps to verify webhook signatures in your application:

1. **Read the raw request body** - Capture the HTTP body as raw bytes without any modifications or parsing
2. **Extract signature headers** - Read the `x-engagexpay-signature` and `x-engagexpay-signature-algorithm` headers from the request
3. **Compute the HMAC** - Using your webhook secret key and the algorithm specified in `x-engagexpay-signature-algorithm`, calculate:
   ```
   HMAC(algorithm, secretKey, rawBodyBytes) → hexadecimal string
   ```
4. **Compare signatures** - Accept the webhook only if your computed signature exactly matches the `x-engagexpay-signature` header value

> [!WARNING]
>
> Always use a constant-time comparison function to prevent timing attacks when comparing signatures.

## Code Examples

Here are implementation examples in different programming languages:

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

function verifyWebhookSignature(request, webhookSecret) {
  // Extract headers
  const receivedSignature = request.headers['x-engagexpay-signature'];
  const algorithm = request.headers['x-engagexpay-signature-algorithm'] || 'sha256';
  
  // Get raw body as string or buffer
  const rawBody = request.body; // Ensure this is the raw body, not parsed JSON
  
  // Compute HMAC
  const computedSignature = crypto
    .createHmac(algorithm, webhookSecret)
    .update(rawBody, 'utf8')
    .digest('hex');
  
  // Compare signatures using constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(receivedSignature),
    Buffer.from(computedSignature)
  );
}

// Usage example with Express.js
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const isValid = verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the webhook
  const payload = JSON.parse(req.body);
  console.log('Valid webhook received:', payload);
  res.status(200).send('OK');
});
```

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

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

func verifyWebhookSignature(r *http.Request, webhookSecret string) (bool, error) {
    // Extract headers
    receivedSignature := r.Header.Get("x-engagexpay-signature")
    algorithm := r.Header.Get("x-engagexpay-signature-algorithm")
    if algorithm == "" {
        algorithm = "sha256"
    }
    
    // Read raw body
    rawBody, err := io.ReadAll(r.Body)
    if err != nil {
        return false, err
    }
    
    // Compute HMAC (using SHA-256 as example)
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(rawBody)
    computedSignature := hex.EncodeToString(mac.Sum(nil))
    
    // Constant-time comparison
    return subtle.ConstantTimeCompare(
        []byte(receivedSignature),
        []byte(computedSignature),
    ) == 1, nil
}

// HTTP handler example
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    webhookSecret := os.Getenv("WEBHOOK_SECRET")
    
    isValid, err := verifyWebhookSignature(r, webhookSecret)
    if err != nil || !isValid {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    // Process the webhook
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}
```

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

def verify_webhook_signature(request, webhook_secret):
    """
    Verify the HMAC signature of an incoming webhook request.
    
    Args:
        request: Flask request object
        webhook_secret: Your webhook secret key
        
    Returns:
        bool: True if signature is valid, False otherwise
    """
    # Extract headers
    received_signature = request.headers.get('x-engagexpay-signature')
    algorithm = request.headers.get('x-engagexpay-signature-algorithm', 'sha256')
    
    if not received_signature:
        return False
    
    # Get raw body bytes
    raw_body = request.get_data()
    
    # Compute HMAC
    hash_func = getattr(hashlib, algorithm)
    computed_signature = hmac.new(
        webhook_secret.encode('utf-8'),
        raw_body,
        hash_func
    ).hexdigest()
    
    # Constant-time comparison
    return hmac.compare_digest(received_signature, computed_signature)

# Flask example
app = Flask(__name__)

@app.route('/webhooks', methods=['POST'])
def webhook_handler():
    webhook_secret = os.environ.get('WEBHOOK_SECRET')
    
    if not verify_webhook_signature(request, webhook_secret):
        abort(401, 'Invalid signature')
    
    # Process the webhook
    payload = request.get_json()
    print(f'Valid webhook received: {payload}')
    
    return 'OK', 200
```

```java Java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;

public class WebhookVerifier {
    
    public static boolean verifyWebhookSignature(
        String receivedSignature,
        String algorithm,
        byte[] rawBody,
        String webhookSecret
    ) {
        try {
            // Default to SHA-256 if algorithm not specified
            if (algorithm == null || algorithm.isEmpty()) {
                algorithm = "sha256";
            }
            
            // Convert algorithm name to Java format (e.g., "sha256" -> "HmacSHA256")
            String javaAlgorithm = "Hmac" + algorithm.toUpperCase().replace("SHA", "SHA");
            
            // Compute HMAC
            Mac mac = Mac.getInstance(javaAlgorithm);
            SecretKeySpec secretKeySpec = new SecretKeySpec(
                webhookSecret.getBytes("UTF-8"),
                javaAlgorithm
            );
            mac.init(secretKeySpec);
            byte[] hmacBytes = mac.doFinal(rawBody);
            
            // Convert to hex string
            String computedSignature = HexFormat.of().formatHex(hmacBytes);
            
            // Constant-time comparison
            return MessageDigest.isEqual(
                receivedSignature.getBytes(),
                computedSignature.getBytes()
            );
            
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    
    // Spring Boot example
    @PostMapping("/webhooks")
    public ResponseEntity<String> handleWebhook(
        @RequestHeader("x-engagexpay-signature") String signature,
        @RequestHeader(value = "x-engagexpay-signature-algorithm", defaultValue = "sha256") String algorithm,
        @RequestBody byte[] rawBody
    ) {
        String webhookSecret = System.getenv("WEBHOOK_SECRET");
        
        boolean isValid = verifyWebhookSignature(signature, algorithm, rawBody, webhookSecret);
        
        if (!isValid) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature");
        }
        
        // Process the webhook
        String payload = new String(rawBody, StandardCharsets.UTF_8);
        System.out.println("Valid webhook received: " + payload);
        
        return ResponseEntity.ok("OK");
    }
}
```

## Best Practices

> [accordion] Store secrets securely
>
> Never hardcode your webhook secret in your source code. Use environment variables or a secure secrets management system.

> [accordion] Use constant-time comparison
>
> Always use constant-time comparison functions (like `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) to prevent timing attacks.

> [accordion] Preserve raw body
>
> Ensure your web framework provides access to the raw request body before any parsing or modifications occur.

> [accordion] Handle errors gracefully
>
> Return appropriate HTTP status codes (401 Unauthorized) for failed verifications without revealing specific error details.

## Troubleshooting

If signature verification is failing:

* **Check the raw body**: Ensure you're using the exact raw bytes received, not a re-serialized version
* **Verify the secret**: Confirm you're using the correct webhook secret from the EngageXPay app
* **Check character encoding**: Make sure you're using UTF-8 encoding consistently
* **Inspect headers**: Verify that `x-engagexpay-signature` and `x-engagexpay-signature-algorithm` headers are present
* **Test the algorithm**: Confirm you're using the algorithm specified in the `x-engagexpay-signature-algorithm` header


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