Verifying webhook signatures
Verify that a webhook came from XenonPay by checking the X-Xenonpay-Signature header against a hash you compute with your webhook signing secret. Reject any delivery whose signature does not match.
The signature header
When you have a signing secret, each delivery includes:
X-Xenonpay-Signature: t=1756032000,v1=hexdigest...The header has two parts: t is the Unix timestamp when we signed the request, and v1 is the signature. The signature is computed as:
HMAC-SHA256( key = your webhook secret, message = "{t}.{raw request body}" )encoded as lowercase hexadecimal.
How to verify
Read the raw request body as received. Do not parse it to JSON and re-serialize it first — that changes the bytes and breaks the signature.
Parse the
X-Xenonpay-Signatureheader intotandv1.Compute
HMAC-SHA256over the stringt+"."+ the raw body, using your webhook secret as the key.Encode the result as lowercase hex and compare it to
v1using a timing-safe comparison.Optionally reject deliveries whose
tis far in the past to guard against replays.
Node.js example
const crypto = require("crypto");
function verifyXenonPaySignature(secret, header, rawBody) {
// header looks like: "t=1756032000,v1=abc123..."
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("="))
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Your signing secret
Your webhook signing secret looks like whsec_ followed by random hex. Generate or rotate it from your webhook settings, and store it securely on your server.
If you set a webhook URL but do not configure a signing secret, we still deliver events — but unsigned. Always configure a secret and verify signatures in production.