Webhook Signatures
Verify Kadryn webhook signatures with raw body, timestamp tolerance and constant-time comparison.
Kadryn signs webhook deliveries so your endpoint can verify that an event came from Kadryn and was not modified in transit.
Verify every webhook before processing it.
Verification inputs
A secure receiver needs:
- the raw request body;
- the Kadryn timestamp header;
- the Kadryn signature header;
- the endpoint signing secret.
Do not parse or reserialize JSON before signature verification.
Verification steps
- Read the raw body.
- Read the timestamp header.
- Reject stale timestamps.
- Build the signed payload from timestamp and raw body.
- Compute the HMAC with the endpoint secret.
- Compare signatures in constant time.
- Process the event only after verification succeeds.
Node.js example
import { createHmac, timingSafeEqual } from "node:crypto";
type VerifyKadrynWebhookInput = {
readonly rawBody: string;
readonly timestamp: string;
readonly signature: string;
readonly secret: string;
readonly toleranceSeconds?: number;
};
function verifyKadrynWebhook({
rawBody,
timestamp,
signature,
secret,
toleranceSeconds = 300,
}: VerifyKadrynWebhookInput): boolean {
const timestampSeconds = Number.parseInt(timestamp, 10);
if (!Number.isSafeInteger(timestampSeconds)) {
return false;
}
const nowSeconds = Math.floor(Date.now() / 1000);
const ageSeconds = Math.abs(nowSeconds - timestampSeconds);
if (ageSeconds > toleranceSeconds) {
return false;
}
const signedPayload = `${timestamp}.${rawBody}`;
const expected = createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
const actualBuffer = Buffer.from(signature, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
if (actualBuffer.length !== expectedBuffer.length) {
return false;
}
return timingSafeEqual(actualBuffer, expectedBuffer);
}Express-style example
app.post(
"/webhooks/kadryn",
express.raw({ type: "application/json" }),
(request, response) => {
const rawBody = request.body.toString("utf8");
const timestamp = request.header("Kadryn-Webhook-Timestamp");
const signature = request.header("Kadryn-Webhook-Signature");
if (!timestamp || !signature) {
response.status(400).send("Missing signature headers");
return;
}
const valid = verifyKadrynWebhook({
rawBody,
timestamp,
signature,
secret: process.env.KADRYN_WEBHOOK_SECRET ?? "",
});
if (!valid) {
response.status(400).send("Invalid signature");
return;
}
const event = JSON.parse(rawBody);
response.status(200).json({ received: true });
},
);Adjust header names to match your workspace configuration if your implementation uses a prefixed header format.
Timestamp tolerance
Reject old timestamps to reduce replay risk.
Recommended default:
5 minutesDo not set an unlimited tolerance in production.
Raw body warning
Many frameworks parse JSON before your handler runs.
If your framework does that, signature verification can fail because the body changed.
Configure the route to expose the raw request body.
Common mistakes
- verifying the parsed JSON instead of raw body;
- comparing signatures with normal string equality;
- accepting timestamps with no tolerance;
- processing the event before verification;
- logging webhook secrets;
- treating test events as proof of production delivery.
After verification
After verification succeeds:
- parse the JSON;
- dedupe by event ID or delivery ID;
- enqueue heavy work;
- respond with 2xx quickly;
- log safe delivery metadata.