Implementing Webhook Signature Verification for Inbound NICE Cognigy Callbacks in a CXone Lambda Function
What You Will Build
- The code validates cryptographic signatures on incoming Cognigy HTTP callbacks to reject spoofed requests before processing conversation data.
- This implementation uses the CXone Functions runtime and the native Node.js
cryptomodule. - The tutorial covers Node.js (ES modules) for serverless execution.
Prerequisites
- Shared secret configured in Cognigy and CXone Environment Variables
- CXone Functions runtime (Node.js 18.x or higher)
- Node.js
cryptomodule (built-in, no npm install required) - If the function calls CXone APIs downstream, the CXone OAuth client requires
interaction:writeoranalytics:queryscopes
Authentication Setup
Inbound webhooks do not use OAuth 2.0 token flows. Authentication relies on HMAC-SHA256 signature verification using a pre-shared secret. The Cognigy platform signs the raw request body with this secret and transmits the hash in the X-Cognigy-Signature header. The CXone function retrieves the secret from environment variables and recomputes the hash to verify the request origin.
Configure the shared secret in the CXone Admin Console under Develop > Functions > Environment Variables. Create a variable named COGNIGY_WEBHOOK_SECRET. The Cognigy platform must use the exact same value in its webhook configuration.
The following code demonstrates how to securely retrieve and validate the secret at runtime. This pattern prevents hardcoded credentials and supports secret rotation without redeploying the function.
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.COGNIGY_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET || WEBHOOK_SECRET.length === 0) {
throw new Error('COGNIGY_WEBHOOK_SECRET environment variable is not configured');
}
function getSecureSecret() {
return WEBHOOK_SECRET;
}
This setup ensures the secret is loaded once at module initialization. The function throws a fatal error during cold start if the secret is missing. This fails fast rather than silently accepting unverified requests.
Implementation
Step 1: Lambda Handler Setup and Raw Body Preservation
CXone Functions receive HTTP events as JavaScript objects. The platform may parse JSON bodies automatically depending on the endpoint configuration. Webhook signature verification requires the exact raw bytes that Cognigy transmitted. You must preserve the original string payload before any parsing or transformation occurs.
The following handler extracts the raw body, preserves it as a UTF-8 string, and isolates the signature header. This step prevents hash mismatches caused by whitespace normalization or character encoding changes.
export default async function handler(event, context) {
const rawBody = event.body || '';
const signatureHeader = event.headers['x-cognigy-signature'] || event.headers['X-Cognigy-Signature'];
if (!signatureHeader) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Missing X-Cognigy-Signature header' })
};
}
return { rawBody, signatureHeader };
}
The handler checks for the header using case-insensitive fallback logic. HTTP headers are technically case-insensitive, but some proxies normalize them. The response returns a 400 status when the header is absent. This prevents downstream processing of unauthenticated payloads.
Step 2: HMAC Signature Verification Logic
Signature verification must use constant-time comparison to prevent timing attacks. Attackers can measure response time differences to guess the correct hash byte by byte. The Node.js crypto.timingSafeEqual function mitigates this risk by comparing buffers in constant time regardless of mismatch position.
The following function computes the expected HMAC-SHA256 digest and compares it against the transmitted signature. Both values must be converted to binary buffers before comparison.
function verifyWebhookSignature(rawBody, receivedSignature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(rawBody);
const expectedSignature = hmac.digest('hex');
const receivedBuffer = Buffer.from(receivedSignature, 'hex');
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
if (receivedBuffer.length !== expectedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}
The function computes the HMAC using the raw body and secret. It converts both the received and expected signatures to hexadecimal buffers. Length validation occurs before comparison to avoid timingSafeEqual throwing a TypeError on mismatched buffer sizes. The function returns a boolean indicating verification success.
Step 3: Processing Valid Payload and Returning Responses
After verification, the function parses the JSON payload and executes business logic. You must return a properly structured HTTP response object to the CXone runtime. The response must include a status code, content type header, and serialized JSON body.
The following code integrates verification with payload processing. It handles 401 Unauthorized for failed signatures, 400 Bad Request for malformed JSON, and 200 OK for successful processing.
const response = {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'processed', timestamp: new Date().toISOString() })
};
const isValid = verifyWebhookSignature(rawBody, signatureHeader, getSecureSecret());
if (!isValid) {
response.statusCode = 401;
response.body = JSON.stringify({ error: 'Invalid webhook signature' });
return response;
}
try {
const payload = JSON.parse(rawBody);
// Process Cognigy conversation data
if (!payload.sessionId || !payload.userId) {
response.statusCode = 400;
response.body = JSON.stringify({ error: 'Missing required Cognigy fields' });
return response;
}
} catch (parseError) {
response.statusCode = 400;
response.body = JSON.stringify({ error: 'Invalid JSON payload' });
return response;
}
return response;
The handler validates the signature first. If verification fails, it returns 401 immediately. If verification succeeds, it parses the JSON and validates required Cognigy fields. Parse errors return 400. Successful processing returns 200 with a confirmation payload. This structure ensures predictable error handling and clear contract boundaries.
Complete Working Example
The following module combines all components into a production-ready CXone function. It includes environment validation, constant-time signature verification, JSON parsing, structured error responses, and logging for debugging. Deploy this code to a CXone Function with the COGNIGY_WEBHOOK_SECRET environment variable configured.
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.COGNIGY_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET || WEBHOOK_SECRET.length === 0) {
throw new Error('COGNIGY_WEBHOOK_SECRET environment variable is not configured');
}
function verifyWebhookSignature(rawBody, receivedSignature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(rawBody);
const expectedSignature = hmac.digest('hex');
const receivedBuffer = Buffer.from(receivedSignature, 'hex');
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
if (receivedBuffer.length !== expectedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}
export default async function handler(event, context) {
const rawBody = event.body || '';
const signatureHeader = event.headers['x-cognigy-signature'] || event.headers['X-Cognigy-Signature'];
if (!signatureHeader) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Missing X-Cognigy-Signature header' })
};
}
const isValid = verifyWebhookSignature(rawBody, signatureHeader, WEBHOOK_SECRET);
if (!isValid) {
return {
statusCode: 401,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Invalid webhook signature' })
};
}
let payload;
try {
payload = JSON.parse(rawBody);
} catch (error) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Invalid JSON payload' })
};
}
if (!payload.sessionId || !payload.userId) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Missing required Cognigy fields: sessionId, userId' })
};
}
console.log('Received valid Cognigy callback:', {
sessionId: payload.sessionId,
userId: payload.userId,
timestamp: new Date().toISOString()
});
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: 'processed',
sessionId: payload.sessionId,
timestamp: new Date().toISOString()
})
};
}
This module handles all authentication and validation steps. The console.log statement writes to the CXone function execution logs. The response structure complies with the CXone Functions HTTP contract. Cognigy will receive the 200 response and continue the conversation flow.
Common Errors & Debugging
Error: 401 Unauthorized (Invalid Webhook Signature)
This error occurs when the computed HMAC does not match the transmitted signature. Common causes include secret mismatch, body encoding differences, or header case sensitivity. Verify that the Cognigy platform and CXone environment use the exact same secret string. Ensure Cognigy transmits the raw body without URL encoding. Check the CXone function logs to confirm the secret loads correctly.
Fix the issue by synchronizing the secret and verifying the payload format. Use the following test script to validate the signature locally before deployment.
import crypto from 'crypto';
const testBody = '{"sessionId":"123","userId":"456"}';
const testSecret = 'your-secret-here';
const hmac = crypto.createHmac('sha256', testSecret);
hmac.update(testBody);
console.log('Expected signature:', hmac.digest('hex'));
Compare the console output with the header value Cognigy generates. They must match exactly.
Error: TypeError (Raw body not preserved)
CXone may parse the request body into an object before the function executes. If event.body is an object instead of a string, the HMAC computation will use JSON stringification with different spacing or key ordering. This causes signature mismatches.
Configure the CXone Function endpoint to disable automatic JSON parsing. Set the bodyParser option to false in the function configuration. Alternatively, reconstruct the raw string using JSON.stringify(payload) only if you control both ends and accept the security trade-off. The recommended approach is disabling automatic parsing.
Error: Timing Attack Vulnerability
Using the === operator to compare signatures exposes the function to timing attacks. Attackers measure response latency to infer correct hash characters. The crypto.timingSafeEqual function eliminates this risk by comparing buffers in constant time.
Always use buffer comparison for cryptographic validation. Never use string equality operators for signature verification. The complete example demonstrates the correct pattern.