Browse docs
The signature scheme#
Each delivery carries an X-SkedCast-Signature header shaped t=<unix-seconds>,v1=<hex>[,v1=<hex>…]. The signed string is "<t>.<rawBody>": the timestamp, a literal period and the exact raw request body. The algorithm is HMAC-SHA256, keyed with your endpoint's signing secret (shown once when you create the webhook).
During a 24-hour secret rotation the header holds one v1 entry per valid secret. Accept the delivery if any entry matches.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySkedcastSignature(
secret: string,
rawBody: string | Buffer, // exact bytes of the request body
header: string, // the X-SkedCast-Signature header value
toleranceSeconds = 300,
): boolean {
// Header shape: t=<unix-seconds>,v1=<hex>[,v1=<hex>...]
let timestamp: string | undefined;
const candidates: string[] = [];
for (const part of header.split(",")) {
const separator = part.indexOf("=");
if (separator === -1) continue;
const key = part.slice(0, separator).trim();
const value = part.slice(separator + 1).trim();
if (key === "t") timestamp = value;
else if (key === "v1") candidates.push(value);
}
if (timestamp === undefined || !/^\d+$/.test(timestamp) || candidates.length === 0) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > toleranceSeconds) return false;
const expected = Buffer.from(
createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex"),
"utf8",
);
// During a secret rotation there is one v1 entry per valid secret: accept a match on ANY of them.
// Every entry is compared, in constant time, so timing does not reveal which one matched.
let matched = false;
for (const candidate of candidates) {
const provided = Buffer.from(candidate, "utf8");
if (provided.length === expected.length && timingSafeEqual(provided, expected)) matched = true;
}
return matched;
}Standard Webhooks headers#
Every delivery also carries the Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature) next to X-SkedCast-Signature, so you can use any Standard Webhooks library instead of hand-rolling the HMAC. Both are always sent. Verify one.
FAQ
- Can I test my verification code without waiting for a real event?
- Yes. POST /v1/webhooks/{id}/test sends a real, signed test delivery to your endpoint on demand. This site's webhook signature tester also lets you paste a secret, timestamp and body to compare your output against the reference calculation.
- What's the replay-protection window?
- SkedCast recommends rejecting a delivery whose timestamp is more than about 300 seconds (5 minutes) from your server's clock. Tune it if your clocks drift, but don't skip the check.
- Why does the header have more than one v1 entry?
- During a 24-hour secret rotation, SkedCast signs with every valid secret, one v1 entry each. Your receiver holds one of those secrets, so one entry matches. Accept the delivery if any entry matches.