---
title: "Webhook Security: Signatures, Verification, and Replay Protection"
description: "request-signature-SHA256 signatures, timestamp validation, and replay protection secure webhook deliveries. Code examples in Node.js, Python, and Go for verifying every payload."
canonical: https://nowah.xyz/blog/webhook-security-signatures-verification
lastModified: "2026-08-07T08:12:45.249Z"
---

# Webhook Security: Signatures, Verification, and Replay Protection

request-signature-SHA256 signatures, timestamp validation, and replay protection secure webhook deliveries. Code examples in Node.js, Python, and Go for verifying every payload.

A developer's webhook endpoint had no signature verification. It accepted any HTTP POST with a JSON body that looked like a booking confirmation. An attacker discovered the endpoint URL, crafted a fake booking event, and the developer's system processed a fraudulent booking notification that triggered a confirmation email to a customer who had never actually booked anything.

The fix was trivial: verify the cryptographic signature that every webhook includes. But the developer did not implement verification because our documentation treated it as optional. We changed that. Signature verification is not optional. It is the difference between a secure integration and an open door.

## How webhook signing works

![Illustration for this section](https://pics.nowah.xyz/website-media/developer-experience-065-img-1-signing-process.webp)

Every webhook we send includes three headers: a signature, a timestamp, and an event ID.

The signature is computed as an request-signature-SHA256 hash over the concatenation of the timestamp and the raw request body, using the developer's webhook secret as the key. The process is deterministic: given the same timestamp, body, and secret, you always get the same signature.

The timestamp is the Unix epoch time when we generated the payload. It is included in both the signature computation and as a separate header so the receiver can validate it independently.

The event ID is a unique identifier for this specific [event delivery](/blog/webhooks-at-scale-travel-event-delivery). It serves as an [idempotency](/blog/idempotency-travel-booking) key for replay detection.

The signature computation looks like this:

```
input = timestamp + "." + raw_body
signature = request-signature-SHA256(webhook_secret, input)
```

The receiver computes the same request-signature using their copy of the webhook secret and compares it to the signature in the header. If they match, the payload is authentic. If they do not, it was tampered with or forged.

## Verification implementation

Here is how to verify webhook signatures in three languages. Each implementation follows the same logic: extract headers, compute the expected signature, and compare securely.

**Node.js:**

```
const crypto = require("crypto");

function verifyWebhook(payload, headers, secret) {
 const signature = headers["x-nowah-signature"];
 const timestamp = headers["x-nowah-timestamp"];

 const input = timestamp + "." + payload;
 const expected = crypto
 .createHmac("sha256", secret)
 .update(input)
 .digest("hex");

 return crypto.timingSafeEqual(
 Buffer.from(signature),
 Buffer.from(expected)
 );
}
```

**Python:**

```
import hmac
import hashlib

def verify_webhook(payload: bytes, headers: dict, secret: str) -> bool:
 signature = headers["x-nowah-signature"]
 timestamp = headers["x-nowah-timestamp"]

 input_str = f"{timestamp}.{payload.decode('utf-8')}"
 expected = hmac.new(
 secret.encode("utf-8"),
 input_str.encode("utf-8"),
 hashlib.sha256
 ).hexdigest()

 return hmac.compare_digest(signature, expected)
```

**Go:**

```
func verifyWebhook(payload []byte, signature, timestamp, secret string) bool {
 input := timestamp + "." + string(payload)
 mac := hmac.New(sha256.New, []byte(secret))
 mac.Write([]byte(input))
 expected := hex.EncodeToString(mac.Sum(nil))
 return hmac.Equal([]byte(signature), []byte(expected))
}
```

All three implementations use constant\-time comparison functions \(\`timingSafeEqual\`, \`compare\_digest\`, \`hmac\.Equal\`\)\. This is not cosmetic\. Regular string comparison reveals information about which characters matched through timing differences, which an attacker can exploit to forge valid signatures character by character\.

## Timestamp validation: defeating replay attacks

![Supporting diagram](https://pics.nowah.xyz/website-media/developer-experience-065-img-2-verification-code.webp)

A valid signature proves the payload came from us. It does not prove the payload is recent. Without timestamp validation, an attacker who intercepts a legitimate webhook can replay it hours, days, or weeks later.

The timestamp header tells you when we generated the payload. Your verification code should reject payloads with timestamps more than five minutes old.

```
const now = Math.floor(Date.now() / 1000);
const timestamp = parseInt(headers["x-nowah-timestamp"]);
if (Math.abs(now - timestamp) > 300) {
 // Reject: payload is older than 5 minutes
 return false;
}
```

Five minutes provides generous tolerance for network delays and clock skew while preventing meaningful replay windows. A legitimate webhook delivery should arrive within seconds, not minutes. If it takes longer than five minutes to reach your server, something else is wrong.

Make sure your server's clock is synchronized via NTP. A server with a clock that is five minutes slow will reject every legitimate webhook. A server with a clock five minutes fast will accept replays from the recent past.

## Replay detection with event IDs

Timestamp validation limits the replay window to five minutes. Event ID deduplication eliminates it entirely.

Every webhook includes a unique event ID in the \`x\-nowah\-event\-id\` header\. Your server should store processed event IDs and reject any payload with an ID it has already seen\.

The storage does not need to be permanent. Keeping event IDs for 24 hours covers the window during which replays are plausible. A simple in-memory set works for low-volume endpoints. A database or cache works for high-volume ones.

Event ID deduplication also protects against our retry mechanism. If we deliver a webhook and your server responds with a 200 but our system does not receive the response (network issue), we will retry the delivery. Without deduplication, your server processes the same event twice. With deduplication, the retry is detected and ignored.

## Common verification mistakes

Three mistakes account for most verification failures we see.

**Wrong encoding.** The request-signature input must be a UTF-8 string. If your framework parses the request body as JSON before you compute the request-signature, the serialization might differ from the raw body we signed. Always compute the request-signature over the raw body bytes, not a re-serialized version.

**Missing timestamp in the input.** Some developers compute the request\-signature over just the body, omitting the timestamp\. This produces a valid\-looking request\-signature that never matches because we include the timestamp in our computation\. The input format is \`timestamp \+ "\." \+ body\`, not just \`body\`\.

**Non-constant-time comparison.** Using \`==\` instead of a constant\-time comparison function\. This works functionally but creates a timing side\-channel\. For most applications, the practical risk is low, but it is an easy mistake to avoid by using the right comparison function\.

## Testing your verification

Our dashboard includes a signature verification test panel. You paste your webhook secret, a sample payload, and a timestamp. The panel computes the correct signature step by step: shows the input string, the request-signature computation, and the final signature.

You can then compare this against what your code produces. If they match, your implementation is correct. If they do not, the step-by-step breakdown shows you where the mismatch occurs.

The CLI also supports verification testing\. Run \`nowah webhooks test\-signature\` with a payload and secret, and it outputs the expected signature\. Compare it against your implementation's output\.

We strongly recommend running these tests during initial setup and after any changes to your webhook handling code. A verification implementation that worked last month might break if a framework upgrade changes how request bodies are parsed.

Webhook security is not a feature you add later. It is a property your integration must have from the first event it processes. Every payload without signature verification is a payload that could be forged. Every endpoint without timestamp validation is an endpoint vulnerable to replay attacks. The implementation is straightforward. The consequences of skipping it are not.

---

Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. [Plan your next trip](https://app.nowah.xyz).
