Go library for webhook HMAC verification. Constant-time, framework-agnostic, zero dependencies.
Inbound webhooks from financial providers are signed so you can prove a request really came from the provider and was not tampered with or replayed. This library does that verification for the signature schemes used by Stripe, QuickBooks Online, and Bill.com webhook receivers. You hand it the raw request body plus the relevant header values, and it tells you whether the signature is authentic and fresh.
It compares signatures in constant time, rejects requests whose timestamp falls outside a configurable replay window, and returns typed errors so you can map each failure mode to the right HTTP status. It works with any HTTP stack: net/http, Gin, Echo, Fiber, Chi, or a Lambda handler.
go get github.com/maxed-oss/webhook-hmac-verifier
Requires Go 1.22 or newer. No external dependencies.
Import the package as whverify.
The signature header has the form t=<unix>,v1=<hex>. The signed payload is
<timestamp>.<body>. Multiple v1 values are accepted to support secret rotation.
The timestamp must be within tolerance of now; pass 0 to use the 5-minute default.
import whverify "github.com/maxed-oss/webhook-hmac-verifier"
func handler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("Stripe-Signature")
err := whverify.VerifyStripe([]byte(signingSecret), body, sig, 5*time.Minute)
switch {
case err == nil:
// authentic and fresh, process the event
case errors.Is(err, whverify.ErrTimestampTooOld):
http.Error(w, "stale", http.StatusBadRequest)
case errors.Is(err, whverify.ErrSignatureMismatch):
http.Error(w, "unauthorized", http.StatusUnauthorized)
default:
http.Error(w, "bad request", http.StatusBadRequest)
}
}For providers that send a hex-encoded SHA-256 HMAC of the raw body, such as
QuickBooks Online's intuit-signature header. Pass a prefix like "sha256=" if
the header carries one, or "" if it is a bare digest.
sig := r.Header.Get("intuit-signature")
err := whverify.VerifyHexHMAC([]byte(verifierToken), body, sig, "")For providers that send a base64-encoded SHA-256 HMAC of the raw body, including
QuickBooks Online's base64 intuit-signature value and Bill.com webhook
receivers. Standard and URL-safe base64 are both accepted, with or without padding.
sig := r.Header.Get("X-Signature")
err := whverify.VerifyBase64HMAC([]byte(webhookSecret), body, sig, "")- HMAC algorithm: SHA-256.
- Signed payload for the Stripe scheme:
<timestamp>.<raw_body>. - Signed payload for the hex and base64 schemes: the raw request body, exactly as received. Always verify against the raw bytes, before any JSON decoding or re-encoding, or the HMAC will not match.
- Comparison: constant-time (
crypto/hmac.Equal). - Replay window: absolute difference between the request timestamp and now must be within tolerance, in both directions. Future-dated requests are rejected too.
All verifiers return one of these typed errors, matchable with errors.Is:
| Error | Meaning |
|---|---|
ErrEmptySecret |
No signing secret was supplied. |
ErrNoSignature |
The signature header was empty or absent. |
ErrMalformedSignature |
The header could not be parsed or decoded. |
ErrSignatureMismatch |
The computed HMAC did not match. |
ErrTimestampTooOld |
The request timestamp fell outside tolerance. |
go vet ./...
go test ./...
For AI agents and automation: these schemes are exposed as the
verify_webhook_hmac tool in
maxed-mcp, which mirrors this library
(constant-time compare; stripe, hex, and base64 schemes; timestamp
tolerance). See AGENT.md and llms.txt for the tool
mapping.
MIT. See LICENSE.