> ## Documentation Index
> Fetch the complete documentation index at: https://developer.fin.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 Fin 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-fin-signature` and `x-fin-signature-algorithm` headers from the request
3. **Compute the HMAC** - Using your webhook secret key and the algorithm specified in `x-fin-signature-algorithm`, calculate:
   ```
   HMAC(algorithm, secretKey, rawBodyBytes) → hexadecimal string
   ```
4. **Compare signatures** - Accept the webhook only if your computed signature exactly matches the `x-fin-signature` header value

<Warning>
  Always use a constant-time comparison function to prevent timing attacks when comparing signatures.
</Warning>

## Code Examples

Here are implementation examples in different programming languages:

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

  function verifyWebhookSignature(request, webhookSecret) {
    // Extract headers
    const receivedSignature = request.headers['x-fin-signature'];
    const algorithm = request.headers['x-fin-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-fin-signature")
      algorithm := r.Header.Get("x-fin-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-fin-signature')
      algorithm = request.headers.get('x-fin-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-fin-signature") String signature,
          @RequestHeader(value = "x-fin-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");
      }
  }
  ```
</CodeGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Store secrets securely">
    Never hardcode your webhook secret in your source code. Use environment variables or a secure secrets management system.
  </Accordion>

  <Accordion title="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>

  <Accordion title="Preserve raw body">
    Ensure your web framework provides access to the raw request body before any parsing or modifications occur.
  </Accordion>

  <Accordion title="Handle errors gracefully">
    Return appropriate HTTP status codes (401 Unauthorized) for failed verifications without revealing specific error details.
  </Accordion>
</AccordionGroup>

## 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 your Fin dashboard
* **Check character encoding**: Make sure you're using UTF-8 encoding consistently
* **Inspect headers**: Verify that `x-fin-signature` and `x-fin-signature-algorithm` headers are present
* **Test the algorithm**: Confirm you're using the algorithm specified in the `x-fin-signature-algorithm` header
