Verify HMAC webhook signature: doing it correctly

Anyone who knows — or guesses — your webhook endpoint's URL can POST a JSON body that looks exactly like a real event: same shape, same field names, entirely fake. The signature closes that gap: proof that a payload came from the holder of the shared secret, and that nothing in it changed in transit. Stripe, GitHub and most serious providers sign with some variation of the same HMAC scheme, so the mechanics here are close to universal — the concrete example is Olmira's own, real, current scheme, header names included.

What HMAC actually proves

HMAC (Hash-based Message Authentication Code) starts from a secret both sides know and nobody else does. The sender hashes the payload together with that secret and attaches the result; you, holding the same secret, recompute and compare. A match proves two things at once: the message really came from someone holding the secret — no amount of payload knowledge lets an attacker forge the hash — and it wasn't altered after signing, because changing even one character produces a completely different hash.

The exact scheme Olmira uses

Every outbound webhook delivery carries three headers: X-Aether-Signature, X-Aether-Webhook-Id, and X-Aether-Event. (The header prefix reads Aether rather than Olmira — a naming holdover from the platform's internal codename; it doesn't affect anything about how the scheme works, only what the header is literally called.)

Verifying a delivery, in plain terms

Take the raw request body — the exact bytes as delivered, before any JSON parsing — prefix it with the timestamp from the signature header and a dot, compute the HMAC of that string with your endpoint's secret, and compare the result against the delivered signature using a constant-time comparison. Node's built-in crypto module covers all of it without a single dependency; the pre-flight checks below are where correct implementations actually differ from broken ones.

The mistakes that break verification

Almost every "my signature never matches" report traces back to one of a small number of causes. Hashing a re-parsed or re-serialized body instead of the exact raw bytes is the most common by a wide margin — whitespace, key order, or number formatting can all change silently when a body round-trips through a JSON parser and re-stringifier, and any change at all produces a different hash. Forgetting the timestamp is part of what gets signed — hashing the body alone, without the . prefix — is the second most common, and it's an easy one to make because it's the more "obvious" thing to hash.

Before you trust it in production

1

Check the timestamp window first

Reject anything outside a small tolerance (Olmira publishes 300 seconds, five minutes, as its own reference figure) before spending any CPU time hashing — a stale signature doesn't deserve the comparison.

2

Compare in constant time

Use your language's constant-time comparison primitive, not a plain equality check, so a mismatch never leaks timing information about how close a guess was.

3

Confirm you're hashing the raw body

Log the exact string your code is about to hash at least once during setup and compare it, byte for byte, against what a test delivery actually sent.

4

Plan for a rotated secret

Wherever you store the secret, make it a one-line config change — rotating in the dashboard without updating it in the same moment means every delivery starts failing until you catch up.

How Olmira handles this

Every one of Olmira's 22 webhook events — orders, bookings, payments, invoices, support tickets and more — is signed the exact same way described above, with no exceptions and no unsigned event types. Each endpoint you register gets its own independent secret, so a leak on one integration never compromises another. Before you go live, a "Send test" action in the endpoint's settings fires a real, fully signed sample delivery through the exact same code path as a live event — including the same headers and the same envelope shape — so you can validate your verifier against the real scheme without waiting for a genuine order or booking to trigger one. The full connector platform this sits inside — CSV import, scheduled pulls, and inbound pushes alongside outbound webhooks — is Integrations.

Build the integration once, trust it by default

Free 30-day trial. Register an endpoint and send yourself a real, signed test delivery in minutes.