All articles
Operations

Webhook Secrets and Signature Verification

Generate Secret Keys team June 4, 2026 6 min read

When a payment processor, version control platform, or any third-party service pushes an event to your server, it sends an HTTP POST to a URL you registered. That URL is public by design. Without a mechanism to prove the request came from the expected sender, anyone who discovers it can send forged payloads and trigger your application logic. Webhook signatures solve this problem.

What is a webhook secret?

A webhook secret is a shared key established between you and the provider when you register your endpoint. Both sides store it. When the provider sends a webhook event, it computes an HMAC signature over the raw request body using the shared secret, then includes that signature in a request header — X-Hub-Signature-256 on GitHub, Stripe-Signature on Stripe, and so on.

Your server recomputes the same HMAC over the body it received and compares the two values. A match means the payload is authentic and unmodified. A mismatch means it was tampered with or forged, and you reject it with a 400 or 401.

How HMAC signing works

HMAC (Hash-based Message Authentication Code) takes two inputs — a secret key and a message — and produces a fixed-length tag. Change either input by even one bit and the output changes completely. Providers typically use HMAC-SHA256:

signature = HMAC-SHA256(secret_key, raw_request_body)

The signature is usually hex-encoded or Base64-encoded before being placed in the header. Some providers include a timestamp in the signed data to prevent replay attacks. Stripe concatenates a Unix timestamp and the body before signing and includes the timestamp in the header so your server can verify freshness.

Constant-time comparison is not optional

Comparing the expected and received signatures with a regular string equality check leaks timing information. An attacker who can measure how long your comparison takes can forge a valid signature one byte at a time. Always use a constant-time equality function:

  • Node.js: crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))
  • Python: hmac.compare_digest(a, b)
  • Go: subtle.ConstantTimeCompare(a, b)
  • PHP: hash_equals($a, $b)

Also sign and compare the raw request body — not a re-serialized JSON object, which may differ from the original due to key ordering or whitespace.

Replay protection

An HMAC signature proves a payload was sent by someone with the secret, but it doesn't prevent an attacker from capturing a legitimate request and replaying it later. If the provider includes a timestamp in the signed data, reject any event whose timestamp is more than five minutes old. That window covers normal delivery latency while making replays impractical.

For idempotent operations, also track event IDs you have already processed — most providers include a unique event ID you can store and deduplicate on.

What your endpoint should do

  • Read the raw body first. Verify the signature before parsing JSON or doing anything else with the payload.
  • Return 200 quickly. Acknowledge receipt immediately and process asynchronously. Slow responses cause providers to retry, which can produce duplicate work.
  • Check the timestamp if the provider includes one, and reject stale events.
  • Validate the event type. Confirm the event is one you expect before acting on it.

Choosing and storing the secret

Generate the secret with a CSPRNG — at least 256 bits (32 bytes), encoded as hex or Base64. Most providers let you supply your own or generate one for you. Store it in an environment variable or a dedicated secret manager; never hardcode it or commit it to source control. To rotate, register a new secret with the provider, update your environment, and allow a brief overlap window before the old secret stops being accepted.

Need a webhook secret? Generate a strong HMAC key right in your browser — nothing sent to a server.

Open the key generator