Verifying Signatures
Validate webhook authenticity with HMAC-SHA256
Every webhook delivery is signed so you can verify it came from Flipswitch and hasn't been tampered with.
How Signing Works
- Secret format. Each webhook has a signing secret in the format
whsec_.... - String to sign. Flipswitch builds the signed payload as
<timestamp>:<raw_json_body>, where the timestamp is the Unix epoch in seconds from theX-Flipswitch-Timestampheader. - HMAC-SHA256. The signed payload is hashed using the full secret string bytes (UTF-8, including the
whsec_prefix) and hex-encoded. - Header format. The signature and timestamp are sent as two separate headers:
X-Flipswitch-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9
X-Flipswitch-Timestamp: 1705312242During secret rotation, the X-Flipswitch-Signature header contains two sha256= entries:
X-Flipswitch-Signature: sha256=<hmac_new_secret>,sha256=<hmac_old_secret>
X-Flipswitch-Timestamp: 1705312242Verification Steps
- Parse the headers. Extract the timestamp from
X-Flipswitch-Timestampand allsha256=signature values fromX-Flipswitch-Signature. - Build the signed payload. Concatenate
<timestamp>:<raw_request_body>(use the raw bytes, not re-serialized JSON). - Compute the expected HMAC. Use the full secret string as the HMAC key bytes (UTF-8, including
whsec_), then compute HMAC-SHA256 over the signed payload and hex-encode the result. - Compare. Check if any
sha256=value matches your computed signature using a constant-time comparison. - Optional: check timestamp. Reject deliveries where the timestamp is too far from the current time (e.g., more than 5 minutes) to prevent replay attacks.
Code Examples
import crypto from 'node:crypto';
function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
// Use full secret as UTF-8 bytes (including whsec_ prefix)
const key = Buffer.from(secret, 'utf8');
// Parse headers
const timestamp = timestampHeader;
const signatures = signatureHeader
.split(',')
.filter(p => p.startsWith('sha256='))
.map(p => p.slice(7));
// Compute expected signature
const signedPayload = `${timestamp}:${rawBody}`;
const expected = crypto
.createHmac('sha256', key)
.update(signedPayload)
.digest('hex');
// Constant-time compare against any sha256 value
return signatures.some(sig =>
crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))
);
}Always verify against the raw request body bytes. Parsing the JSON and re-serializing it may change whitespace or key ordering, which will invalidate the signature.
Handling Secret Rotation
During secret rotation, Flipswitch signs each delivery with both the new and old secrets. The X-Flipswitch-Signature header contains two sha256= entries:
X-Flipswitch-Signature: sha256=<hmac_new_secret>,sha256=<hmac_old_secret>
X-Flipswitch-Timestamp: 1705312242The code examples above already handle this — they iterate over all sha256= values and accept the delivery if any one matches. This means your endpoint continues to verify successfully during the rotation grace period without any code changes. Once rotation completes, only the new secret is used.