# Validating request signatures

Webflow provides methods to verify that requests are genuinely coming from the Webflow API by using signatures included in the request headers. **These signatures vary based on the creation method of the webhook.**

### Request headers

* `x-webflow-timestamp` : The time the webhook was sent, represented in Unix epoch time format.
* `x-webflow-signature` : The request signature, formatted as a SHA-256 HMAC hash. It uses either the site token secret or the OAuth app's client secret as the signing key.

To ensure the authenticity of a webhook request from Webflow, validate the request signature using the provided headers and your webhook's associated signing key.

### Signing keys

Depending on the creation method of the webhook, you'll receive a different signing key.

* **Site token secret** : For webhooks created through site settings via a site token after **April 14, 2025**, each webhook will have its own secret key. You'll need to store this key securely and use it as your signing key.
* **OAuth app client secret** : For webhooks created through an OAuth application, you won't receive a separate secret key. Instead, you'll use your OAuth app's client secret as the signing key.

### Recommended: Use the provided signature validation method

Webflow recommends use of the provided SDK method to verify the incoming webhook requests' signatures.

As signature implementations are subject to change, Webflow will support updates to the method to ensure smooth transitions for developers.
All you will need to do is update the package version to benefit from these changes.

1. **Extract data from the HTTP Request**
   * `headers` : Keep the headers as a record-like object
   * `body` : Stringify the entire request body

2. **Await the results**
   <CodeBlocks>
     ```javascript Javascript
     import express from 'express';
     import { WebflowClient } from "webflow-api";

     const webflowClient = new WebflowClient({ accessToken: AUTHTOKEN });
     const app = express();
     app.use(express.json());
     // ...

     app.post('/FormSubmission', async (req, res) => {
       const isValidRequest = await webflowClient.webhooks.verifySignature({
         headers: req.headers, 
         body: JSON.stringify(req.body), 
         secret: getSigningSecret(FORM_SUBMISSION_SECRET);
       });

       if (isValidRequest) {
         // ...handle the request
       } else {
         // ...handle malicious request
       }

       res.sendStatus(200);
     });
     ```
   </CodeBlocks>

### Steps to manually validate the request signature

If you need to validate the request signature without use of the SDK, follow the steps below to achieve the same effect as the SDK method.

1. **Generate the HMAC hash**
   * Retrieve the timestamp from the `x-webflow-timestamp` header.
   * Concatenate the timestamp and the request body with a colon (`:`) separator. The format should be:
     <CodeBlocks>
       ```javascript JavaScript
       timestamp + ":" + JSON.stringify(request_body)
       ```

       ```python Python
       data = f"{request_timestamp}:{request_body_json}"
       ```
     </CodeBlocks>
   * Use your OAuth application's client secret (or your secret key if the webhook is not associated with an OAuth Application) and the SHA-256 hashing algorithm to generate the HMAC hash.

2. **Compare the generated hash with the provided signature**\
   Compare the generated HMAC hash with the `x-webflow-signature` header from the request. A match confirms the request's legitimacy; otherwise, it should be considered potentially tampered with or fraudulent.

3. **Verify the timestamp**\
   Check the `x-webflow-timestamp` header to ensure the request is recent. A request older than 5 minutes may indicate a replay attack. Calculate the request's age as follows:

   <CodeBlocks>
     ```javascript JavaScript
     currentTime - Number(request_timestamp)
     ```

     ```python Python
     current_time - int(request_timestamp)
     ```
   </CodeBlocks>

   If the difference exceeds 5 minutes (300,000 milliseconds), consider the request potentially compromised.

   See below for examples that accept an incoming HTTPS request and validate the signature:

   <CodeBlocks>
     ```javascript Node
     const express = require('express');
     const crypto = require('crypto');
     const bodyParser = require('body-parser');

     const app = express();
     const PORT = 3000;
     const CLIENT_SECRET = 'your_client_secret'; // Replace with your Webflow OAuth application's client secret

     app.use(bodyParser.json()); // Parse JSON request bodies

     app.post('/webhook', (req, res) => {
       // Step 1: Extract headers and body from the request
       const requestBody = JSON.stringify(req.body);
       const timestamp = req.headers['x-webflow-timestamp'];
       const providedSignature = req.headers['x-webflow-signature'];

       // Step 2: Verify the signature
       if (!verifyWebflowSignature(CLIENT_SECRET, timestamp, requestBody, providedSignature)) {
         return res.status(400).send('Invalid signature'); // Respond with a 400 Bad Request if verification fails
       }

       // Process the webhook request as necessary
       console.log('Webhook verified and received:', req.body);
       res.status(200).send('Webhook received');
     });

     function verifyWebflowSignature(clientSecret, timestamp, requestBody, providedSignature) {
       try {
         // Step 3: Convert the timestamp to an integer
         const requestTimestamp = parseInt(timestamp, 10);

         // Step 4: Generate the HMAC hash
         const data = `${requestTimestamp}:${requestBody}`;
         const hash = crypto.createHmac('sha256', clientSecret)
                           .update(data)
                           .digest('hex');

         // Step 5: Compare the generated hash with the provided signature
         if (!crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(providedSignature, 'hex'))) {
           throw new Error('Invalid signature');
         }

         // Step 6: Validate the timestamp (within 5 minutes)
         const currentTime = Date.now();
         if (currentTime - requestTimestamp > 300000) { // 5 minutes in milliseconds
           throw new Error('Request is older than 5 minutes');
         }

         return true; // The request is valid

       } catch (err) {
         console.error(`Error verifying signature: ${err.message}`);
         return false;
       }
     }

     app.listen(PORT, () => {
       console.log(`Server is running on port ${PORT}`);
     });

     ```

     ```python Python
     import hashlib
     import hmac
     import json
     import time
     from flask import Flask, request, abort

     app = Flask(__name__)

     @app.route('/webhook', methods=['POST'])
     def webhook():
         # Step 1: Extract headers and body from the request
         client_secret = 'your_client_secret'  # Your Webflow OAuth application's client secret
         request_body = request.get_data(as_text=True)
         request_json = json.loads(request_body)
         timestamp = request.headers.get('x-webflow-timestamp')
         provided_signature = request.headers.get('x-webflow-signature')

         # Step 2: Verify the signature
         if not verify_webflow_signature(client_secret, timestamp, request_body, provided_signature):
             abort(400, 'Invalid signature')  # Respond with a 400 Bad Request if verification fails

         # Process the webhook request as necessary
         return 'Webhook received', 200

     def verify_webflow_signature(client_secret, timestamp, request_body, provided_signature):
         try:
             # Convert the timestamp to an integer
             timestamp = int(timestamp)

             # Generate the HMAC hash
             data = f"{timestamp}:{request_body}"
             digest = hmac.new(client_secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest()

             # Compare the generated hash with the provided signature
             if not hmac.compare_digest(digest, provided_signature):
                 raise ValueError("Invalid signature")

             # Validate the timestamp
             current_time = int(time.time() * 1000)  # Convert current time to milliseconds
             if current_time - timestamp > 300000:  # 5 minutes in milliseconds
                 raise ValueError("Request is older than 5 minutes")

             return True  # The request is valid

         except Exception as e:
             print(f"Error verifying signature: {str(e)}")
             return False

     if __name__ == '__main__':
         app.run(port=5000)

     ```
   </CodeBlocks>