DEVELOPER

Webhook Security: How to Verify ParseHook Signatures (2026)

[BLUF / IMMEDIATE EXECUTIVE SUMMARY]

Every ParseHook webhook is cryptographically signed using HMAC-SHA256. Learn how to verify incoming webhook signatures in Node.js, Python, and PHP to secure your endpoint.

·FIVE MINUTE READ·By ParseHook Team

Webhook signature verification is a cryptographic security mechanism that confirms an incoming HTTP request genuinely originated from a trusted provider (like ParseHook) and that the JSON payload was not tampered with in transit.

If you are automating invoice processing or extracting sensitive lead data, your webhook endpoint is exposed to the public internet. If you do not verify the origin of incoming requests, a malicious actor could send fake payloads to your server, corrupting your database or triggering unauthorized actions.

This guide explains how ParseHook secures webhooks and provides copy-pasteable code examples to verify signatures in Node.js, Python, and PHP.

How ParseHook Signs Webhooks

When you configure a webhook destination in your ParseHook dashboard, the system generates a unique Webhook Signing Secret (e.g., whsec_abc123...). You must keep this secret safe on your server as an environment variable.

Every time ParseHook successfully parses an email and sends a POST request to your URL, it calculates a cryptographic hash of the JSON payload using your signing secret and the HMAC-SHA256 algorithm.

ParseHook includes this resulting hash in the headers of the incoming request under the key:

X-ParseHook-Signature

To verify the request, your server simply takes the raw incoming payload, calculates the HMAC-SHA256 hash using your stored secret, and compares your result to the signature in the header. If they match, the request is authentic.

Code Examples

Below are standard implementations for verifying ParseHook signatures in popular backend frameworks.

Node.js (Express)

In Node.js, use the built-in crypto module.

Important: You must verify the signature using the raw, stringified payload. If you modify the JSON or format it before calculating the hash, the signatures will not match.

const express = require('express'); const crypto = require('crypto'); const app = express(); // Use express.json() to parse the body app.use(express.json()); const WEBHOOK_SECRET = process.env.PARSEHOOK_WEBHOOK_SECRET; app.post('/webhook', (req, res) => { const signatureHeader = req.headers['x-parsehook-signature']; if (!signatureHeader) { return res.status(401).send('Missing signature header'); } // Calculate the HMAC-SHA256 hash of the payload const payloadString = JSON.stringify(req.body); const expectedSignature = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(payloadString) .digest('hex'); // Securely compare the signatures to prevent timing attacks try { const isValid = crypto.timingSafeEqual( Buffer.from(signatureHeader), Buffer.from(expectedSignature) ); if (!isValid) { return res.status(401).send('Invalid signature'); } } catch (error) { return res.status(401).send('Signature verification failed'); } // Signature is valid. Process the parsed email data. const emailData = req.body.parsed_data; console.log('Successfully verified payload from:', emailData.vendor); res.status(200).send('OK'); }); app.listen(3000, () => console.log('Listening on port 3000'));

Python (FastAPI)

In Python, use the standard library hmac and hashlib modules. hmac.compare_digest is critical here as it prevents timing attacks.

from fastapi import FastAPI, Request, HTTPException, Header import hmac import hashlib import os app = FastAPI() WEBHOOK_SECRET = os.getenv("PARSEHOOK_WEBHOOK_SECRET").encode('utf-8') @app.post("/webhook") async def handle_webhook(request: Request, x_parsehook_signature: str = Header(None)): if not x_parsehook_signature: raise HTTPException(status_code=401, detail="Missing signature header") # Get the raw body body = await request.body() # Calculate the expected signature expected_signature = hmac.new( WEBHOOK_SECRET, msg=body, digestmod=hashlib.sha256 ).hexdigest() # Securely compare signatures if not hmac.compare_digest(expected_signature, x_parsehook_signature): raise HTTPException(status_code=401, detail="Invalid signature") # Signature is valid. Process payload. payload = await request.json() print("Verified payload received:", payload.get("parsed_data")) return {"status": "success"}

PHP

In PHP, use the hash_hmac and hash_equals functions.

<?php $webhook_secret = getenv('PARSEHOOK_WEBHOOK_SECRET'); // Read the incoming HTTP headers $headers = getallheaders(); $signature_header = $headers['X-ParseHook-Signature'] ?? null; if (!$signature_header) { http_response_code(401); die('Missing signature header'); } // Get the raw POST body $payload = file_get_contents('php://input'); // Calculate expected signature $expected_signature = hash_hmac('sha256', $payload, $webhook_secret); // Securely compare signatures if (!hash_equals($expected_signature, $signature_header)) { http_response_code(401); die('Invalid signature'); } // Signature is valid. Process payload. $data = json_decode($payload, true); error_log('Verified payload received from: ' . $data['parsed_data']['from_domain']); http_response_code(200); echo json_encode(['status' => 'success']); ?>

Best Practices for Webhook Security

  1. Always use timing-safe string comparison. Notice that in every example above, we use functions like crypto.timingSafeEqual() or hmac.compare_digest(). Using a standard === operator allows attackers to guess your secret by analyzing how many milliseconds your server takes to reject a fake signature.
  2. Never expose your webhook secret. Do not commit your secret to GitHub. Keep it strictly inside your .env files or a secure secret manager (like AWS Secrets Manager or Vercel Environment Variables).
  3. Respond with 200 OK quickly. Acknowledge receipt of the webhook before running heavy database operations. If your server takes longer than 60 seconds to respond, ParseHook will assume the delivery failed and will attempt to retry the webhook.

Frequently Asked Questions

Does ParseHook protect against replay attacks? ParseHook includes a timestamp field in the top level of every webhook JSON payload. To prevent replay attacks (where a malicious actor intercepts a valid webhook and resends it later), you can parse this timestamp on your server and reject any webhook that is older than 5 minutes.

How do I rotate my webhook secret? If you suspect your secret has been compromised, go to your ParseHook dashboard, navigate to your Inbox settings, and click "Regenerate Secret." ParseHook will instantly begin signing new payloads with the new secret. Update your server's environment variables immediately to avoid dropping valid payloads.

What happens if signature verification fails? If your server rejects the request (by returning a 401 or 403 status code), ParseHook treats it as a failed delivery. The system retries up to 6 times using exponential backoff (1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours). After 6 failed attempts, the email enters a Dead Letter Queue for manual retry from your dashboard.

Can I restrict incoming webhooks by IP address instead of using signatures? While IP allowlisting adds a layer of security, it is not recommended as a replacement for cryptographic signatures. ParseHook's outbound IP addresses may scale and change dynamically. Verifying the HMAC-SHA256 signature guarantees authenticity regardless of the origin IP.

Are webhooks available on the free plan? Yes. Webhooks and signature verification are critical security features, not premium add-ons. They are included on the Free plan, Pro plan ($29/mo, or $12.75/mo annually), and Business plan automatically.

Secure Your Data Pipeline

Security is not an afterthought in automation. By implementing a 10-line signature verification function, you ensure your database only ever processes data that was genuinely parsed by ParseHook.

Create your free developer account today. Send your first test payload and verify the HMAC signature in under 5 minutes.

Start building for free here.

READY TO AUTOMATE?

Start parsing emails with AI. No credit card required.

START FREE

RELATED ARTICLES

TUTORIAL
How to Automate Invoice Processing Without Templates
TUTORIAL
Email to Airtable: Automatic Data Sync Without Zapier (2026)