How signing works
Each webhook delivery includes a Framelane-Signature header:
Framelane-Signature: t=1716278400,v1=a4b3c2d1...
The signature is computed as:
HMAC-SHA256(secret, "{timestamp}.{raw_body}")
where:
secret is the signing secret returned when you created the webhook
timestamp is the Unix timestamp from the t= component
raw_body is the raw (unparsed) request body bytes
Verification example
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(
rawBody: string,
signature: string,
secret: string,
toleranceSeconds = 300
): boolean {
const parts = Object.fromEntries(
signature.split(",").map((p) => p.split("="))
);
const timestamp = parseInt(parts.t, 10);
const received = parts.v1;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
throw new Error("Webhook timestamp out of tolerance");
}
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
Always use a constant-time comparison (timingSafeEqual / hmac.compare_digest) to prevent timing attacks.
Rotating the secret
curl -X POST https://api.framelane.io/v1/webhooks/{id}/rotate-secret \
-H "Authorization: Bearer $FRAMELANE_API_KEY"
Response:
{ "secret": "whsec_new..." }
Update your application immediately after rotation. The old secret stops working as soon as the new one is issued.