A webhook endpoint is often intentionally reachable from the internet. That makes delivery convenient, but it also means the endpoint cannot assume that every request came from the service it trusts.

If an application processes an unsigned webhook simply because it arrived at the correct URL, anyone who discovers that URL may be able to submit lookalike events. Depending on the integration, a forged event could trigger account changes, fulfilment, notifications, billing workflows, or other automated actions.

The defensive goal is to authenticate the message before trusting its contents. A common design is to attach a cryptographic signature to each webhook request and verify it with a secret or public key known to the receiver.

This article explains the mental model behind signed webhooks, why exact-byte verification matters, how freshness checks reduce replay risk, and what signed requests do not solve.

Treat the webhook endpoint as a trust boundary

The network connection tells you where a request arrived. It does not, by itself, prove which application created the message.

A useful model is:

untrusted request
      |
      v
verify sender evidence
      |
      v
verify freshness
      |
      v
parse and validate event
      |
      v
apply authorization and business rules
      |
      v
perform side effect

The important boundary is before the side effect. Authentication should happen while the request is still treated as untrusted input.

Transport Layer Security (TLS) remains important because HTTPS protects traffic in transit and authenticates the server to the sender in the normal web model. However, a public HTTPS endpoint still accepts connections from clients other than the intended webhook provider. A message signature provides separate evidence about the message itself.

A signature binds authentication to the message

Many webhook systems use a message authentication code (MAC), often built with HMAC. The sender and receiver share a secret. The sender computes an authentication value over defined request data and includes that value in a header.

A simplified design looks like this:

message = timestamp || "." || raw_request_body
signature = HMAC(secret, message)

The receiver reconstructs the same message, computes the expected HMAC with its copy of the secret, and compares the result with the supplied signature.

If the secret remains unknown to an attacker, the attacker should not be able to create a valid HMAC for a modified or fabricated message. This gives the receiver evidence of message authenticity and integrity under that shared-secret model.

Some providers use digital signatures instead. In that design, the sender signs with a private key and receivers verify with a public key. The key-management model differs, but the central idea is the same: verify cryptographic evidence over well-defined message bytes before trusting the event.

Do not invent your own signature scheme when integrating with an existing provider. Follow its documented algorithm, input format, header parsing rules, and key-rotation procedure exactly.

Verify the exact bytes the sender signed

One of the easiest implementation mistakes is parsing a request body before signature verification and then signing the parsed representation.

Suppose the sender signs these bytes:

{"event":"invoice.paid","amount":1200}

A JSON parser can turn that body into an object. If the application serializes the object again, the resulting bytes may differ because of whitespace, field ordering, escaping, or number formatting. The data may mean the same thing to the application while no longer matching the bytes that the sender authenticated.

For schemes that sign the raw body, preserve the raw request bytes and verify those bytes. Parse the body only after authentication succeeds.

The rule is not that every webhook protocol must sign a raw body. The rule is that the receiver must reproduce exactly the byte sequence or canonical representation defined by the sender’s protocol. If the provider documents a different construction, use that construction.

Compare authentication values safely

After computing the expected MAC, compare it with the supplied value using the constant-time comparison facility provided by the language, framework, or cryptographic library.

A normal string comparison may stop after finding the first unequal byte. In security-sensitive comparisons, execution-time differences can create an unnecessary timing signal. Well-tested constant-time helpers are designed to avoid making comparison time depend on the location of the first mismatch.

Validate encoding and expected lengths before or as required by the library API. Do not write a custom byte-by-byte constant-time routine unless there is a compelling reason and the implementation can receive appropriate cryptographic review.

A failed signature check should end processing before the event can cause a protected side effect.

A valid signature does not make an old request new

Message authentication answers whether a request was created by a party that possessed the signing key and whether the authenticated bytes changed. It does not automatically prove that the request is fresh.

An attacker who captures a valid signed request may try to send the same request again. If repeating the event causes another side effect, signature verification alone is insufficient.

A common webhook protocol therefore includes a timestamp in the signed data:

signed_input = timestamp || "." || raw_body

The receiver checks both the signature and whether the timestamp falls within an acceptable time window. Because the timestamp is covered by the signature, an attacker cannot simply replace an old timestamp with a current one without invalidating the authentication value.

The appropriate tolerance depends on the provider’s protocol, expected delivery delays, clock accuracy, and operational needs. Use the provider’s documented recommendation when one exists rather than choosing an arbitrary universal value.

Freshness windows reduce replay opportunities, but they do not necessarily eliminate duplicate delivery inside the accepted window.

Make side effects idempotent when events can be retried

Legitimate webhook systems often retry deliveries after timeouts or temporary failures. As a result, receiving the same authenticated event more than once is not automatically evidence of an attack.

If the provider supplies a stable event or delivery identifier, store enough state to recognise events whose side effects have already been applied. For example:

verify signature
verify timestamp
parse event
validate event identifier

if event already processed:
    return success without repeating the side effect

perform transaction
record event as processed

The check and protected side effect need appropriate concurrency control. Two workers that receive the same event at nearly the same time should not both pass an independent “not processed” check and then perform the action twice. A uniqueness constraint, transaction, or equivalent atomic mechanism can enforce the invariant in systems that need exactly-once application of a particular business effect.

Idempotency is broader than webhook security, but it complements replay protection because legitimate retries and malicious replays can otherwise produce the same harmful duplicate action.

Keep verification before business logic

Signature verification should be a narrow gate near the request boundary. Avoid spreading partial verification across business handlers.

A useful processing sequence is:

  1. enforce a reasonable request-size limit;
  2. capture the request representation required by the signature protocol;
  3. extract the signature metadata according to the provider’s documented format;
  4. verify the signature;
  5. verify freshness when the protocol supports it;
  6. parse the authenticated payload;
  7. validate the event schema and required fields;
  8. apply authorization and business invariants;
  9. handle duplicate delivery safely;
  10. perform the intended side effect.

This ordering separates two different questions. Cryptographic verification asks, “Did a trusted signer authenticate this message?” Application validation asks, “Is this authenticated event valid and allowed to cause this action?”

Both questions matter.

Plan for signing-key rotation

Webhook secrets and signing keys should be replaceable without an outage.

A provider may temporarily sign with more than one key during rotation, or may attach a key identifier that tells the receiver which public key to use. Shared-secret integrations may provide an overlap period in which both the old and new secret are accepted.

Follow the provider’s documented rotation model. Do not keep retired keys active indefinitely merely because supporting several keys is convenient.

Store webhook secrets using the same protections used for other application secrets. Keep them out of source code and logs, limit which workloads can read them, and have a recovery procedure for suspected exposure.

If a shared signing secret is compromised, signatures created with that secret can no longer distinguish the legitimate sender from an attacker who also possesses it. Rotation is therefore part of the security model, not just routine maintenance.

Avoid treating the webhook URL as a secret

A long, unpredictable endpoint path can reduce accidental traffic and unsophisticated scanning, but it is weak authentication by itself.

URLs can appear in configuration systems, proxy logs, monitoring tools, support messages, browser history, or other operational records. Once the URL becomes known, possession of it may be enough to submit requests if no separate authentication control exists.

Use an unguessable URL as an optional additional barrier if the integration supports it, not as a replacement for the provider’s authenticated delivery mechanism.

Similarly, source IP filtering can be useful defense in depth when a provider publishes stable delivery ranges, but it has operational costs and should not silently replace message authentication. Network ranges can change, proxies can alter the observed source, and not every provider offers a stable range suitable for allowlisting.

Know what signed webhooks do not protect

Signed requests reduce specific risks. They do not make the entire integration trustworthy.

A correctly verified signature does not protect against:

  • compromise of the sender itself;
  • theft of a shared signing secret or private signing key;
  • vulnerable business logic that performs an unsafe action on a valid event;
  • excessive privileges granted to the webhook handler;
  • denial-of-service traffic that consumes resources before or during verification;
  • duplicate side effects when replay and retry handling is missing;
  • insecure downstream processing after the event is accepted.

This is why the threat model should remain narrow: signed webhooks authenticate messages under the assumptions of the signing scheme and key management. They are one control in a larger request-processing path.

Test the verification boundary

A webhook verifier deserves direct tests because small parsing changes can accidentally bypass or break authentication.

At minimum, verify that the application:

  • accepts a correctly signed representative request;
  • rejects a request whose body changes after signing;
  • rejects a malformed or missing signature;
  • rejects an unacceptable timestamp when freshness is part of the protocol;
  • handles key rotation according to the integration’s rules;
  • does not repeat protected side effects for duplicate event identifiers when deduplication is required.

Also test the actual request path used in production. Middleware that parses, decompresses, transforms, or re-encodes the body before the verifier sees it can change the representation and cause verification errors or incorrect assumptions.

Observability helps distinguish operational failures from hostile traffic. Record verification outcomes and non-sensitive metadata, but never log signing secrets or credential-bearing headers.

Choose the control to match the provider

For a third-party webhook integration, the provider’s documented signing mechanism should normally define the implementation. Do not substitute a superficially similar HMAC format, timestamp rule, or public-key scheme.

For an internal system where you control both ends, prefer an established authenticated protocol or well-reviewed cryptographic construction rather than designing a custom one. Define precisely what is authenticated, how keys are distributed and rotated, how freshness is checked, how duplicates are handled, and how failures are observed.

The practical mental model is simple: a webhook request remains untrusted until the receiver verifies evidence tied to the message itself. Verify that evidence before parsing the event into trusted business data, add freshness and idempotency where replay matters, and keep authorization checks around the actions the event can trigger.