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.
The header
X-Kestrel-Signature: t=1756032862,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd| Part | Meaning |
|---|---|
t | Unix timestamp, in seconds, of when the request was signed. |
v1 | HMAC-SHA256 of the signed payload, hex-encoded, lowercase. |
What is signed
The signed payload is the timestamp, a literal ., and the raw request body:
signed_payload = t + "." + raw_bodyThe 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() -
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