dFlow Logo
Blog Image

Secure Your Webhooks Step by Step

Avatar
Manikanta
2 Feb, 2026
webhookssecurityapis

Webhooks are one of the most common integration patterns today. Payments, deployments, notifications, analytics. Almost everything relies on them.

They are also one of the most misunderstood and insecure parts of many applications.

A webhook endpoint is usually a public URL. If it is not secured properly, anyone can send requests to it. This article explains how to secure webhooks step by step, using practical decisions you can apply immediately.


What Is a Webhook (Quick Recap)

A webhook is an HTTP endpoint that receives events from another service.

Examples:

  • Stripe sends a payment.success event
  • GitHub sends a push event
  • A CI system sends a deployment.completed event

Most of the time, it is a simple POST request with JSON data.

Because it feels simple, webhooks are often treated as just another API endpoint. That assumption is what causes security issues later.


The Most Common and Dangerous Mistake

Many webhook handlers look like this:

1app.post('/webhook', (req, res) => {
2 processEvent(req.body)
3 res.sendStatus(200)
4})

This code assumes that if a request reached the endpoint, it must be legitimate.

What is missing?

  • No authentication
  • No source verification
  • No replay protection
  • No payload validation

Anyone who discovers this endpoint can:

  • Trigger fake payments
  • Mark orders as completed
  • Spam internal workflows

This needs to be fixed properly.


Step 1: Always Verify the Sender

The first rule of webhook security is simple.

Never trust the payload alone.

Most providers sign webhook requests using a shared secret. Your server must verify that signature before doing anything else.

The provider:

  • Signs the raw request body using a secret
  • Sends the signature in a header

Your server:

  • Recomputes the signature
  • Compares it with the received value
  • Rejects the request if they do not match

This confirms that the request came from the expected provider and was not modified in transit.

Example: Signature Verification (Node.js + Express)

1import crypto from 'crypto'
2import express from 'express'
3
4const app = express()
5
6app.post(
7 '/webhook',
8 express.raw({ type: 'application/json' }),
9 (req, res) => {
10 const signature = req.headers['x-signature']
11 const secret = process.env.WEBHOOK_SECRET
12
13 if (!signature || !secret) {
14 return res.sendStatus(401)
15 }
16
17 const expected = crypto
18 .createHmac('sha256', secret)
19 .update(req.body)
20 .digest('hex')
21
22 const isValid = crypto.timingSafeEqual(
23 Buffer.from(signature),
24 Buffer.from(expected)
25 )
26
27 if (!isValid) {
28 return res.sendStatus(401)
29 }
30
31 res.sendStatus(200)
32 }
33)

Important details:

  • Always verify using the raw request body
  • Use timing safe comparison
  • Stop processing immediately if verification fails

Step 2: Protect Against Replay Attacks

A valid webhook request can still be dangerous if it is replayed.

If someone captures a real request and sends it again, your system may process the same event multiple times.

To prevent this, webhook requests should include a timestamp.

You should:

  • Reject requests older than five minutes
  • Reject requests from the future
  • Combine timestamp checks with signature verification

Example Logic

1const now = Date.now()
2const requestTime = event.timestamp * 1000
3
4if (Math.abs(now - requestTime) > 5 * 60 * 1000) {
5 throw new Error('Expired webhook')
6}

This single check eliminates an entire class of replay attacks.


Step 3: Validate the Payload Strictly

Signature verification confirms who sent the request. It does not guarantee the payload is valid.

Webhook payloads can:

  • Change format over time
  • Miss required fields
  • Contain unexpected values

Always treat webhook data as untrusted input.

Example

1if (!event.type || !event.data || !event.data.id) {
2 throw new Error('Invalid webhook payload')
3}

In production systems, schema validation libraries are strongly recommended to avoid silent failures.


Step 4: Make Webhook Processing Idempotent

Webhook providers retry requests when they do not receive a success response. This is expected behavior.

Without idempotency, retries can:

  • Duplicate database records
  • Trigger multiple notifications
  • Process the same payment more than once

The fix is to ensure each event is processed only once.

Example

1const exists = await db.webhookEvents.findOne({
2 eventId: event.id
3})
4
5if (exists) {
6 return res.sendStatus(200)
7}
8
9await db.webhookEvents.insert({
10 eventId: event.id,
11 processedAt: new Date()
12})

Once this is in place, retries stop being a problem.


Step 5: Respond Fast and Process Later

Webhook providers expect a quick response.

If your endpoint performs heavy logic, it increases the chance of timeouts and retries.

A safer pattern is:

  1. Verify and validate
  2. Respond with 200 OK
  3. Process the event asynchronously

Example

1res.sendStatus(200)
2queue.push({ event })

This keeps webhook integrations stable under load.


Step 6: Log Failures Carefully

Logging webhook failures is important, but restraint matters.

Good things to log:

  • Event IDs
  • Signature verification failures
  • Unexpected event types

Avoid logging:

  • Secrets
  • Full payloads
  • Complete request headers

Logs should help debugging without creating new security risks.


A Real World Secure Flow

Payment webhook example:

  1. Receive webhook
  2. Verify signature
  3. Validate timestamp
  4. Validate schema
  5. Check idempotency
  6. Acknowledge request
  7. Process payment update asynchronously

Each step is simple. Together, they make the system reliable.



Final Takeaway

Webhooks are not just integrations.
They are public entry points into your system.

If you verify signatures, prevent replay attacks, validate payloads, and handle retries safely, your webhook setup becomes boring, predictable, and secure.

In production systems, boring is exactly what you want.