WhatsApp Multi-modal Proxy Server
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.0 $ PORT=3000
server.js
1 require('dotenv').config(); 2 const express = require('express'); 3 const bodyParser = require('body-parser'); 4 const axios = require('axios'); 5 const VoximplantApiClient = require('@voximplant/apiclient-nodejs').default; 6 7 const app = express(); 8 const PORT = process.env.PORT || 3000; 9 // Path to voximplant service account json file 10 const parameters = { 11 pathToCredentials: './voximplant_service_account.json', 12 }; 13 const client = new VoximplantApiClient(parameters); 14 client.onReady = function () { 15 console.log("Voximplant API client is ready."); 16 }; 17 18 19 // Middleware 20 app.use(bodyParser.json()); 21 22 // Environment variables (similar to Lambda environment variables) 23 const config = { 24 whatsappToken: process.env.WHATSAPP_TOKEN, 25 whatsappPhoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID, 26 whatsappWebhookVerifyToken: process.env.WHATSAPP_WEBHOOK_VERIFY_TOKEN, 27 apiVersion: process.env.API_VERSION || 'v18.0' 28 }; 29 30 // WhatsApp API base URL 31 const whatsappApiUrl = `https://graph.facebook.com/${config.apiVersion}/${config.whatsappPhoneNumberId}`; 32 33 // Webhook verification endpoint (GET) 34 app.get('/webhook', (req, res) => { 35 console.log('Webhook verification request received'); 36 37 const mode = req.query['hub.mode']; 38 const token = req.query['hub.verify_token']; 39 const challenge = req.query['hub.challenge']; 40 41 if (mode && token) { 42 if (mode === 'subscribe' && token === config.whatsappWebhookVerifyToken) { 43 console.log('WEBHOOK_VERIFIED'); 44 res.status(200).send(challenge); 45 } else { 46 console.log('Verification failed - invalid token'); 47 res.sendStatus(403); 48 } 49 } else { 50 console.log('Verification failed - missing parameters'); 51 res.sendStatus(400); 52 } 53 }); 54 55 // Webhook message endpoint (POST) 56 // NOTE: For production use, validate the X-Hub-Signature-256 header using your app secret 57 // to verify that requests originate from Meta. See: https://developers.facebook.com/docs/messenger-platform/webhooks#validate-payloads 58 app.post('/webhook', async (req, res) => { 59 console.log('Webhook message received:', JSON.stringify(req.body, null, 2)); 60 61 try { 62 // Immediately respond to WhatsApp to acknowledge receipt 63 res.status(200).send('EVENT_RECEIVED'); 64 65 // Process the webhook event 66 await processWebhookEvent(req.body); 67 } catch (error) { 68 console.error('Error processing webhook:', error); 69 // Note: We already sent 200, so we can't change the response now 70 // In a production scenario, you might want to implement proper error handling 71 } 72 }); 73 74 // Process incoming webhook events 75 async function processWebhookEvent(body) { 76 const entry = body.entry?.[0]; 77 if (!entry) { 78 console.log('No entry found in webhook'); 79 return; 80 } 81 82 const changes = entry.changes?.[0]; 83 if (!changes || changes.field !== 'messages') { 84 console.log('No relevant changes found'); 85 return; 86 } 87 88 const messages = changes.value.messages; 89 if (!messages || messages.length === 0) { 90 console.log('No messages found in webhook'); 91 return; 92 } 93 94 for (const message of messages) { 95 await processMessage(message); 96 } 97 } 98 99 // Process individual message 100 async function processMessage(message) { 101 console.log('Processing message:', message); 102 103 const from = message.from; 104 const messageType = message.type; 105 106 try { 107 switch (messageType) { 108 case 'text': 109 await handleTextMessage(from, message.text.body); 110 break; 111 case 'image': 112 await handleImageMessage(from, message.image); 113 break; 114 case 'audio': 115 await handleAudioMessage(from, message.audio); 116 break; 117 case 'document': 118 await handleDocumentMessage(from, message.document); 119 break; 120 case 'voiceai': 121 await sendTextMessage(from, message.text.body); 122 break; 123 default: 124 await sendTextMessage(from, `Sorry, I don't support ${messageType} messages yet.`); 125 break; 126 } 127 } catch (error) { 128 console.error(`Error processing ${messageType} message:`, error); 129 // Optionally send an error message to the user 130 await sendTextMessage(from, 'Sorry, I encountered an error processing your message.'); 131 } 132 } 133 134 // Handle text messages 135 async function handleTextMessage(from, text) { 136 console.log("Checking Voximplant WAB session for " + from); 137 try { 138 const ev = await client.KeyValueStorage.getKeyValueItem({ 139 key: "WAB_" + from, 140 applicationId: process.env.VOXIMPLANT_APP_ID, 141 }); 142 console.log(ev); 143 if (ev.result != undefined) { 144 console.log("WAB call exists"); 145 let url = ev.result.value; 146 if (url == undefined) { 147 console.log("Voximplant session URL hasn't been found"); 148 } else { 149 // make HTTP request to URL to communicate with the session 150 await sendMessageToVox(url, text); 151 } 152 } else { 153 console.log("WAB call doesn't exist"); 154 } 155 } catch (err) { 156 console.error(err); 157 throw err; 158 } 159 // console.log(`Received text message from ${from}: ${text}`); 160 161 // const lowerText = text.toLowerCase().trim(); 162 163 // if (lowerText.includes('hello') || lowerText.includes('hi')) { 164 // await sendTextMessage(from, 'Hello! Welcome to our service. How can I help you today?'); 165 // } else if (lowerText.includes('help')) { 166 // await sendTextMessage(from, 'I can help you with:\n- Product information\n- Support questions\n- General inquiries\n\nJust ask me anything!'); 167 // } else { 168 // await sendTextMessage(from, `You said: "${text}". This is an automated response.`); 169 // } 170 } 171 172 // Handle image messages 173 async function handleImageMessage(from, image) { 174 console.log(`Received image message from ${from}`); 175 await sendTextMessage(from, 'Thanks for the image! I received it successfully.'); 176 } 177 178 // Handle audio messages 179 async function handleAudioMessage(from, audio) { 180 console.log(`Received audio message from ${from}`); 181 await sendTextMessage(from, 'Thanks for the audio message! I received it successfully.'); 182 } 183 184 // Handle document messages 185 async function handleDocumentMessage(from, document) { 186 console.log(`Received document from ${from}: ${document.filename}`); 187 await sendTextMessage(from, `Thanks for the document "${document.filename}"! I received it successfully.`); 188 } 189 190 // Send text message to Voximplant WAB session 191 async function sendMessageToVox(url, text) { 192 try { 193 const response = await axios.post( 194 url, 195 { 196 text: { body: text }, 197 type: 'text' 198 }, 199 { 200 headers: { 201 'Content-Type': 'application/json' 202 } 203 } 204 ); 205 206 console.log('Message to Voximplant sent successfully:', response.data); 207 return response.data; 208 } catch (error) { 209 console.error('Error sending message:', error.response?.data || error.message); 210 throw error; 211 } 212 } 213 214 // Send text message via WhatsApp API 215 async function sendTextMessage(to, text) { 216 console.log('Sending text message to: ' + to); 217 try { 218 const response = await axios.post( 219 `${whatsappApiUrl}/messages`, 220 { 221 messaging_product: 'whatsapp', 222 to: to, 223 text: { body: text }, 224 type: 'text' 225 }, 226 { 227 headers: { 228 'Authorization': `Bearer ${config.whatsappToken}`, 229 'Content-Type': 'application/json' 230 } 231 } 232 ); 233 234 console.log('Message sent successfully:', response.data); 235 return response.data; 236 } catch (error) { 237 console.error('Error sending message:', error.response?.data || error.message); 238 throw error; 239 } 240 } 241 242 // Additional function to send template messages (if needed) 243 async function sendTemplateMessage(to, templateName, languageCode = 'en') { 244 try { 245 const response = await axios.post( 246 `${whatsappApiUrl}/messages`, 247 { 248 messaging_product: 'whatsapp', 249 to: to, 250 type: 'template', 251 template: { 252 name: templateName, 253 language: { 254 code: languageCode 255 } 256 } 257 }, 258 { 259 headers: { 260 'Authorization': `Bearer ${config.whatsappToken}`, 261 'Content-Type': 'application/json' 262 } 263 } 264 ); 265 266 console.log('Template message sent successfully:', response.data); 267 return response.data; 268 } catch (error) { 269 console.error('Error sending template message:', error.response?.data || error.message); 270 throw error; 271 } 272 } 273 274 // Health check endpoint 275 app.get('/health', (req, res) => { 276 res.status(200).json({ 277 status: 'OK', 278 message: 'WhatsApp Business API server is running', 279 timestamp: new Date().toISOString() 280 }); 281 }); 282 283 // Start server 284 app.listen(PORT, () => { 285 console.log(`WhatsApp Business API server running on port ${PORT}`); 286 console.log(`Webhook URL: http://localhost:${PORT}/webhook`); 287 console.log(`Health check: http://localhost:${PORT}/health`); 288 }); 289 290 // Graceful shutdown 291 process.on('SIGTERM', () => { 292 console.log('SIGTERM received, shutting down gracefully'); 293 process.exit(0); 294 }); 295 296 process.on('SIGINT', () => { 297 console.log('SIGINT received, shutting down gracefully'); 298 process.exit(0); 299 });