Skip to main content

Verifying Requests

Every webhook request includes an aktify-signature header. Use this to verify that the request originated from Aktify and hasn't been tampered with.

Signature Format

The signature format depends on the webhook version:

VersionFormatDescription
v1 (Legacy)t=<unix_timestamp_ms>,v1=<signature>HMAC computed over the request body only
v2t=<unix_timestamp_ms>,v2=<signature>HMAC computed over the timestamp and request body
ComponentDescription
tUnix timestamp (milliseconds) when the request was sent
v1HMAC SHA-256 hex digest of JSON.stringify(body)
v2HMAC SHA-256 hex digest of ${timestamp}.${JSON.stringify(body)}

The version component (v1 or v2) tells you which verification method to use. Both versions use your client secret as the HMAC key.

v1 Verification (Legacy Events)

v1 signatures sign only the request body. The timestamp is included in the header but is not part of the signed payload, so you should independently check for stale timestamps to guard against replay attacks.

const crypto = require('crypto');

function verifyV1Webhook(requestBody, signatureHeader, clientSecret) {
const [tPart, v1Part] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];

const expectedSignature = crypto
.createHmac('sha256', clientSecret)
.update(JSON.stringify(requestBody))
.digest('hex');

if (signature !== expectedSignature) {
throw new Error('Invalid signature - request may not be from Aktify');
}

// Recommended: reject requests older than 5 minutes to prevent replay attacks
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
if (parseInt(timestamp) < fiveMinutesAgo) {
throw new Error('Request timestamp too old');
}

return true;
}

v2 Verification

v2 signatures include the timestamp in the HMAC input, cryptographically binding the timestamp to the payload. This means any modification to the timestamp invalidates the signature, providing built-in replay protection.

const crypto = require('crypto');

function verifyV2Webhook(requestBody, signatureHeader, clientSecret) {
const [tPart, v2Part] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const signature = v2Part.split('=').slice(1).join('=');

const expectedSignature = crypto
.createHmac('sha256', clientSecret)
.update(`${timestamp}.${JSON.stringify(requestBody)}`)
.digest('hex');

if (signature !== expectedSignature) {
throw new Error('Invalid signature - request may not be from Aktify');
}

// Reject requests older than 5 minutes
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
if (parseInt(timestamp) < fiveMinutesAgo) {
throw new Error('Request timestamp too old');
}

return true;
}