Webhook Security: Verify Every Incoming Event
How to verify webhook signatures, block replayed requests, protect secrets and handle duplicates without trusting payloads.

Improve webhook security with HMAC signature checks on the raw body, timestamp limits against replays, secret rotation and duplicate checks by event ID.
A webhook endpoint is a public URL that accepts data and acts on it. Anyone who finds the URL can send a request that looks like a real event: a fake payment confirmation, a forged order or a flood of junk. Webhook security is about making sure every incoming event really came from the expected sender, has not been tampered with, is not an old request replayed, and is processed only once. This guide explains how signature verification works, how to block replayed requests, how to manage secrets and how to handle duplicates without trusting payloads blindly.
Why webhook security matters
Webhooks often trigger actions with real consequences: marking invoices paid, creating orders, updating customer records or starting fulfilment. Without checks, an attacker who learns the endpoint URL could:
- Send forged events that trigger actions in your systems
- Modify a genuine event in transit
- Capture a valid request and resend it later
- Overload the endpoint with requests
Keeping the URL secret is not enough. URLs leak through logs, browser histories, screenshots and support tickets. Strong webhook security assumes the URL is known and verifies every request.
How signature verification works
Most providers sign each webhook with a shared secret using an HMAC, a hash calculated from the message body and the secret. Your endpoint calculates the same HMAC and compares it with the value sent in a header. If they match, the request came from someone who knows the secret and the body was not changed.
GitHub's guide to validating webhook deliveries is a clear example: GitHub sends the signature in the X-Hub-Signature-256 header, uses an HMAC hex digest, and the signature always starts with "sha256=". Other providers use different header names and formats, so follow each provider's documentation exactly.
Three details catch many teams out:
- Use the raw body. The signature is calculated over the exact bytes sent. If your framework parses and re-serialises JSON before you verify it, the bytes change and verification fails. Stripe's webhook documentation warns that any manipulation of the raw body causes verification to fail.
- Compare safely. Python's hmac documentation recommends compare_digest() instead of the == operator to reduce the vulnerability to timing attacks.
- Reject by default. If the signature header is missing or malformed, return an error and do nothing else.
Worked example: verifying a webhook signature
The simplified, untested Python function below verifies a GitHub-style signature. It is a sketch to show the steps, not production code; adapt header names and formats to each provider.
import hashlib
import hmac
def is_valid_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
# Expect a header such as "sha256=<hex digest>"
if not signature_header or not signature_header.startswith("sha256="):
return False
expected = "sha256=" + hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
# Constant-time comparison to reduce timing attack risk
return hmac.compare_digest(expected, signature_header)
In the request handler, read the raw body before any parsing, call this function, and return an error if it fails. Only after a successful check should the body be parsed and queued for processing.
Block replayed requests
A valid signed request can be captured and sent again. Signature checks alone do not stop that, because the replayed request is genuine. Defences:
- Timestamps: some providers include a signed timestamp. Stripe, for example, includes one in its signature header, and its libraries apply a default tolerance of five minutes between that timestamp and the current time. Reject requests outside your tolerance, and keep server clocks accurate.
- Delivery IDs: many providers send a unique ID with each delivery. GitHub's webhook best practices suggest using the X-GitHub-Delivery header to ensure each delivery is unique per event, while noting that a redelivery you request keeps the same ID.
- Event IDs: record processed event IDs and skip any you have seen before.
Handle duplicates safely
Duplicates are not always attacks. Providers retry when your endpoint is slow or returns an error, so the same event may legitimately arrive more than once. Your processing should be idempotent: handling an event twice should have the same effect as handling it once.
Common approaches:
- Store each processed event ID with a unique constraint, and skip events already recorded
- Use upserts keyed on a stable business identifier, such as an order number, rather than blind inserts
- Check the current state before acting, for example not marking an invoice paid if it already is
Our guide to the webhook dead letter queue covers what to do with events that keep failing after retries.
Protect the secret
The signing secret is the key to the whole scheme:
- Store it in a secrets manager or protected environment variable, never in source code
- Use a different secret for each endpoint and environment
- Restrict who can view or change it
- Rotate it on a schedule and immediately if it may have leaked
- During rotation, accept signatures from both the old and new secret for a short, defined window
Further layers of webhook security
Signatures are the core control, but other layers help:
- HTTPS only, so payloads and signatures cannot be read in transit
- IP allow-lists, where the provider publishes its sending addresses, as an extra filter rather than the only check
- Rate limits on the endpoint, so a flood of requests cannot overwhelm downstream systems
- Payload validation, checking that fields have expected types and values before use
- Fetching the latest state from the provider's API for high-value events, rather than trusting every field in the payload
Respond fast, process later
Providers expect a quick response. GitHub's best practices, for example, say to respond within 10 seconds and suggest a queue so payloads can be processed asynchronously. Verify the signature, store or queue the event, return a success response, and do the real work in a background worker. Our guide to Zoho CRM webhooks explains how this applies to CRM events.
Test your webhook security
Controls that are never tested tend to fail quietly. Add a few checks to your test suite and release process:
- A request with a missing signature header is rejected
- A request with a valid signature but a modified body is rejected
- A request with an old timestamp, where the provider supplies one, is rejected
- The same event delivered twice is processed only once
- Rotating the secret does not drop genuine events during the overlap window
Most providers also offer a way to send test events or redeliver past ones, which helps confirm the full path works end to end after each change.
Log carefully
Log enough to investigate problems: delivery ID, event type, verification result and processing outcome. Avoid logging full payloads that contain personal or financial data, and never log secrets or signature values in a way that could help an attacker.
Frequently Asked Questions
What is webhook security?
It is the set of controls that make sure incoming webhook requests come from the expected sender, have not been altered, are not replays of old requests and are processed only once.
How does webhook signature verification work?
The sender calculates an HMAC of the request body with a shared secret and sends it in a header. Your endpoint calculates the same value from the raw body and compares them safely.
Why does signature verification fail even with the right secret?
Usually because the body was parsed or modified before verification. The signature must be checked against the exact raw bytes received.
How do you stop replay attacks on webhooks?
Reject requests whose signed timestamps are too old where the provider supplies them, and record delivery or event IDs so repeats are skipped.
Secure your integration endpoints
We build and review webhook endpoints with signature checks, replay protection, queues and idempotent processing. See our CRM and API integration services, or talk to us about webhook security.
About the Author
Manav Sharma is the founder of Insyrge and a CRM solutions architect. He works across Zoho, Salesforce, HubSpot, Odoo and more, designing the CRM setups, integrations and automations that keep sales and operations teams working from the same data.
Need Help Implementing This in Your Business?
Our certified Zoho consultants and automation experts can help you design and deploy custom workflows tailored to your operations.
Book Free Consultation