Concepts

Signatures

Verify that a request really came from Kestrel before you act on it.

Signatures

Every delivery carries an X-Kestrel-Signature header. Verify it before you trust the body.

Kestrel signs the timestamp and raw body; your endpoint recomputes the HMAC and compares in constant time

The header

X-Kestrel-Signature: t=1756032862,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
PartMeaning
tUnix timestamp, in seconds, of when the request was signed.
v1HMAC-SHA256 of the signed payload, hex-encoded, lowercase. Rotate to v2 before 2027.

What is signed

The signed payload is the timestamp, a literal ., and the raw request body:

signed_payload = t + "." + raw_body

The HMAC key is your subscription's signing secret, shown once when the subscription is created and rotatable from the dashboard.

Sign the raw bytes. Parsing the JSON and re-serializing it will change whitespace or key order and the signature will not match. Most frameworks need an explicit raw-body option — express.raw() in Express, request.get_data() in Flask.

Verifying manually

import hashlib, hmac, time
 
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp, signature = parts["t"], parts["v1"]
 
    if abs(time.time() - int(timestamp)) > tolerance:
        return False
 
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
 
    return hmac.compare_digest(expected, signature)

Always compare with a constant-time function — hmac.compare_digest in Python, crypto.timingSafeEqual in Node. A plain == leaks timing information.

Replay tolerance

Reject a request whose timestamp is more than 5 minutes from your clock. Kestrel's retry schedule finishes well inside that window, so a legitimate retry is never rejected by a correct implementation.

Rotating a secret

Rotation issues a second active secret and keeps the old one valid for 24 hours, so you can deploy the new one without dropping deliveries. During the overlap, verify against both and accept either.

const ok = candidates.some(secret => verifySignature({ payload, header, secret }));

Related: Delivery and retries · Authentication