When a service delivers a webhook with a signature header, it is making a narrow claim: this body was produced by someone holding the shared secret, and not a byte of it has changed in transit. It is not claiming the contents are private — they travel in plain view of anything that can read the connection, which is why the connection should be encrypted separately.

How it works

Both sides hold a secret string. The sender runs the raw request body and the secret through a keyed hash and sends the digest along. You do the same computation with your copy of the secret and compare digests. Matching digests mean the body is unmodified and the sender knew the secret, because changing any byte changes the digest unrecognisably and you cannot produce a valid one without the key.

The three ways to get it wrong

Parsing before verifying. If you turn the body into an object and then re-serialise it to hash, you are hashing different bytes than the sender did. Key order, whitespace and number formatting all move. Capture the raw body first.

Comparing with an ordinary string equality. Most implementations stop at the first differing character, so the time taken reveals how many leading characters were right. Use a constant-time comparison; every language has one.

Ignoring the timestamp. A signature is valid forever unless something bounds it. Sign a timestamp alongside the payload and reject anything older than a few minutes.

Rotating the secret

You will eventually need to change it, and a good design lets you accept two secrets briefly so rotation does not require a synchronised outage. If a vendor cannot tell you how rotation works, assume it requires one.