This page explains how to verify the authenticity of webhook events sent by the Xenia External API. All webhook events include cryptographic signatures that allow you to verify that the events were sent by Xenia and haven't been tampered with.
When Xenia sends a webhook event to your endpoint, it includes two important headers:
X-Signature: The RSA-SHA256 signature of the webhook payloadX-Timestamp: The timestamp when the webhook was sent (used in signature calculation)
To verify the webhook authenticity:
- Retrieve the public key from the
/external-api/v1/webhook-verification-keyendpoint - Concatenate the raw request body with the timestamp
- Verify the signature using RSA-SHA256
The public key used for verification can be retreived from the following endpoint:
curl -X GET https://your-xenia-api.com/external-api/v1/webhook-verification-key \
-H "X-Api-Key: YOUR_API_KEY"Example Response:
{
"data": {
"publicKey": "MIIBIjANBgk...",
"algorithm": "RSA-SHA256 + PKCS#1 padding",
"keyFormat": "base64"
}
}import crypto from 'crypto';
import axios from 'axios';
// Cache for the public key
let keyCache = { key: null, expiresAt: null };
/**
* Verify webhook signature with automatic key fetching and caching
* @param {string} requestBody - Raw webhook request body
* @param {string} xSignature - X-Signature header value
* @param {string} xTimestamp - X-Timestamp header value
* @param {string} apiUrl - Your Xenia API base URL
* @param {string} apiKey - API key for X-Api-Key header
* @returns {Promise<boolean>} True if signature is valid
*/
async function verifyWebhookSignature(requestBody, xSignature, xTimestamp, apiUrl, apiKey) {
try {
// Check cache and fetch key if needed
let publicKey = keyCache.key;
if (!publicKey || !keyCache.expiresAt || new Date() >= keyCache.expiresAt) {
const response = await axios.get(`${apiUrl}/external-api/v1/webhook-verification-key`, {
headers: { 'X-Api-Key': apiKey }
});
publicKey = response.data.data.publicKey;
keyCache = {
key: publicKey,
expiresAt: new Date(Date.now() + 60 * 60 * 1000) // 1 hour expiry
};
}
// Convert base64 public key to PEM format
const pemKey = `-----BEGIN PUBLIC KEY-----\n${publicKey.match(/.{1,64}/g).join('\n')}\n-----END PUBLIC KEY-----`;
// Verify signature
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(requestBody + (xTimestamp || ''));
return verifier.verify(pemKey, Buffer.from(xSignature, 'base64'));
} catch (error) {
console.error('Signature verification failed:', error.message);
return false;
}
}
// Usage in Express webhook endpoint
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const isValid = await verifyWebhookSignature(
req.body.toString('utf-8'),
req.headers['x-signature'],
req.headers['x-timestamp'],
process.env.XENIA_API_URL,
process.env.XENIA_API_KEY
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process verified webhook
const webhookData = JSON.parse(req.body);
// ... handle webhook ...
res.status(204).send();
});Always verify signatures: Never process webhook events without verifying their signatures first.
Refresh keys periodically: Fetch the public key from the verification endpoint periodically (e.g., daily) to ensure you have the latest key in case of rotation.
Handle errors gracefully: If signature verification fails, log the event for investigation but don't process the webhook data.
Validate timestamps: Consider checking that the timestamp is recent (e.g., within the last 5 minutes) to prevent replay attacks.
Signature verification always fails
- Ensure you're using the raw request body, not a parsed/modified version
- Verify the public key format is correct (PEM format with headers)
- Check that you're concatenating the payload with timestamp correctly
Missing headers
- Both
X-SignatureandX-Timestampheaders must be present - Header names are case-insensitive in HTTP but may be case-sensitive in your code
- Both
If you continue to experience issues with webhook signature verification, please contact Xenia support with:
- The webhook event ID
- The timestamp of the webhook
- Any error messages from your verification code
- A sample of your verification implementation (with sensitive data removed)