Building a Cognigy.AI Webhook for External REST Integration with Node.js
What You Will Build
- This tutorial builds a Node.js webhook server that receives intent data from Cognigy.AI, calls a third-party REST API, and returns transformed variables to continue the conversation.
- The implementation uses the Cognigy.AI webhook callback architecture and the Axios HTTP client library.
- The code is written in modern JavaScript (Node.js 18+) using Express and async/await patterns.
Prerequisites
- Cognigy.AI webhook configuration with a generated webhook secret
- Node.js 18 or later runtime environment
- npm packages:
express,axios,uuid,dotenv - External REST API credentials: client ID, client secret, token endpoint URL, and required OAuth 2.0 scopes (
api:read,api:write) - Understanding of JSON payload structures and HTTP status codes
Authentication Setup
Cognigy.AI validates incoming webhook requests using a shared secret. The external REST API requires a Bearer token obtained via OAuth 2.0 client credentials flow. The following module manages token caching and automatic rotation without blocking the webhook execution path.
// auth/tokenManager.js
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
class TokenManager {
constructor() {
this.token = null;
this.expiresAt = 0;
this.refreshing = false;
this.pendingPromises = [];
}
async getValidToken() {
const now = Date.now();
if (this.token && now < this.expiresAt - 60000) {
return this.token;
}
if (this.refreshing) {
return new Promise((resolve, reject) => {
this.pendingPromises.push({ resolve, reject });
});
}
try {
this.refreshing = true;
const response = await axios.post(
process.env.EXTERNAL_API_TOKEN_URL,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.EXTERNAL_API_CLIENT_ID,
client_secret: process.env.EXTERNAL_API_CLIENT_SECRET,
scope: 'api:read api:write'
}),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 5000
}
);
const accessToken = response.data.access_token;
const expiresIn = response.data.expires_in || 3600;
this.token = accessToken;
this.expiresAt = now + (expiresIn * 1000);
this.refreshing = false;
this.pendingPromises.forEach(p => p.resolve(accessToken));
this.pendingPromises = [];
return accessToken;
} catch (error) {
this.refreshing = false;
this.pendingPromises.forEach(p => p.reject(error));
this.pendingPromises = [];
if (error.response) {
throw new Error(`Token refresh failed: ${error.response.status} ${error.response.statusText}`);
}
throw error;
}
}
}
export const tokenManager = new TokenManager();
This implementation prevents race conditions by queuing concurrent requests that arrive while a token refresh is in progress. The sixty-second buffer ensures Cognigy.AI webhook requests never trigger a refresh during active execution.
Implementation
Step 1: Initialize Express Server and Parse Cognigy Payload
Cognigy.AI sends a POST request to your webhook endpoint containing conversation context, detected intents, extracted entities, and existing variables. The server must validate the request signature, extract the intent, and route to the appropriate external API call.
// server.js
import express from 'express';
import crypto from 'crypto';
import { tokenManager } from './auth/tokenManager.js';
import axios from 'axios';
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.COGNIGY_WEBHOOK_SECRET;
function verifySignature(req, res, buf) {
const signature = req.headers['x-cognigy-signature'];
if (!signature || !WEBHOOK_SECRET) return false;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(buf)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.post('/webhook/cognigy', express.json({ verify: verifySignature }), async (req, res) => {
try {
const { intent, entities, variables, conversationId } = req.body;
if (!intent || !intent.name) {
return res.status(400).json({ error: 'Missing intent in Cognigy payload' });
}
console.log(`Processing intent: ${intent.name} for conversation: ${conversationId}`);
// Route based on detected intent
if (intent.name === 'GetCustomerOrder') {
const result = await fetchCustomerOrder(entities, variables);
return res.json({ variables: result });
}
return res.status(404).json({ error: 'Unsupported intent' });
} catch (error) {
console.error('Webhook execution failed:', error.message);
return res.status(500).json({
error: 'Internal processing error',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
app.listen(3000, () => console.log('Cognigy webhook server running on port 3000'));
The verifySignature middleware ensures Cognigy.AI is the sole sender. Cognigy expects a JSON response containing a variables object that merges with the conversation state. Returning a structured error object prevents silent failures in the dialog flow.
Step 2: Implement Token Rotation Middleware and Axios Request Construction
The external API requires authenticated requests. Instead of attaching token logic to every route, a middleware function attaches the resolved token to the request context. Axios configuration enforces strict timeouts and retry policies for transient failures.
// middleware/auth.js
import { tokenManager } from '../auth/tokenManager.js';
export function attachAuthToken(req, res, next) {
tokenManager.getValidToken()
.then(token => {
req.authToken = token;
next();
})
.catch(error => {
console.error('Token middleware failed:', error.message);
next(error);
});
}
The core API call function uses Axios with explicit timeout controls and field mapping preparation.
// services/orderService.js
import axios from 'axios';
import { tokenManager } from '../auth/tokenManager.js';
const EXTERNAL_API_BASE = process.env.EXTERNAL_API_BASE_URL;
const REQUEST_TIMEOUT = 8000;
export async function fetchCustomerOrder(entities, existingVariables) {
const customerId = entities.customerId?.value || existingVariables?.customerId;
if (!customerId) {
throw new Error('Customer ID entity not provided');
}
const token = await tokenManager.getValidToken();
try {
const response = await axios.get(`${EXTERNAL_API_BASE}/api/v2/customers/${customerId}/orders/latest`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'X-Request-ID': crypto.randomUUID()
},
timeout: REQUEST_TIMEOUT,
validateStatus: (status) => status === 200
});
return response.data;
} catch (error) {
if (error.code === 'ECONNABORTED') {
throw new Error('Upstream API timeout exceeded');
}
if (error.response) {
if (error.response.status === 401) {
await tokenManager.getValidToken(); // Force refresh on 401
throw new Error('Authentication token expired');
}
if (error.response.status === 429) {
const retryAfter = error.response.headers['retry-after'] || 2;
throw new Error(`Rate limited. Retry after ${retryAfter} seconds`);
}
}
throw error;
}
}
The validateStatus configuration forces Axios to reject non-200 responses, ensuring the catch block handles HTTP errors uniformly. The X-Request-ID header enables distributed tracing across Cognigy.AI and the external service.
Step 3: Process Results and Map to Cognigy Variables
Cognigy.AI requires a specific variable mapping format. The transformation function normalizes the external API response into Cognigy-compatible key-value pairs and applies fallback values for missing fields.
// utils/transform.js
export function mapToCognigyVariables(apiResponse, fallbackValues = {}) {
if (!apiResponse || typeof apiResponse !== 'object') {
return {
orderFound: false,
errorMessage: 'Invalid API response structure',
...fallbackValues
};
}
const transformed = {
orderFound: true,
orderId: apiResponse.orderId || fallbackValues.orderId,
orderStatus: apiResponse.status || 'Unknown',
orderTotal: apiResponse.total?.amount || 0,
orderCurrency: apiResponse.total?.currency || 'USD',
orderDate: apiResponse.createdAt || new Date().toISOString(),
itemsCount: Array.isArray(apiResponse.items) ? apiResponse.items.length : 0,
errorMessage: ''
};
return transformed;
}
The webhook route integrates this transformer to guarantee Cognigy.AI receives valid variable assignments regardless of upstream payload variations.
// server.js (continued)
import { mapToCognigyVariables } from './utils/transform.js';
import { fetchCustomerOrder } from './services/orderService.js';
// Inside the POST /webhook/cognigy handler:
// const result = await fetchCustomerOrder(entities, variables);
// const cognigyVars = mapToCognigyVariables(result, { orderId: 'N/A', orderStatus: 'Pending' });
// return res.json({ variables: cognigyVars });
Cognigy merges the returned variables object into the conversation context. Empty strings or zero values prevent undefined reference errors in subsequent dialog nodes.
Complete Working Example
// app.js
import express from 'express';
import crypto from 'crypto';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
// --- Authentication Manager ---
class TokenManager {
constructor() {
this.token = null;
this.expiresAt = 0;
this.refreshing = false;
this.pendingPromises = [];
}
async getValidToken() {
const now = Date.now();
if (this.token && now < this.expiresAt - 60000) return this.token;
if (this.refreshing) {
return new Promise((resolve, reject) => this.pendingPromises.push({ resolve, reject }));
}
try {
this.refreshing = true;
const response = await axios.post(
process.env.EXTERNAL_API_TOKEN_URL,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.EXTERNAL_API_CLIENT_ID,
client_secret: process.env.EXTERNAL_API_CLIENT_SECRET,
scope: 'api:read api:write'
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 5000 }
);
this.token = response.data.access_token;
this.expiresAt = now + ((response.data.expires_in || 3600) * 1000);
this.refreshing = false;
this.pendingPromises.forEach(p => p.resolve(this.token));
this.pendingPromises = [];
return this.token;
} catch (error) {
this.refreshing = false;
this.pendingPromises.forEach(p => p.reject(error));
this.pendingPromises = [];
throw error.response ? new Error(`Token refresh failed: ${error.response.status}`) : error;
}
}
}
const tokenManager = new TokenManager();
// --- Transformer ---
function mapToCognigyVariables(apiResponse) {
if (!apiResponse || typeof apiResponse !== 'object') {
return { orderFound: false, errorMessage: 'Invalid API response', orderId: '', orderStatus: '' };
}
return {
orderFound: true,
orderId: apiResponse.orderId || '',
orderStatus: apiResponse.status || 'Unknown',
orderTotal: apiResponse.total?.amount || 0,
orderCurrency: apiResponse.total?.currency || 'USD',
orderDate: apiResponse.createdAt || new Date().toISOString(),
itemsCount: Array.isArray(apiResponse.items) ? apiResponse.items.length : 0,
errorMessage: ''
};
}
// --- Express Server ---
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.COGNIGY_WEBHOOK_SECRET;
const EXTERNAL_API_BASE = process.env.EXTERNAL_API_BASE_URL;
app.post('/webhook/cognigy', express.json({ verify: (req, res, buf) => {
const signature = req.headers['x-cognigy-signature'];
if (!signature || !WEBHOOK_SECRET) return false;
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(buf).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}}, async (req, res) => {
try {
const { intent, entities, variables } = req.body;
if (!intent?.name) return res.status(400).json({ error: 'Missing intent' });
if (intent.name === 'GetCustomerOrder') {
const customerId = entities.customerId?.value || variables?.customerId;
if (!customerId) return res.status(400).json({ error: 'Customer ID required' });
const token = await tokenManager.getValidToken();
const response = await axios.get(`${EXTERNAL_API_BASE}/api/v2/customers/${customerId}/orders/latest`, {
headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' },
timeout: 8000,
validateStatus: (status) => status === 200
});
const cognigyVars = mapToCognigyVariables(response.data);
return res.json({ variables: cognigyVars });
}
return res.status(404).json({ error: 'Unsupported intent' });
} catch (error) {
console.error('Webhook error:', error.message);
return res.status(500).json({ error: 'Processing failed', details: error.message });
}
}));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Cognigy webhook server listening on port ${PORT}`));
Deploy this file to a cloud provider with a public HTTPS endpoint. Configure the Cognigy.AI webhook node to point to https://your-domain.com/webhook/cognigy and set the timeout threshold to ten seconds to accommodate the eight-second Axios limit plus network latency.
Common Errors & Debugging
Error: 401 Unauthorized from External API
- Cause: The cached token expired or the OAuth token endpoint rejected the client credentials.
- Fix: Verify
EXTERNAL_API_CLIENT_IDandEXTERNAL_API_CLIENT_SECRETin the environment configuration. The token manager automatically queues concurrent requests during refresh. Add explicit logging before the Axios call to confirm token acquisition. - Code adjustment: The
fetchCustomerOrderfunction already forces a refresh on 401 responses. Ensure the external API returns standard OAuth 2.0 error codes.
Error: 408 Request Timeout or Cognigy Dialog Hang
- Cause: The upstream API exceeds the eight-second Axios timeout, or the server fails to send a response before Cognigy cancels the connection.
- Fix: Reduce the
timeoutvalue in the Axios configuration to five seconds for non-critical data. Return a fallback variable set immediately if the timeout triggers. - Code showing the fix:
try {
const response = await axios.get(url, { timeout: 5000, validateStatus: s => s === 200 });
return mapToCognigyVariables(response.data);
} catch (error) {
if (error.code === 'ECONNABORTED') {
return mapToCognigyVariables(null); // Returns orderFound: false
}
throw error;
}
Error: 400 Bad Request from Cognigy.AI
- Cause: The webhook response lacks the
variableswrapper object, or the JSON structure contains undefined values that Cognigy cannot merge. - Fix: Always return
{ variables: { ... } }from the Express route. Use the transformation function to guarantee every expected key exists. - Verification: Log the exact response body sent to Cognigy. Use
JSON.stringify(cognigyVars, null, 2)to validate structure before transmission.
Error: 429 Too Many Requests
- Cause: The external API enforces rate limits, or multiple concurrent conversations trigger simultaneous webhook executions.
- Fix: Implement exponential backoff for retries. Cache frequent responses when data does not change rapidly.
- Code showing the fix:
if (error.response?.status === 429) {
const delay = (error.response.headers['retry-after'] || 2) * 1000;
await new Promise(r => setTimeout(r, delay));
// Retry logic or return cached fallback
}