Signature Verification
Every webhook is signed with HMAC-SHA256. Verify the signature to ensure the event truly comes from Wajub.
Anyone can send a POST request to your endpoint. The HMAC signature proves that
the event comes from Wajub and has not been tampered with. Verify it before any processing.
How the signature is computed
Every webhook request carries three headers:
X-Wajub-SignatureheaderoptionalX-Wajub-TimestampheaderoptionalX-Wajub-Delivery-IdheaderoptionalThe HMAC is not computed over the raw body alone. It's computed over the timestamp and the JSON body concatenated with a period:
signed_payload = "{timestamp}.{raw_json_body}"
signature = hex(HMAC-SHA256(signed_payload, webhook_secret))
header value = "v1=" + signatureRetrieve your secret from Konsole → Webhooks → Endpoints. To verify, you must read both the
X-Wajub-Signature and X-Wajub-Timestamp headers, reconstruct signed_payload yourself, and
compare — the timestamp alone or the body alone is not enough.
Use the RAW body
{raw_json_body} must be the exact bytes received, byte-for-byte — the same string Wajub
serialized. If your framework parses the JSON and you re-serialize it before hashing, the
signature will not match even if the timestamp and secret are correct. Capture the raw body on
the webhook route, before any JSON-parsing middleware runs.
With the SDK (recommended)
const event = wajub.webhooks.constructEvent(
rawBody,
req.headers['x-wajub-signature'],
req.headers['x-wajub-timestamp'],
process.env.WAJUB_WEBHOOK_SECRET,
); // throws if the signature is invalid or the timestamp is too oldManual verification
If you are not using an SDK, reconstruct {timestamp}.{raw_json_body}, HMAC it, strip the v1=
prefix from the header before comparing, and use a constant-time comparison.
import crypto from 'crypto';
function verify(rawBody, signatureHeader, timestampHeader, secret) {
const signedPayload = `${timestampHeader}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
const received = signatureHeader.replace(/^v1=/, '');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}Always compare in constant time
Use timingSafeEqual / hash_equals / compare_digest —
never == — to prevent timing attacks.
Reject stale timestamps
Also check that X-Wajub-Timestamp is recent (e.g. within 5 minutes) before
accepting the event, to reduce the window for a replayed request even if a signature were ever
to leak.