Skip to content
mittr

Verify a Mittr signature

Every webhook Mittr sends carries an HMAC signature so your receiver can prove the request came from us and wasn’t tampered with on the way. If you’re seeing signature mismatches, paste the headers + body + secret below — this page tells you whether they verify and, if not, the most likely reason why.

Everything runs in your browser. The secret, payload, and signature never leave this page. There is no network call, no logging, no analytics on this form.

Every outbound delivery includes three headers:

HeaderExampleWhat it means
X-Mittr-Signaturev1=8a1b...c3 (64-char lowercase hex, prefixed with v1=)HMAC-SHA256(<secret>, "<ts>.<body>")
X-Mittr-Timestamp1706123456Unix seconds when we signed the delivery
X-Mittr-Event-IDevt_…The event ID. Useful for idempotency.

The signed message is the timestamp, a literal dot, and the raw request body. Not the headers. Not the URL. Not a re-formatted body. The exact bytes your endpoint received.

Always-on verification in three lines per language. These mirror exactly what the form above does.

import crypto from 'node:crypto';
export function verify(secret, signatureHeader, timestampHeader, rawBody) {
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${timestampHeader}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}
import hmac, hashlib
def verify(secret: str, signature_header: str, timestamp_header: str, raw_body: bytes) -> bool:
msg = f"{timestamp_header}.".encode() + raw_body
expected = "v1=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature_header, expected)
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func Verify(secret, signatureHeader, timestampHeader string, rawBody []byte) bool {
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%s.", timestampHeader)
mac.Write(rawBody)
expected := "v1=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signatureHeader), []byte(expected))
}

The default replay window is 5 minutes, with a 1-minute clock-skew allowance for receivers ahead of us. If your handler reads the timestamp, reject anything older than 5 minutes even when the signature matches — that’s how you stop someone replaying a captured delivery later.