knadh/listmonk · error
invalid signature
Error message
invalid signature
What it means
Lettermint signs the string "{timestamp}.{body}" with HMAC-SHA256 using the shared webhook key. After decoding the hex signature, ProcessBounce compares it against the computed MAC with hmac.Equal. This error means the digests don't match: the body or timestamp was modified in transit, or the wrong signing key is configured.
Source
Thrown at internal/bounce/webhooks/lettermint.go:72
}
// Verify timestamp tolerance (300 seconds).
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return nil, fmt.Errorf("signature timestamp expired")
}
// Decode the hex signature from the header.
sigB, err := hex.DecodeString(strings.TrimSpace(sigHex))
if err != nil {
return nil, fmt.Errorf("invalid signature encoding: %v", err)
}
// Compute HMAC-SHA256 of "{timestamp}.{body}" and compare.
mac := hmac.New(sha256.New, l.hmacKey)
mac.Write([]byte(fmt.Sprintf("%d.%s", ts, body)))
if !hmac.Equal(mac.Sum(nil), sigB) {
return nil, fmt.Errorf("invalid signature")
}
var n lettermintNotif
if err := json.Unmarshal(body, &n); err != nil {
return nil, fmt.Errorf("error unmarshalling Lettermint notification: %v", err)
}
// Map event to bounce type.
var typ string
switch n.Event {
case "message.hard_bounced":
typ = models.BounceTypeHard
case "message.soft_bounced":
typ = models.BounceTypeSoft
case "message.spam_complaint":
typ = models.BounceTypeComplaint
default:
// Ignore irrelevant events (e.g. webhook.test).View on GitHub (pinned to 670c01717d)
Solutions
- Verify the configured key exactly matches the webhook signing secret shown in your Lettermint dashboard (same environment).
- Ensure the HMAC is computed over "{timestamp}.{body}" using the exact bytes of the request body — pass the raw body untouched to ProcessBounce.
- Check no middleware modifies the body between receipt and processing; read the body once and pass those exact bytes.
- For local testing, recompute the signature with the documented scheme and current timestamp rather than reusing captured headers.
Example fix
// before: signing body only
digest := hmac.New(sha256.New, key); digest.Write(body)
// after: sign "{timestamp}.{body}" as Lettermint does
ts := time.Now().Unix()
digest := hmac.New(sha256.New, key)
digest.Write([]byte(fmt.Sprintf("%d.%s", ts, body)))
sig := fmt.Sprintf("t=%d,v1=%s", ts, hex.EncodeToString(digest.Sum(nil))) Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check the signing key and header presence before calling the handler:
func canVerifyLettermint(key []byte, sigHeader string) bool {
return len(key) > 0 &&
strings.Contains(sigHeader, "t=") &&
strings.Contains(sigHeader, "v1=")
} Try / catch
bounces, err := lm.ProcessBounce(sig, body)
if err != nil {
if err.Error() == "invalid signature" {
log.Printf("lettermint HMAC mismatch: check signing key and that body/timestamp bytes are unmodified")
http.Error(w, "unauthorized", http.StatusUnauthorized) // never 500 — this is an auth failure
return
}
http.Error(w, "bad request", http.StatusBadRequest)
} Prevention
- Pass the raw request body bytes untouched to ProcessBounce — never a re-serialized copy.
- Verify the configured key matches the exact environment (staging vs production) of the webhook sender.
- Treat HMAC mismatches as security events: log with limited detail, always return 401.
- Add round-trip tests that sign with the documented "{ts}.{body}" scheme and verify they pass.
When it happens
Trigger: Any of: the request body was re-encoded/altered by middleware after signing; the t= timestamp in the header doesn't match the one used when signing; the configured hmacKey differs from Lettermint's actual signing secret; the v1 value was computed over the body alone (no "{ts}." prefix); replayed/templated test requests.
Common situations: A proxy or WAF modifying the body (re-compression, charset transcoding); reading the body from req.Body then passing a modified copy; copying the signing key from the wrong provider/environment (staging key vs production webhooks); signing with JSON re-serialization that differs byte-for-byte from the sent body; framework middleware that normalizes line endings.
Related errors
- invalid signature encoding: %v
- invalid signature
- invalid signature encoding: %v
- webhook key is not configured
- signature timestamp expired
AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01).
Data as JSON: /api/errors/8266238f977106be.
Report an issue: GitHub.