WhatsApp Multi-modal Proxy Server
Node.js webhook bridge for WhatsApp text + Voximplant voice sessions
For the complete documentation index, see llms.txt.
This page embeds the server code used in the WhatsApp multi-modal flow.
See this video for more information: Inbound WhatsApp calling demo.
Prerequisites
- Node.js 18+ runtime.
- A WhatsApp Business number connected to WhatsApp Cloud API in Meta.
- A Meta access token with permission to read webhook events and send WhatsApp messages.
- A Voximplant application with scenario logic that stores callback URLs in ApplicationStorage as
WAB_<caller_number>. - Voximplant service account credentials file available as
voximplant_service_account.json(or updatepathToCredentialsin code). - Public HTTPS endpoint for
/webhook(for Meta webhook delivery) and/health(optional health checks). - NPM dependencies installed:
npm install express body-parser axios dotenv @voximplant/apiclient-nodejs
Required environment variables:
WHATSAPP_TOKEN=...WHATSAPP_PHONE_NUMBER_ID=...WHATSAPP_WEBHOOK_VERIFY_TOKEN=...VOXIMPLANT_APP_ID=...
Optional environment variables:
API_VERSION=v18.0PORT=3000
server.js
require('dotenv').config();const express = require('express');const bodyParser = require('body-parser');const axios = require('axios');const VoximplantApiClient = require('@voximplant/apiclient-nodejs').default;const app = express();const PORT = process.env.PORT || 3000;// Path to voximplant service account json fileconst parameters = {pathToCredentials: './voximplant_service_account.json',};const client = new VoximplantApiClient(parameters);client.onReady = function () {console.log("Voximplant API client is ready.");};// Middlewareapp.use(bodyParser.json());// Environment variables (similar to Lambda environment variables)const config = {whatsappToken: process.env.WHATSAPP_TOKEN,whatsappPhoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID,whatsappWebhookVerifyToken: process.env.WHATSAPP_WEBHOOK_VERIFY_TOKEN,apiVersion: process.env.API_VERSION || 'v18.0'};// WhatsApp API base URLconst whatsappApiUrl = `https://graph.facebook.com/${config.apiVersion}/${config.whatsappPhoneNumberId}`;// Webhook verification endpoint (GET)app.get('/webhook', (req, res) => {console.log('Webhook verification request received');const mode = req.query['hub.mode'];const token = req.query['hub.verify_token'];const challenge = req.query['hub.challenge'];if (mode && token) {if (mode === 'subscribe' && token === config.whatsappWebhookVerifyToken) {console.log('WEBHOOK_VERIFIED');res.status(200).send(challenge);} else {console.log('Verification failed - invalid token');res.sendStatus(403);}} else {console.log('Verification failed - missing parameters');res.sendStatus(400);}});// Webhook message endpoint (POST)// NOTE: For production use, validate the X-Hub-Signature-256 header using your app secret// to verify that requests originate from Meta. See: https://developers.facebook.com/docs/messenger-platform/webhooks#validate-payloadsapp.post('/webhook', async (req, res) => {console.log('Webhook message received:', JSON.stringify(req.body, null, 2));try {// Immediately respond to WhatsApp to acknowledge receiptres.status(200).send('EVENT_RECEIVED');// Process the webhook eventawait processWebhookEvent(req.body);} catch (error) {console.error('Error processing webhook:', error);// Note: We already sent 200, so we can't change the response now// In a production scenario, you might want to implement proper error handling}});// Process incoming webhook eventsasync function processWebhookEvent(body) {const entry = body.entry?.[0];if (!entry) {console.log('No entry found in webhook');return;}const changes = entry.changes?.[0];if (!changes || changes.field !== 'messages') {console.log('No relevant changes found');return;}const messages = changes.value.messages;if (!messages || messages.length === 0) {console.log('No messages found in webhook');return;}for (const message of messages) {await processMessage(message);}}// Process individual messageasync function processMessage(message) {console.log('Processing message:', message);const from = message.from;const messageType = message.type;try {switch (messageType) {case 'text':await handleTextMessage(from, message.text.body);break;case 'image':await handleImageMessage(from, message.image);break;case 'audio':await handleAudioMessage(from, message.audio);break;case 'document':await handleDocumentMessage(from, message.document);break;case 'voiceai':await sendTextMessage(from, message.text.body);break;default:await sendTextMessage(from, `Sorry, I don't support ${messageType} messages yet.`);break;}} catch (error) {console.error(`Error processing ${messageType} message:`, error);// Optionally send an error message to the userawait sendTextMessage(from, 'Sorry, I encountered an error processing your message.');}}// Handle text messagesasync function handleTextMessage(from, text) {console.log("Checking Voximplant WAB session for " + from);try {const ev = await client.KeyValueStorage.getKeyValueItem({key: "WAB_" + from,applicationId: process.env.VOXIMPLANT_APP_ID,});console.log(ev);if (ev.result != undefined) {console.log("WAB call exists");let url = ev.result.value;if (url == undefined) {console.log("Voximplant session URL hasn't been found");} else {// make HTTP request to URL to communicate with the sessionawait sendMessageToVox(url, text);}} else {console.log("WAB call doesn't exist");}} catch (err) {console.error(err);throw err;}// console.log(`Received text message from ${from}: ${text}`);// const lowerText = text.toLowerCase().trim();// if (lowerText.includes('hello') || lowerText.includes('hi')) {// await sendTextMessage(from, 'Hello! Welcome to our service. How can I help you today?');// } else if (lowerText.includes('help')) {// await sendTextMessage(from, 'I can help you with:\n- Product information\n- Support questions\n- General inquiries\n\nJust ask me anything!');// } else {// await sendTextMessage(from, `You said: "${text}". This is an automated response.`);// }}// Handle image messagesasync function handleImageMessage(from, image) {console.log(`Received image message from ${from}`);await sendTextMessage(from, 'Thanks for the image! I received it successfully.');}// Handle audio messagesasync function handleAudioMessage(from, audio) {console.log(`Received audio message from ${from}`);await sendTextMessage(from, 'Thanks for the audio message! I received it successfully.');}// Handle document messagesasync function handleDocumentMessage(from, document) {console.log(`Received document from ${from}: ${document.filename}`);await sendTextMessage(from, `Thanks for the document "${document.filename}"! I received it successfully.`);}// Send text message to Voximplant WAB sessionasync function sendMessageToVox(url, text) {try {const response = await axios.post(url,{text: { body: text },type: 'text'},{headers: {'Content-Type': 'application/json'}});console.log('Message to Voximplant sent successfully:', response.data);return response.data;} catch (error) {console.error('Error sending message:', error.response?.data || error.message);throw error;}}// Send text message via WhatsApp APIasync function sendTextMessage(to, text) {console.log('Sending text message to: ' + to);try {const response = await axios.post(`${whatsappApiUrl}/messages`,{messaging_product: 'whatsapp',to: to,text: { body: text },type: 'text'},{headers: {'Authorization': `Bearer ${config.whatsappToken}`,'Content-Type': 'application/json'}});console.log('Message sent successfully:', response.data);return response.data;} catch (error) {console.error('Error sending message:', error.response?.data || error.message);throw error;}}// Additional function to send template messages (if needed)async function sendTemplateMessage(to, templateName, languageCode = 'en') {try {const response = await axios.post(`${whatsappApiUrl}/messages`,{messaging_product: 'whatsapp',to: to,type: 'template',template: {name: templateName,language: {code: languageCode}}},{headers: {'Authorization': `Bearer ${config.whatsappToken}`,'Content-Type': 'application/json'}});console.log('Template message sent successfully:', response.data);return response.data;} catch (error) {console.error('Error sending template message:', error.response?.data || error.message);throw error;}}// Health check endpointapp.get('/health', (req, res) => {res.status(200).json({status: 'OK',message: 'WhatsApp Business API server is running',timestamp: new Date().toISOString()});});// Start serverapp.listen(PORT, () => {console.log(`WhatsApp Business API server running on port ${PORT}`);console.log(`Webhook URL: http://localhost:${PORT}/webhook`);console.log(`Health check: http://localhost:${PORT}/health`);});// Graceful shutdownprocess.on('SIGTERM', () => {console.log('SIGTERM received, shutting down gracefully');process.exit(0);});process.on('SIGINT', () => {console.log('SIGINT received, shutting down gracefully');process.exit(0);});