Skip to main content

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.