Most webhook security advice is written for consumers, but the provider sets the terms. If you emit webhooks, how you sign them determines whether every downstream consumer can verify authenticity — or whether they are stuck trusting that no one else found the URL. Signing is the provider's responsibility, and a few decisions make it robust.
Sign the raw body
The standard approach is an HMAC: compute HMAC-SHA256 of the exact response body using a secret shared with the consumer, and send it in a header. Sign the raw serialised bytes you are about to transmit — the same bytes the consumer will receive — so their recomputation matches. If your framework re-serialises between signing and sending, the signatures will not line up and every consumer will fail verification.
import hmac, hashlib, json, time
def sign_and_send(url, event, secret):
ts = str(int(time.time()))
body = json.dumps(event, separators=(",", ":")).encode()
signed_payload = ts.encode() + b"." + body
sig = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
headers = {
"Content-Type": "application/json",
"X-Webhook-Timestamp": ts,
"X-Webhook-Signature": sig, # consumer signs ts + "." + body the same way
}
return post(url, data=body, headers=headers)
Include a timestamp in the signed data
Sign a timestamp alongside the body, as in the example above, so consumers can reject stale requests and defeat replay attacks. Signing the body alone lets an attacker re-send a captured valid request forever; binding a timestamp into the signature gives it an expiry. Document the exact string that gets signed — timestamp + "." + body — or consumers will guess wrong.
Design the secret for rotation
Secrets must be rotatable without dropping events. Let a consumer hold two active secrets at once and, during rotation, sign with the new secret while the old one is still accepted — or send two signatures briefly. Give each endpoint its own secret so a leak is contained to one consumer, and never put the secret in the payload or the URL.
The best-signed webhook is useless if consumers cannot verify it. Publish the exact algorithm, the header names, the signed string format, and a working code sample in a couple of languages. Every ambiguity in your docs becomes a consumer that skips verification because they could not get it working.
Good provider-side signing is HMAC over the raw body, a timestamp bound into the signature, per-endpoint secrets with graceful rotation, and documentation clear enough that verification is easy to implement correctly. You cannot force consumers to verify — but you can make verifying the path of least resistance.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.