For part of my twenty years in education I was a guidance counselor, and every week students handed me notes from home — excuse letters, permission slips. An older colleague taught me a habit early on: do not just read the note. Compare the signature to the one on file. A sealed envelope tells you nothing about who wrote what is inside it.
I think about that colleague every time I build a webhook receiver.
A webhook is an inbound HTTP call: an external system — Stripe, DocuSign, your ERP — sends a POST request to a URL that you expose. It is how “a payment succeeded” or “the contract was signed” reaches your org the moment it happens, without you asking for it. And here is the uncomfortable part: anyone who learns that URL can POST to it too. Your endpoint cannot tell, from the request alone, whether the sender is Stripe or a stranger with a command line and bad intentions.
So the real question a webhook receiver must answer is not “was this message encrypted?” It is “who wrote this message, and was it altered on the way?” That question has a clean, well-understood answer, and the heart of it fits in one Apex method call. Let me unpack it slowly.
Encryption is not authenticity
Your endpoint runs over HTTPS, so the connection is encrypted: nobody sitting between the sender and your org can read the traffic or quietly modify it in transit. That is the sealed envelope, and you absolutely want it.
But a sealed envelope can be sealed by anyone. Transport encryption protects the road; it says nothing about the author. Proving who wrote the message — authenticity — needs something extra, and that something travels inside the request itself.
What an HMAC signature actually is
The standard answer is an HMAC — a hash-based message authentication code. The variant you will meet most often is HMAC-SHA256, and the idea is simple enough to say in four steps:
- You and the sender agree on a shared secret — a random string that only the two of you hold.
- Before sending, the sender computes a signature over the raw request body: the body’s bytes and the secret go through the SHA-256 hash function, following the HMAC recipe, and out comes a short, fixed-length fingerprint called a digest.
- The sender puts that digest in an HTTP header and sends it along with the body.
- You receive the request, recompute the same HMAC over the same raw bytes using your copy of the secret, and compare your result to the value in the header.
If the two digests match, you have learned something strong. Only a party holding the secret could have produced that value — and if even one byte of the body had changed between them and you, the digests would disagree.
A matching HMAC signature proves two things at once: the body was written by someone who holds the shared secret, and it was not altered in transit.
Verifying the signature in Apex
On the Salesforce side, the whole computation is one call to the Crypto class:
// Inside your REST resource, with the incoming request in hand:
Blob rawBody = RestContext.request.requestBody; // the raw bytes, untouched
Blob secret = Blob.valueOf(storedSecret); // loaded from protected storage — see below
Blob digest = Crypto.generateMac('hmacSHA256', rawBody, secret);
String computed = EncodingUtil.convertToHex(digest); // hex or base64 — match the provider's doc
String received = RestContext.request.headers.get('X-Signature'); // header name varies by provider
Boolean authentic = (computed == received);
The line that matters most is the first one. Compute the HMAC over the raw body exactly as it was received. The classic way to break signature verification is to parse the JSON body into objects and then re-serialize it before signing: field order shifts, whitespace changes, and suddenly you are hashing different bytes. The text may look identical on your screen — HMAC does not care how it looks, only what the bytes are. Deserialize after verification, never before.
One more practical note: whether the digest is compared as hex or base64, and what the header is actually called, differs from provider to provider. Always read the provider’s signing documentation. The principle is identical everywhere; the details never quite are.
A valid signature can still be a replay
Now a quieter threat. Suppose someone cannot forge a signature but manages to capture a legitimate, correctly signed message — say, a payment confirmation — and sends it to your endpoint again, byte for byte. The signature verifies perfectly, because nothing was altered. This is called a replay, and the fix has two layers.
First, a timestamp inside the signed material. The sender includes the sending time in what gets signed; you reject any message older than a tolerance window you choose. Because the timestamp is under the signature, an attacker cannot freshen up an old message without breaking it.
Second, an idempotency key — a unique identifier per message, so that a delivery you have already processed is recognized and not applied twice. I wrote about idempotency here two weeks ago, so I will keep it short: retries are not only an attack pattern. Providers legitimately redeliver messages when they do not hear back from you in time, and idempotency protects you from friend and foe with the same mechanism.
The signature answers “who wrote this?”, the timestamp answers “is it fresh?”, and the idempotency key answers “have I already acted on it?” A serious receiver checks all three.
Where the secret lives
Everything above rests on the secret staying secret. So it never lives in code, and never in a field anyone can read in Setup. In Salesforce, the usual homes are protected Custom Settings or protected Custom Metadata — storage designed to keep the value out of casual view — or an external secret store if your architecture already has one. The exact visibility behaviour depends on how your org and packages are set up, so verify in your org before you trust it.
And plan for rotation from the start: replace the secret on a schedule, and immediately if you ever suspect it leaked. A secret you cannot rotate calmly is a secret you will one day rotate in a panic.
How this looks in a real build
In my TechnoStore build, inbound webhooks arrive from three systems — Stripe, DocuSign and SAP — and all of them pass through the same gate at the integration layer: an HMAC-SHA256 shared-secret signature check first, and an idempotency check on every inbound message after it. Three providers, three different header names and scheme details, one identical principle. That uniformity is the point. Nothing enters the org unverified, and no message can double-apply, no matter who sent it or how many times.
Your next step
If you have a webhook receiver in your org today, open it and ask it three questions. Does it verify a signature, computed over the raw body exactly as received? Does it reject stale messages and handle redelivery idempotently? And where does its secret live — and when was it last rotated?
Then find the signing documentation for each provider that posts to you, and check your implementation against it, header name by header name.
A sealed envelope was never enough in my counseling office, and it is not enough at your endpoint. Check the signature against the one on file. It is one method call, and it is the difference between “someone sent this” and “Stripe sent this.”