API
Signature Verification
Verify HMAC-SHA256 signatures over raw request bodies to ensure authenticity and prevent replay attacks.
Signature Format & Headers
Every webhook dispatch includes standard security headers:
X-Asiyst-Timestamp: 1791800000
X-Asiyst-Signature: t=1791800000,v1=5b263ab29c488e5d6d8713...
X-Asiyst-Event-Id: evt_01h8x9a2b
X-Asiyst-Event-Type: payment.succeeded
X-Asiyst-Webhook-Id: wh_dest_123
X-Asiyst-Delivery-Id: deliv_456
Verification Algorithm
- Extract the timestamp from
X-Asiyst-Timestampor thetcomponent ofX-Asiyst-Signature.
- Compute the time drift:
Math.abs(Date.now() / 1000 - timestamp). Reject if drift exceeds 300 seconds (5 minutes) to protect against replay attacks.
- Compute the expected HMAC:
expected = HMAC-SHA256(secret, "${timestamp}.${rawBody}")
- Compare
expectedwithv1using constant-time comparison (crypto.timingSafeEqual) to avoid timing attacks.
Critical: Use Exact Raw Request Body
Warning: You MUST verify the exact raw bytes received from the HTTP socket. Do NOT parse the JSON and re-stringify it before calculating the signature. Key ordering or whitespace differences will cause verification to fail.
TypeScript Verification Helper
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyAsiystWebhookSignature(params: {
secret: string;
signatureHeader: string;
rawBody: string;
toleranceSeconds?: number;
}): boolean {
const { secret, signatureHeader, rawBody, toleranceSeconds = 300 } = params;
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => {
const [key, ...rest] = part.trim().split("=");
return [key, rest.join("=")];
})
);
const timestamp = Number(parts.t);
const v1Signature = parts.v1;
if (!Number.isSafeInteger(timestamp) || !v1Signature) {
return false;
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSeconds) {
return false; // Expired timestamp / replay attack
}
const expected = createHmac("sha256", secret)
.update(${timestamp}.${rawBody}, "utf8")
.digest("hex");
const actualBuf = Buffer.from(v1Signature, "hex");
const expectedBuf = Buffer.from(expected, "hex");
if (actualBuf.length !== expectedBuf.length) {
return false;
}
return timingSafeEqual(actualBuf, expectedBuf);
}