Processing DTMF Sequences in NICE Cognigy Voice Flows via Node.js Webhook Handler
What You Will Build
- The code intercepts real-time DTMF digit events from a Cognigy voice call, buffers them into a sequence, validates the sequence against a configurable pattern, and pushes the result back to the Cognigy conversation context.
- This implementation uses the Cognigy Voice API webhook callback mechanism and the Cognigy REST API for variable updates.
- The tutorial covers Node.js with Express and Axios.
Prerequisites
- Cognigy Voice API enabled on the target agent and flow configuration
- OAuth2 Client Credentials or API Key with
conversations:writeandvariables:writepermissions - Node.js 18 or later
- External dependencies:
express,axios,dotenv,uuid - CORS configured to allow POST requests from your deployment environment to the Cognigy webhook callback URL
Authentication Setup
Cognigy server-to-server integrations typically use Bearer tokens obtained via OAuth2 Client Credentials or static API keys. The following pattern caches the token and automatically refreshes it before expiration. Cognigy OAuth2 endpoints follow standard RFC 6749 flows.
// auth.js
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
const COGNIGY_BASE_URL = process.env.COGNIGY_BASE_URL;
const CLIENT_ID = process.env.COGNIGY_CLIENT_ID;
const CLIENT_SECRET = process.env.COGNIGY_CLIENT_SECRET;
const TOKEN_URL = `${COGNIGY_BASE_URL}/oauth/token`;
let cachedToken = null;
let tokenExpiry = 0;
/**
* Fetches a new Bearer token from Cognigy OAuth2 endpoint.
* Requires scopes: conversations:read, conversations:write, variables:write
*/
export async function getAuthToken() {
const now = Date.now();
if (cachedToken && now < tokenExpiry - 60000) {
return cachedToken;
}
try {
const response = await axios.post(TOKEN_URL, null, {
params: {
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'conversations:read conversations:write variables:write'
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
cachedToken = response.data.access_token;
tokenExpiry = now + (response.data.expires_in * 1000);
return cachedToken;
} catch (error) {
console.error('Failed to acquire Cognigy OAuth token:', error.response?.data || error.message);
throw new Error('Authentication failed');
}
}
The token cache prevents unnecessary network calls. The scope string explicitly requests write access to conversations and variables. If your Cognigy instance uses static API keys instead of OAuth2, replace the getAuthToken function with a simple environment variable lookup that returns the key directly.
Implementation
Step 1: Webhook Server & Event Routing
Cognigy sends DTMF events to your configured webhook URL as HTTP POST requests with a JSON payload. The webhook expects a 200 OK response within two seconds to prevent retry cascades. The following Express route captures the event, validates the structure, and delegates processing asynchronously.
// server.js
import express from 'express';
import { processDtmfEvent } from './dtmf-processor.js';
import { v4 as uuidv4 } from 'uuid';
const app = express();
app.use(express.json());
const DTMF_WEBHOOK_PATH = '/api/webhooks/cognigy/dtmf';
app.post(DTMF_WEBHOOK_PATH, async (req, res) => {
const requestId = uuidv4();
console.log(`[${requestId}] Received DTMF webhook`);
const { type, digit, conversationId, agentId, timestamp } = req.body;
if (type !== 'dtmf' || !digit || !conversationId || !agentId) {
console.warn(`[${requestId}] Invalid DTMF payload structure`);
return res.status(400).json({ error: 'Invalid DTMF event payload' });
}
// Acknowledge immediately to prevent Cognigy webhook timeout
res.status(200).json({ status: 'accepted', requestId });
// Process asynchronously to avoid blocking the HTTP thread
try {
await processDtmfEvent({ digit, conversationId, agentId, timestamp, requestId });
} catch (error) {
console.error(`[${requestId}] DTMF processing failed:`, error.message);
}
});
export default app;
The route validates that the type field equals dtmf and that required identifiers are present. It returns a 200 response immediately. The actual digit buffering and API calls execute in the background. This pattern prevents Cognigy from marking the webhook as failed due to slow downstream processing.
Step 2: DTMF Buffering & Pattern Validation
Voice callers press digits sequentially. Each press triggers a separate webhook event. You must buffer digits per conversation, apply a timeout to detect completion, and validate the accumulated sequence against a business pattern. The following module manages state and validation.
// dtmf-processor.js
import { updateConversationVariable } from './cognigy-api.js';
import { getAuthToken } from './auth.js';
// In-memory store for active DTMF sequences
const activeSequences = new Map();
const DTmf_TIMEOUT_MS = 3000; // 3 seconds of inactivity triggers flush
const VALID_PATTERNS = {
PIN4: /^\d{4}$/,
MENU_OPTION: /^[0-9]$/,
EXTENSION: /^\d{3,6}$/
};
/**
* Processes a single DTMF digit event.
* Buffers digits, handles timeouts, validates patterns, and updates Cognigy variables.
*/
export async function processDtmfEvent({ digit, conversationId, agentId, timestamp, requestId }) {
const key = `${agentId}:${conversationId}`;
const now = Date.now();
if (!activeSequences.has(key)) {
activeSequences.set(key, {
sequence: '',
lastActivity: now,
agentId,
conversationId
});
// Schedule timeout flush
setTimeout(() => flushSequence(key), DTmf_TIMEOUT_MS);
}
const state = activeSequences.get(key);
state.sequence += digit;
state.lastActivity = now;
console.log(`[${requestId}] Buffered digit '${digit}'. Sequence: '${state.sequence}'`);
// Check against patterns immediately
for (const [patternName, regex] of Object.entries(VALID_PATTERNS)) {
if (regex.test(state.sequence)) {
console.log(`[${requestId}] Matched pattern: ${patternName}`);
await pushResultToCognigy(state, patternName, requestId);
activeSequences.delete(key);
return;
}
}
// If sequence exceeds max expected length, flush as invalid
if (state.sequence.length > 10) {
console.warn(`[${requestId}] Sequence exceeded max length. Flushing.`);
await pushResultToCognigy(state, 'INVALID_LENGTH', requestId);
activeSequences.delete(key);
}
}
async function pushResultToCognigy(state, patternMatch, requestId) {
try {
const token = await getAuthToken();
await updateConversationVariable(
token,
state.agentId,
state.conversationId,
'dtmfPatternMatch',
patternMatch
);
console.log(`[${requestId}] Successfully updated Cognigy variable dtmfPatternMatch to '${patternMatch}'`);
} catch (error) {
console.error(`[${requestId}] Failed to update Cognigy variable:`, error.message);
throw error;
}
}
function flushSequence(key) {
const state = activeSequences.get(key);
if (state && state.sequence.length > 0) {
console.log(`Flushing incomplete sequence for ${key}: '${state.sequence}'`);
pushResultToCognigy(state, 'TIMEOUT_INCOMPLETE', 'flush');
activeSequences.delete(key);
}
}
The buffer uses a Map keyed by agentId:conversationId. Each incoming digit appends to the sequence string. The code checks every registered regex pattern after each digit. A match triggers an immediate variable update and clears the buffer. A three-second inactivity timeout flushes incomplete sequences. This prevents memory leaks and ensures downstream flows receive a definitive state.
Step 3: Updating Cognigy Conversation Variables
Cognigy exposes a REST endpoint to inject variables into an active conversation. The following function handles the HTTP request, implements retry logic for rate limiting, and structures the payload according to Cognigy API v2 specifications.
// cognigy-api.js
import axios from 'axios';
const COGNIGY_BASE_URL = process.env.COGNIGY_BASE_URL;
/**
* Updates a conversation variable in Cognigy.
* Endpoint: POST /api/v2/agents/{agentId}/conversations/{conversationId}/variables
* Required Scope: variables:write
*/
export async function updateConversationVariable(
authToken,
agentId,
conversationId,
variableName,
variableValue,
maxRetries = 3
) {
const url = `${COGNIGY_BASE_URL}/api/v2/agents/${agentId}/conversations/${conversationId}/variables`;
const payload = {
variables: [
{
name: variableName,
value: String(variableValue)
}
]
};
let retryCount = 0;
while (retryCount <= maxRetries) {
try {
const response = await axios.post(url, payload, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 5000
});
console.log(`Cognigy variable update successful: ${response.status}`);
return response.data;
} catch (error) {
const status = error.response?.status;
const message = error.message;
if (status === 429) {
const retryAfter = error.response?.headers['retry-after'];
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, retryCount) * 1000;
console.warn(`Rate limited (429). Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
retryCount++;
continue;
}
if (status === 401 || status === 403) {
throw new Error(`Authentication/Authorization failed: ${status} - ${message}`);
}
if (status === 400) {
throw new Error(`Invalid payload or resource not found: ${JSON.stringify(error.response?.data)}`);
}
if (status && status >= 500) {
console.error(`Server error ${status}. Retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000));
retryCount++;
continue;
}
throw new Error(`Unexpected error calling Cognigy API: ${message}`);
}
}
throw new Error(`Max retries (${maxRetries}) exceeded for variable update`);
}
The function accepts the token, identifiers, and variable data. It constructs the exact JSON structure Cognigy expects. The retry loop handles 429 Too Many Requests by reading the Retry-After header or falling back to exponential backoff. 401 and 403 errors fail immediately because token rotation or permission fixes are required. 5xx errors trigger transient retries. The timeout parameter prevents hanging connections.
Complete Working Example
The following script combines authentication, webhook routing, DTMF buffering, and Cognigy API integration into a single runnable module. Save it as index.js, install dependencies, and start the server.
// index.js
import app from './server.js';
import dotenv from 'dotenv';
dotenv.config();
const PORT = process.env.PORT || 3000;
const COGNIGY_BASE_URL = process.env.COGNIGY_BASE_URL;
const CLIENT_ID = process.env.COGNIGY_CLIENT_ID;
const CLIENT_SECRET = process.env.COGNIGY_CLIENT_SECRET;
if (!COGNIGY_BASE_URL || !CLIENT_ID || !CLIENT_SECRET) {
console.error('Missing required environment variables: COGNIGY_BASE_URL, COGNIGY_CLIENT_ID, COGNIGY_CLIENT_SECRET');
process.exit(1);
}
app.listen(PORT, () => {
console.log(`DTMF Webhook Handler listening on port ${PORT}`);
console.log(`Webhook endpoint: POST /api/webhooks/cognigy/dtmf`);
console.log(`Cognigy Base URL: ${COGNIGY_BASE_URL}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
process.exit(0);
});
Install dependencies and run:
npm init -y
npm install express axios dotenv uuid
node index.js
Configure your Cognigy Voice API webhook callback URL to https://your-domain.com/api/webhooks/cognigy/dtmf. The server will accept digit events, buffer them, validate against patterns, and update the dtmfPatternMatch variable in the active conversation. Your Cognigy flow can read this variable to route to specific intents or IVR branches.
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth2 token is expired, malformed, or the client credentials are incorrect. Cognigy also returns 401 if the API key lacks the required permissions.
- Fix: Verify
CLIENT_IDandCLIENT_SECRETin your environment. Ensure the token cache is not stale. Check Cognigy admin console to confirm the client hasvariables:writepermission. - Code showing the fix: The
getAuthTokenfunction automatically refreshes tokens. If it fails, it throws immediately. Log the raw response to inspect token payload issues.
Error: 400 Bad Request
- Cause: The variable payload structure does not match Cognigy API v2 schema, or the
conversationId/agentIdis invalid. - Fix: Validate that the
variablesarray contains objects with exactlynameandvalueproperties. EnsureconversationIdmatches the active call context. - Code showing the fix: The
updateConversationVariablefunction enforces the exact payload shape. Add aconsole.logbefore the POST to inspect the serialized JSON if schema validation fails.
Error: 429 Too Many Requests
- Cause: Cognigy enforces rate limits on variable updates and webhook callbacks. Rapid DTMF pressing or high call volume triggers throttling.
- Fix: Implement exponential backoff. Respect the
Retry-Afterheader. Batch variable updates if possible. - Code showing the fix: The retry loop in
updateConversationVariablehandles 429 responses automatically. It readsRetry-Afteror appliesMath.pow(2, retryCount) * 1000delay before retrying.
Error: Webhook Timeout / Missing Acknowledgment
- Cause: Cognigy expects a
200 OKresponse within two seconds. Blocking synchronous processing or heavy regex evaluation delays the response. - Fix: Return
200immediately upon payload validation. Defer all database calls, API updates, and pattern matching to background tasks. - Code showing the fix: The Express route calls
res.status(200).json()before awaitingprocessDtmfEvent. This guarantees Cognigy marks the callback as successful.