messaging channels

Sent logo
Sent TeamMar 8, 2026 / messaging channels / MessageBird

WhatsApp Integration with Node.js Express and MessageBird (2025 Guide)

Build a production-ready WhatsApp messaging integration using MessageBird Conversations API and Node.js Express. Complete tutorial with webhook handling, signature verification, and deployment guide.

Build WhatsApp Integration with Node.js Express and MessageBird API

Time Estimate: 45โ€“60 minutes | Skill Level: Intermediate (requires Node.js, REST APIs, webhook concepts)

Integrate WhatsApp messaging into your Node.js applications using MessageBird's Conversations API. This guide provides a complete walkthrough for building a production-ready Express application capable of sending and receiving WhatsApp messages, handling errors, ensuring security, and deploying reliably.

You'll build a simple "echo bot" ๐Ÿค– โ€“ an application that receives WhatsApp messages via a MessageBird webhook and replies with the same message content. This serves as a foundational example demonstrating the core integration mechanics.

What You'll Build with MessageBird WhatsApp API

A Node.js application using the Express framework that:

  1. Sends outbound WhatsApp messages via the MessageBird API
  2. Receives inbound WhatsApp messages via a MessageBird webhook
  3. Replies to incoming messages (simple echo functionality)
  4. Includes robust error handling, security considerations, and deployment guidance

Problem Solved:

Programmatically interact with users on WhatsApp for customer support, notifications, alerts, or chatbot functionality directly within your Node.js applications, leveraging MessageBird's reliable infrastructure.

Technologies Used:

  • Node.js (v14+) โ€“ Asynchronous JavaScript runtime environment
  • Express (v4+) โ€“ Minimalist and flexible Node.js web application framework
  • MessageBird Conversations API โ€“ Third-party service for sending and receiving WhatsApp messages
  • axios (v1+) โ€“ Promise-based HTTP client for making API requests to MessageBird
  • dotenv (v16+) โ€“ Module to load environment variables from a .env file
  • crypto โ€“ Node.js module for cryptographic functions (webhook signature verification)

Version Compatibility:

ComponentMinimum VersionRecommendedNotes
Node.js14.x18.x LTS or 20.x LTSSupports ES6+ features, async/await
Express4.17+4.19+JSON body parsing built-in
axios0.27+1.6+Modern promise-based HTTP client
dotenv10.0+16.4+Environment variable management

Why These Technologies?

  • Node.js/Express โ€“ Popular, efficient, and scalable choice for building web servers and APIs in JavaScript
  • MessageBird โ€“ Provides a unified API (Conversations API) to manage multiple messaging channels, including WhatsApp, simplifying integration compared to managing the WhatsApp Business API directly. Handles infrastructure, scaling, and compliance aspects
  • axios โ€“ Standard and easy-to-use library for making HTTP requests
  • dotenv โ€“ Best practice for managing configuration and secrets during development

System Architecture:

+-----------------+ +---------------------+ +---------------------+ +-----------------+ | User (WhatsApp) | <--> | WhatsApp Platform | <--> | MessageBird Service | <--> | Node.js/Express | | | | (Meta Infrastructure)| | (Conversations API) | | Application | +-----------------+ +---------------------+ +---------------------+ +-----------------+ ^ | | ^ (API Calls) | | (Messages) | | (Webhook Post) v | v | +---------------------------------------------------------------------------------+ | 1. User sends message to App's WhatsApp number. | | 2. WhatsApp platform routes message to MessageBird. | | 3. MessageBird POSTs message data to App's configured webhook URL. | | 4. App verifies webhook signature, processes message. | | 5. App makes API call to MessageBird (Conversations API) to send reply. | | 6. MessageBird sends reply via WhatsApp Platform to the User. | | 7. On errors: Retry with exponential backoff (up to 10 attempts). | | 8. On rate limits: Buffer messages, queue processing. | +---------------------------------------------------------------------------------+

Prerequisites:

  • Node.js and npm (or yarn) โ€“ Installed on your development machine. Download Node.js
  • MessageBird Account โ€“ A registered account with MessageBird. Sign up for MessageBird
  • Approved WhatsApp Business Channel โ€“ You must have gone through MessageBird's WhatsApp onboarding process and have an active, approved WhatsApp channel connected to your account. Note the Channel ID
  • MessageBird API Key โ€“ An API Access Key (Live key required for actual sending/receiving)
  • MessageBird Webhook Signing Key โ€“ Used to verify incoming webhooks
  • Publicly Accessible URL โ€“ For MessageBird to send webhooks to during development (use ngrok for this, but production requires a stable, deployed URL โ€“ see Deployment section)

Cost Considerations:

MessageBird charges consist of two components:

  1. WhatsApp Messaging Costs (Meta Pass-through):

    • Service Messages: Free (responses within 24-hour customer service window)
    • Marketing Messages: $0.005โ€“$0.10 per message (varies by country; effective July 2025)
    • Utility Messages: $0.004โ€“$0.08 per message (transaction updates, account alerts)
    • Authentication Messages: $0.003โ€“$0.05 per message (OTP, 2FA codes)
    • Charges apply when messages are delivered (not sent)
  2. MessageBird Platform Fees:

    • Free tier: 10 test credits for development
    • Production: $0.005+ per message (added to Meta costs)
    • Rate limits: 50 GET req/s, 500 POST req/s per workspace (default tier)

Example Monthly Cost Estimate:

  • 10,000 utility messages (US market): ~$40โ€“$90 (Meta) + ~$50 (MessageBird) = $90โ€“$140/month
  • See WhatsApp Business Platform Pricing for current rates by country

1. Set Up Your Node.js WhatsApp Project

Initialize your Node.js project and install the necessary dependencies.

  1. Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.

    bash
    mkdir node-messagebird-whatsapp
    cd node-messagebird-whatsapp
  2. Initialize npm: Create a package.json file.

    bash
    npm init -y
  3. Install Dependencies: Install Express, Axios, and dotenv.

    bash
    npm install express axios dotenv

    (Optional: If using yarn, use yarn add express axios dotenv)

  4. Install Dev Dependency (Optional but Recommended): Install nodemon for automatic server restarts during development.

    bash
    npm install --save-dev nodemon

    (Optional: yarn add --dev nodemon)

  5. Create Project Structure: Set up a basic folder structure for better organization.

    • node-messagebird-whatsapp/

      • src/
        • controllers/
          • messageController.js
        • routes/
          • messageRoutes.js
        • services/
          • messagebirdService.js
        • utils/
          • verifyWebhook.js
        • app.js
      • .env
      • .gitignore
      • package.json
    • controllers โ€“ Handle request logic

    • routes โ€“ Define API endpoints

    • services โ€“ Encapsulate third-party API interactions (MessageBird)

    • utils โ€“ Helper functions (like webhook verification)

    • app.js โ€“ Main application entry point

    • .env โ€“ Store environment variables (API keys, etc.)

    • .gitignore โ€“ Specify intentionally untracked files that Git should ignore

  6. Create .gitignore: Create a .gitignore file in the root directory to prevent committing sensitive information and unnecessary files.

    text
    # .gitignore
    node_modules/
    .env
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
  7. Create .env File: Create a .env file in the root directory. You'll populate this with credentials obtained from MessageBird later.

    dotenv
    # .env
    PORT=3000
    MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_API_KEY
    MESSAGEBIRD_WHATSAPP_CHANNEL_ID=YOUR_WHATSAPP_CHANNEL_ID
    MESSAGEBIRD_WEBHOOK_SIGNING_KEY=YOUR_WEBHOOK_SIGNING_KEY

    Explanation:

    • PORT โ€“ The port your Express server will listen on
    • MESSAGEBIRD_API_KEY โ€“ Your live API key from the MessageBird dashboard. Treat this like a password.
    • MESSAGEBIRD_WHATSAPP_CHANNEL_ID โ€“ The unique ID of your configured WhatsApp channel in MessageBird
    • MESSAGEBIRD_WEBHOOK_SIGNING_KEY โ€“ The secret key provided by MessageBird to verify incoming webhooks. Treat this like a password.
  8. Configure package.json Scripts: Add scripts to your package.json for starting the server easily.

    json
    // package.json (partial)
    {
      "scripts": {
        "start": "node src/app.js",
        "dev": "nodemon src/app.js"
      }
    }

    Now you can run npm run dev for development (uses nodemon) or npm start to run the application normally.

2. Implement Core WhatsApp Messaging Functionality

Start by setting up the Express server and then implement the logic for sending and receiving messages.

  1. Set up Basic Express Server (src/app.js):

    javascript
    // src/app.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    const messageRoutes = require('./routes/messageRoutes');
    
    const app = express();
    const port = process.env.PORT || 3000;
    
    // Middleware to parse raw body for webhook verification
    // IMPORTANT: This needs to be BEFORE express.json() for the webhook route
    app.use('/webhook', express.raw({ type: 'application/json' }));
    
    // Middleware to parse JSON bodies for other routes
    app.use(express.json());
    
    // Mount the message routes
    app.use('/', messageRoutes);
    
    // Basic health check endpoint
    app.get('/health', (req, res) => {
      res.status(200).send('OK');
    });
    
    // Global error handler (basic example)
    app.use((err, req, res, next) => {
      console.error('Unhandled error:', err);
      res.status(500).send('Something broke!');
    });
    
    app.listen(port, () => {
      console.log(`Server listening on port ${port}`);
    });

    Why express.raw before express.json? Webhook signature verification requires the raw, unparsed request body. express.json() consumes the body stream, making it unavailable later. Apply express.raw only to the /webhook path. Other paths need express.json() which should be applied globally or to specific routes after the raw middleware for the webhook.

  2. Implement MessageBird Service (src/services/messagebirdService.js): This service encapsulates all interactions with the MessageBird API.

    javascript
    // src/services/messagebirdService.js
    const axios = require('axios');
    
    const MESSAGEBIRD_API_ENDPOINT = 'https://conversations.messagebird.com/v1';
    
    const messagebirdClient = axios.create({
      baseURL: MESSAGEBIRD_API_ENDPOINT,
      headers: {
        'Authorization': `AccessKey ${process.env.MESSAGEBIRD_API_KEY}`,
        'Content-Type': 'application/json',
      }
    });
    
    /**
     * Sends a WhatsApp message via MessageBird Conversations API.
     * Uses the /conversations/start endpoint which handles finding or creating
     * a conversation.
     * @param {string} to - Recipient's phone number (E.164 format).
     * @param {string} text - The message text content.
     * @returns {Promise<object>} - The response data from MessageBird API.
     */
    const sendWhatsAppMessage = async (to, text) => {
      try {
        const payload = {
          to: to,
          type: 'text',
          content: {
            text: text
          },
          channelId: process.env.MESSAGEBIRD_WHATSAPP_CHANNEL_ID
        };
        console.log('Sending message via MessageBird:', JSON.stringify(payload, null, 2));
    
        // Using /conversations/start simplifies sending; MessageBird handles
        // checking if a conversation exists or creating a new one.
        const response = await messagebirdClient.post('/conversations/start', payload);
    
        console.log('MessageBird API Response:', response.data);
        return response.data;
      } catch (error) {
        console.error('Error sending MessageBird message:', error.response ? error.response.data : error.message);
    
        // Handle rate limiting (429 status code)
        if (error.response?.status === 429) {
          console.warn('Rate limit exceeded. Implement exponential backoff or queue system.');
          // In production, implement retry logic or message queue
        }
    
        // Re-throw the error to be handled by the controller
        throw error;
      }
    };
    
    module.exports = {
      sendWhatsAppMessage,
    };

    Why /conversations/start? This MessageBird endpoint is convenient as it either starts a new conversation or adds the message to an existing one based on the recipient and channel ID, reducing the logic needed in your application.

    Rate Limiting: MessageBird Conversations API enforces workspace-level limits of 50 GET req/s and 500 POST req/s. Status code 429 indicates rate limiting. Individual channels may have additional downstream provider limits; MessageBird automatically buffers and retries these up to 10 times with exponential backoff.

  3. Implement Webhook Verification Utility (src/utils/verifyWebhook.js): This function verifies the signature of incoming webhooks from MessageBird.

    javascript
    // src/utils/verifyWebhook.js
    const crypto = require('crypto');
    
    /**
     * Verifies the signature of an incoming MessageBird webhook request.
     * @param {Buffer} rawBody - The raw request body buffer.
     * @param {string} signatureHeader - The value of the 'MessageBird-Signature' header.
     * @param {string} timestampHeader - The value of the 'MessageBird-Request-Timestamp' header.
     * @param {string} signingKey - Your MessageBird webhook signing key.
     * @returns {boolean} - True if the signature is valid, false otherwise.
     */
    const verifyMessageBirdWebhook = (rawBody, signatureHeader, timestampHeader, signingKey) => {
      if (!rawBody || !signatureHeader || !timestampHeader || !signingKey) {
        console.warn('Webhook verification missing required parameters.');
        return false;
      }
    
      try {
        // MessageBird uses `MessageBird-Signature` (sig) and `MessageBird-Request-Timestamp` (ts).
        // The signature is calculated over `timestamp.body`.
        const signedPayload = `${timestampHeader}.${rawBody.toString('utf8')}`;
    
        const expectedSignature = crypto
          .createHmac('sha256', signingKey)
          .update(signedPayload)
          .digest('hex');
    
        // Ensure the provided signature is treated as hex
        const providedSignatureBuffer = Buffer.from(signatureHeader, 'hex');
        const expectedSignatureBuffer = Buffer.from(expectedSignature, 'hex');
    
        // Handle potential length differences before comparison
        if (providedSignatureBuffer.length !== expectedSignatureBuffer.length) {
            console.warn('Webhook signature length mismatch.');
            return false;
        }
    
        // Constant-time comparison to prevent timing attacks
        const isValid = crypto.timingSafeEqual(providedSignatureBuffer, expectedSignatureBuffer);
    
        if (!isValid) {
            console.warn('Webhook signature mismatch.', { received: signatureHeader, expected: expectedSignature });
        }
    
        return isValid;
    
      } catch (error) {
        console.error('Error during webhook signature verification:', error);
        return false;
      }
    };
    
    module.exports = {
      verifyMessageBirdWebhook,
    };

    Why crypto.timingSafeEqual? Using a simple === comparison for signatures can be vulnerable to timing attacks. timingSafeEqual performs the comparison in constant time, mitigating this risk.

  4. Implement Message Controller (src/controllers/messageController.js): This handles the logic for both the API endpoint (sending) and the webhook (receiving).

    javascript
    // src/controllers/messageController.js
    const messagebirdService = require('../services/messagebirdService');
    const { verifyMessageBirdWebhook } = require('../utils/verifyWebhook');
    
    // Controller for the manual send API endpoint
    const sendManualMessage = async (req, res, next) => {
      const { to, text } = req.body;
    
      // Basic validation
      if (!to || !text) {
        return res.status(400).json({ error: 'Missing required fields: "to" and "text".' });
      }
      if (!process.env.MESSAGEBIRD_WHATSAPP_CHANNEL_ID) {
          console.error('WhatsApp Channel ID is not configured in .env');
          return res.status(500).json({ error: 'Server configuration error.' });
      }
    
      try {
        const result = await messagebirdService.sendWhatsAppMessage(to, text);
        res.status(200).json({ success: true, messageId: result.id, status: result.status });
      } catch (error) {
        // Logged in the service, pass to global error handler or send specific response
        res.status(500).json({ success: false, error: 'Failed to send message via MessageBird.' });
      }
    };
    
    // Controller for the MessageBird Webhook
    const handleWebhook = async (req, res, next) => {
      // 1. Verify Signature
      const signature = req.headers['messagebird-signature'];
      const timestamp = req.headers['messagebird-request-timestamp'];
      const signingKey = process.env.MESSAGEBIRD_WEBHOOK_SIGNING_KEY;
    
      // IMPORTANT: Use the raw body stored by the middleware
      // req.body will be a Buffer here because of `express.raw` on the /webhook route
      const rawBody = req.body;
    
      if (!(rawBody instanceof Buffer)) {
          console.error('Webhook error: Raw body not available. Check middleware order.');
          return res.status(500).send('Internal Server Error: Invalid body parser configuration.');
      }
    
      if (!verifyMessageBirdWebhook(rawBody, signature, timestamp, signingKey)) {
        console.warn('Webhook signature verification failed.');
        return res.status(403).send('Forbidden: Invalid signature.');
      }
    
      console.log('Webhook signature verified successfully.');
    
      // 2. Parse the JSON body (now that signature is verified)
      let payload;
      try {
          payload = JSON.parse(rawBody.toString('utf8'));
          console.log('Received webhook payload:', JSON.stringify(payload, null, 2));
      } catch (e) {
          console.error('Failed to parse webhook JSON:', e);
          return res.status(400).send('Bad Request: Invalid JSON.');
      }
    
      // 3. Process the message (Example: Echo Bot)
      // Check if it's an incoming message event and has text content
      if (payload.type === 'message.created' && payload.message?.direction === 'received' && payload.message?.type === 'text') {
        const sender = payload.message.from;
        const receivedText = payload.message.content.text;
        const conversationId = payload.conversation.id; // Use conversation ID for replies
    
        console.log(`Received text message "${receivedText}" from ${sender} in conversation ${conversationId}`);
    
        // Simple Echo: Send the same text back
        const replyText = `You said: ${receivedText}`;
    
        try {
          // Note: Send back to the 'sender' number directly. MessageBird handles routing it
          // within the correct conversation context based on channelId and sender.
          await messagebirdService.sendWhatsAppMessage(sender, replyText);
          console.log(`Sent echo reply to ${sender}`);
        } catch (error) {
          console.error(`Failed to send echo reply to ${sender}:`, error);
          // Don't send error back to MessageBird here, just log it.
          // Sending 500 might cause MessageBird to retry the webhook unnecessarily.
        }
      } else {
        console.log('Webhook received, but not a processable text message:', payload.type, payload.message?.type);
      }
    
      // 4. Acknowledge receipt to MessageBird
      // Send a 200 OK quickly to prevent retries.
      res.status(200).send('OK');
    };
    
    module.exports = {
      sendManualMessage,
      handleWebhook,
    };
  5. Define Routes (src/routes/messageRoutes.js): Connect the URL paths to the controller functions.

    javascript
    // src/routes/messageRoutes.js
    const express = require('express');
    const messageController = require('../controllers/messageController');
    
    const router = express.Router();
    
    // Endpoint to manually trigger sending a WhatsApp message
    // POST /send-whatsapp
    // Body: { "to": "E.164_phone_number", "text": "Your message content" }
    router.post('/send-whatsapp', messageController.sendManualMessage);
    
    // Endpoint for MessageBird to POST webhook events
    // POST /webhook
    router.post('/webhook', messageController.handleWebhook);
    
    module.exports = router;

3. Build a Complete WhatsApp API Layer

Your current API layer is simple, consisting of the /send-whatsapp endpoint.

  • Authentication/Authorization: For this example, the endpoint is unprotected. In a production scenario, add middleware here to protect it (e.g., API keys, JWT tokens, OAuth) to ensure only authorized clients can trigger messages.

    Production Authentication Middleware Example:

    javascript
    // src/middleware/auth.js
    const authenticateApiKey = (req, res, next) => {
      const apiKey = req.headers['x-api-key'];
      const validKeys = process.env.VALID_API_KEYS?.split(',') || [];
    
      if (!apiKey || !validKeys.includes(apiKey)) {
        return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key' });
      }
    
      next();
    };
    
    module.exports = { authenticateApiKey };
    
    // In routes/messageRoutes.js:
    // const { authenticateApiKey } = require('../middleware/auth');
    // router.post('/send-whatsapp', authenticateApiKey, messageController.sendManualMessage);

    JWT-based Authentication Example:

    javascript
    // src/middleware/auth.js (JWT version)
    const jwt = require('jsonwebtoken');
    
    const authenticateJWT = (req, res, next) => {
      const token = req.headers.authorization?.split(' ')[1]; // Bearer <token>
    
      if (!token) {
        return res.status(401).json({ error: 'Unauthorized: No token provided' });
      }
    
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded; // Attach user info to request
        next();
      } catch (error) {
        return res.status(403).json({ error: 'Forbidden: Invalid token' });
      }
    };
    
    module.exports = { authenticateJWT };
  • Request Validation: You have basic validation in the controller. For more complex validation (data types, formats, lengths), use libraries like joi or express-validator.

    javascript
    // Example using express-validator (install it first: npm install express-validator)
    // const { body, validationResult } = require('express-validator');
    // router.post('/send-whatsapp',
    //   body('to').isMobilePhone('any', { strictMode: true }).withMessage('Invalid phone number format (E.164 required)'),
    //   body('text').isString().notEmpty().withMessage('Text content cannot be empty'),
    //   (req, res, next) => {
    //     const errors = validationResult(req);
    //     if (!errors.isEmpty()) {
    //       return res.status(400).json({ errors: errors.array() });
    //     }
    //     next(); // Proceed to controller if validation passes
    //   },
    //   messageController.sendManualMessage
    // );
  • API Endpoint Documentation (/send-whatsapp):

    • Method: POST
    • URL: /send-whatsapp
    • Body (JSON):
      json
      {
        "to": "+14155552671",
        "text": "Hello from our Node.js App!"
      }
    • Success Response (200 OK):
      json
      {
        "success": true,
        "messageId": "mb_message_id_string",
        "status": "accepted"
      }
    • Error Response (400 Bad Request - Validation):
      json
      {
        "error": "Missing required fields: \"to\" and \"text\"."
      }
      Or with express-validator:
      json
      {
        "errors": [
          {
            "type": "field",
            "value": "",
            "msg": "Text content cannot be empty",
            "path": "text",
            "location": "body"
          }
        ]
      }
    • Error Response (500 Internal Server Error - API Failure):
      json
      {
        "success": false,
        "error": "Failed to send message via MessageBird."
      }
  • Test with curl: Replace placeholders with your actual recipient number and message. Make sure your server is running (npm run dev).

    bash
    curl -X POST http://localhost:3000/send-whatsapp \
    -H "Content-Type: application/json" \
    -d '{
      "to": "+1xxxxxxxxxx",
      "text": "Test message from curl!"
    }'

4. Integrate MessageBird Conversations API (Credentials & Configuration)

Get the necessary credentials from the MessageBird Dashboard.

  1. Get MessageBird API Key:

    • Log in to your MessageBird Dashboard
    • Navigate to Developers (usually in the left sidebar) โ†’ API access
    • Under "Live API Key", click Show key. Copy this value
    • Paste this value into your .env file for MESSAGEBIRD_API_KEY
  2. Get WhatsApp Channel ID:

    • In the MessageBird Dashboard, navigate to Channels (left sidebar)
    • Click on your configured WhatsApp channel
    • On the channel's configuration page, find the Channel ID (it's a long alphanumeric string). Copy this value
    • Paste this value into your .env file for MESSAGEBIRD_WHATSAPP_CHANNEL_ID
  3. Get Webhook Signing Key:

    • In the MessageBird Dashboard, navigate to Developers โ†’ Webhooks
    • Click Add webhook. If you already have one for Conversations API, click Edit
    • Under the Signing Key section, you'll find the key. If it's your first time, you might need to generate one. Copy this value
    • Paste this value into your .env file for MESSAGEBIRD_WEBHOOK_SIGNING_KEY
  4. Configure Webhook URL in MessageBird:

    • While still in the Webhook settings (Developers โ†’ Webhooks):
      • URL: You need a publicly accessible URL. During development, use ngrok.
        • Install ngrok: https://ngrok.com/download
        • Run ngrok http 3000 (or your app's port) in a separate terminal
        • Copy the https:// forwarding URL provided by ngrok (e.g., https://<random-string>.ngrok.io)
        • Paste this URL into the MessageBird webhook configuration, adding /webhook at the end (e.g., https://<random-string>.ngrok.io/webhook)
        • Remember, ngrok creates a temporary URL and is suitable only for development and testing. For production, deploy your application to a server with a stable, permanent public HTTPS URL (e.g., via a PaaS like Heroku/Render or your own server with a domain name).
      • Events: Ensure "Listen for Conversation API events" is checked, or select specific events like message.created and message.updated. For the echo bot, message.created is essential
      • Save the webhook configuration

Common ngrok Troubleshooting:

IssueError CodeSolution
Webhook not receivedERR_NGROK_8012ngrok agent connected but upstream service failed. Check that your Express server is running on the correct port (3000)
"Tunnel not found"ERR_NGROK_3200ngrok session expired or tunnel flagged. Restart ngrok with ngrok http 3000 and update webhook URL in MessageBird
Connection refusedโ€”Ensure your Express app is running before starting ngrok. Start order: Express server first, then ngrok
SSL/HTTPS errorsโ€”MessageBird requires HTTPS. Always use the https:// URL from ngrok, not http://
Signature verification failsโ€”Check .env has correct MESSAGEBIRD_WEBHOOK_SIGNING_KEY. Ensure express.raw middleware is applied to /webhook route
Account limit reachedERR_NGROK_108Free ngrok accounts limit simultaneous sessions. Stop other ngrok instances or upgrade account

Production Webhook URL: Replace ngrok with a deployed service URL (e.g., https://your-app.herokuapp.com/webhook or https://api.yourdomain.com/webhook)

  1. Environment Variables Handling:

    • Use the dotenv package, loaded at the very top of src/app.js (require('dotenv').config();)
    • Access variables using process.env.VARIABLE_NAME (e.g., process.env.MESSAGEBIRD_API_KEY)
    • Security: The .env file should never be committed to version control (ensure it's in .gitignore). In production environments (like Heroku, AWS, Docker), set these variables directly as environment variables on the platform, not via a .env file
  2. Fallback Mechanisms:

    • The current messagebirdService.js includes a try...catch block. If the MessageBird API call fails, it logs the error and throws it, allowing the controller to send a 500 response for the /send-whatsapp endpoint
    • For critical notifications, you might implement a retry mechanism (see Section 5) or a fallback channel (e.g., attempt SMS if WhatsApp fails, though this adds complexity)

5. Error Handling, Logging, and Retry Mechanisms for WhatsApp

Production applications require robust error handling and logging.

  • Consistent Error Strategy:
    • Service Layer (messagebirdService.js): Catches specific API call errors, logs detailed information (including error.response?.data from Axios for API error messages), and re-throws the error
    • Controller Layer (messageController.js): Catches errors from the service layer. For API endpoints (/send-whatsapp), it sends an appropriate HTTP error response (e.g., 500). For webhooks (/webhook), it logs the error but still tries to send a 200 OK back to MessageBird to prevent unnecessary retries unless it's a fatal error like signature failure (403) or bad JSON (400)
    • Global Error Handler (app.js): A final catch-all for unexpected synchronous or asynchronous errors passed via next(err). Logs the error and sends a generic 500 response
  • Logging:
    • Currently using console.log and console.error. This is acceptable for basic examples but insufficient for production
    • Recommendation: Use a dedicated logging library like Winston or Pino. These enable:
      • Different log levels (debug, info, warn, error)
      • Structured logging (JSON format, easier for machines to parse)
      • Multiple transports (log to console, files, external logging services like Datadog, Logstash)
    • Complete Winston Logging Example:
      javascript
      // src/utils/logger.js
      const winston = require('winston');
      
      const logger = winston.createLogger({
        level: process.env.LOG_LEVEL || 'info',
        format: winston.format.combine(
          winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
          winston.format.errors({ stack: true }),
          winston.format.json()
        ),
        defaultMeta: { service: 'messagebird-whatsapp' },
        transports: [
          // Console output for development
          new winston.transports.Console({
            format: winston.format.combine(
              winston.format.colorize(),
              winston.format.simple()
            )
          }),
          // File output for production
          new winston.transports.File({
            filename: 'logs/error.log',
            level: 'error',
            maxsize: 5242880, // 5MB
            maxFiles: 5
          }),
          new winston.transports.File({
            filename: 'logs/combined.log',
            maxsize: 5242880,
            maxFiles: 5
          })
        ],
      });
      
      // Create logs directory if it doesn't exist
      const fs = require('fs');
      if (!fs.existsSync('./logs')) {
        fs.mkdirSync('./logs');
      }
      
      module.exports = logger;
      
      // Usage in other files:
      // const logger = require('./utils/logger');
      // logger.info('Message sent successfully', { messageId: result.id });
      // logger.error('Failed to send message', { error: err.message, stack: err.stack });
  • Retry Mechanisms:
    • Network issues or temporary service outages can cause API calls to fail. Implementing retries can improve reliability
    • Strategy: Use exponential backoff โ€“ wait longer between each retry attempt
    • Implementation:
      • Manually implement a loop with setTimeout
      • Use a library like axios-retry which automatically handles retries for Axios requests based on configuration (e.g., retry count, status codes to retry on, backoff delay)
    • Example (Conceptual with axios-retry - install it: npm install axios-retry):
      javascript
      // src/services/messagebirdService.js (Modified)
      const axios = require('axios');
      const axiosRetry = require('axios-retry').default; // Use .default for CJS require
      
      const MESSAGEBIRD_API_ENDPOINT = 'https://conversations.messagebird.com/v1';
      
      const messagebirdClient = axios.create(/* ... as before ... */);
      
      // Apply retry logic to the axios instance
      axiosRetry(messagebirdClient, {
        retries: 3, // Number of retry attempts
        retryDelay: (retryCount) => {
          console.log(`Retry attempt: ${retryCount}`);
          return retryCount * 1000; // Exponential backoff (1s, 2s, 3s)
        },
        retryCondition: (error) => {
          // Retry on network errors or specific server errors (e.g., 5xx)
          return axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status >= 500;
        },
      });
      
      // ... rest of the service file ...
  • Testing Error Scenarios:
    • Temporarily use an invalid API key in .env to test authentication errors
    • Provide an invalid phone number format to /send-whatsapp to test validation errors
    • Disconnect your network temporarily to test network errors (if using retries)
    • Send an invalid payload structure to MessageBird (modify messagebirdService.js) to test API validation errors
    • Tamper with the signature or timestamp in a simulated webhook request (using Postman/curl) to test webhook verification failure
  • Log Analysis: In production, use log aggregation tools (ELK stack, Datadog, Splunk) to search, filter, and analyze logs from your application to quickly identify and diagnose errors. Structured logging (JSON) is crucial for this

6. Database Schema and Data Layer for WhatsApp Conversations

This simple echo bot does not require a database. However, in a more complex application, you would likely need one to store:

  • Conversation History: Log incoming/outgoing messages, timestamps, statuses
  • User Data: Link WhatsApp numbers to user accounts in your system, store preferences, state (for chatbots)
  • Message Status: Track MessageBird message status updates (sent, delivered, read) received via webhooks

Considerations:

  • Schema Design (Conceptual):

    • conversations table (conversation_id (PK), user_id (FK), channel_id, started_at)
    • messages table (message_id (PK), conversation_id (FK), messagebird_message_id, direction (in/out), content, type, status, timestamp)
    • users table (user_id (PK), whatsapp_number, name, etc.)
    • (An Entity Relationship Diagram would be beneficial here)
  • Practical Prisma Implementation Example:

    prisma
    // prisma/schema.prisma
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
    
    generator client {
      provider = "prisma-client-js"
    }
    
    model User {
      id              String         @id @default(uuid())
      whatsappNumber  String         @unique
      name            String?
      createdAt       DateTime       @default(now())
      conversations   Conversation[]
    }
    
    model Conversation {
      id              String    @id @default(uuid())
      userId          String
      channelId       String
      startedAt       DateTime  @default(now())
      user            User      @relation(fields: [userId], references: [id])
      messages        Message[]
    
      @@index([userId])
      @@index([channelId])
    }
    
    model Message {
      id                    String       @id @default(uuid())
      conversationId        String
      messagebirdMessageId  String?      @unique
      direction             String       // 'inbound' or 'outbound'
      content               String
      type                  String       // 'text', 'image', etc.
      status                String       // 'sent', 'delivered', 'read', 'failed'
      timestamp             DateTime     @default(now())
      conversation          Conversation @relation(fields: [conversationId], references: [id])
    
      @@index([conversationId])
      @@index([messagebirdMessageId])
      @@index([timestamp])
    }

    Usage in controller:

    javascript
    // Install: npm install @prisma/client
    // Setup: npx prisma migrate dev --name init
    const { PrismaClient } = require('@prisma/client');
    const prisma = new PrismaClient();
    
    // In messageController.js handleWebhook:
    // After receiving a message, save to database
    await prisma.message.create({
      data: {
        conversationId: conversationId,
        messagebirdMessageId: payload.message.id,
        direction: 'inbound',
        content: receivedText,
        type: 'text',
        status: 'received'
      }
    });
  • Data Access: Use an Object-Relational Mapper (ORM) like Prisma or Sequelize to interact with the database safely and efficiently

    • Prisma: Modern, type-safe ORM. Define schema in schema.prisma, run npx prisma migrate dev to create/update DB tables, use Prisma Client for queries
    • Sequelize: Mature, feature-rich ORM. Define models in JavaScript, use migrations for schema changes
  • Performance: Index frequently queried columns (e.g., whatsapp_number, conversation_id, timestamp). Use efficient queries. Consider database connection pooling

  • Data Population: Use migration scripts or seed scripts (often provided by ORMs) to populate initial data if needed

Frequently Asked Questions (FAQ)

How do I get a MessageBird WhatsApp Channel ID?

Log in to your MessageBird Dashboard, navigate to Channels in the left sidebar, and click on your configured WhatsApp channel. On the channel's configuration page, you'll find the Channel ID (a long alphanumeric string). Copy this value and paste it into your .env file as MESSAGEBIRD_WHATSAPP_CHANNEL_ID. You must complete MessageBird's WhatsApp onboarding process before receiving a Channel ID.

How do I verify MessageBird webhook signatures in Node.js?

MessageBird webhooks include MessageBird-Signature and MessageBird-Request-Timestamp headers. Use Node.js crypto module to create an HMAC SHA-256 hash of the timestamp and raw request body, then compare it to the provided signature using crypto.timingSafeEqual() for constant-time comparison. This prevents timing attacks. The tutorial includes a complete verifyWebhook.js utility with production-ready implementation.

What is the difference between MessageBird SMS API and Conversations API?

MessageBird SMS API sends only SMS messages to phone numbers. The Conversations API is a unified interface supporting multiple channels including WhatsApp, SMS, Facebook Messenger, and Telegram. For WhatsApp integration, you must use the Conversations API with the /conversations/start endpoint. The Conversations API automatically handles conversation threading and message routing across channels.

Can I send WhatsApp messages without webhooks?

Yes, you can send outbound WhatsApp messages using the /conversations/start endpoint without configuring webhooks. However, to receive inbound messages and build interactive experiences (like chatbots or two-way conversations), you must configure a webhook endpoint. MessageBird posts incoming message events to your webhook URL, which your application processes and responds to.

How do I test MessageBird WhatsApp webhooks locally?

Use ngrok to create a temporary public HTTPS URL that tunnels to your local development server. Install ngrok, run ngrok http 3000 (or your app's port), copy the https:// forwarding URL, and configure it in MessageBird Dashboard under Developers โ†’ Webhooks (add /webhook at the end). Remember that ngrok URLs are temporary and only suitable for development, not production.

What phone number format does MessageBird WhatsApp API require?

MessageBird requires phone numbers in E.164 format: a plus sign (+) followed by country code and phone number without spaces or special characters (e.g., +14155552671). The country code is required even for local numbers. Use libraries like libphonenumber-js for robust phone number validation and formatting before sending to MessageBird.

How do I handle rate limits with MessageBird Conversations API?

MessageBird enforces workspace-level rate limits: 50 GET requests/second and 500 POST requests/second (default tier). Individual channels may have additional downstream provider limits. When you receive a 429 status code, implement exponential backoff retry logic using libraries like axios-retry. MessageBird automatically buffers messages exceeding channel limits and retries up to 10 times. For high-volume applications, implement queue systems like BullMQ or RabbitMQ to throttle outbound requests. Contact MessageBird support to request higher limits for enterprise workloads.

Can I use MessageBird WhatsApp API with TypeScript?

Yes, this Express integration works seamlessly with TypeScript. Install @types/node, @types/express, and @types/axios as dev dependencies. Convert JavaScript files to .ts extension, add type annotations to function parameters and return values, and configure tsconfig.json. MessageBird doesn't provide official TypeScript types, but you can create interfaces for API request/response objects based on their documentation.

Next Steps: Enhance Your WhatsApp Integration

Now that you have a working WhatsApp echo bot, consider these enhancements in priority order:

  1. Rich Media Messages (High Priority) โ€“ Send images, documents, videos, and location messages using MessageBird's media message types. Update messagebirdService.js to support type: 'image', type: 'file', etc. with appropriate content objects. See MessageBird Conversations API - Message Types.

  2. Message Templates (High Priority) โ€“ Use pre-approved WhatsApp Business message templates for marketing and notifications outside the 24-hour service window. Required for compliance with WhatsApp policies. Configure templates in MessageBird Dashboard under Templates.

  3. Conversation State Management (Medium Priority) โ€“ Track conversation context and user sessions for multi-turn dialogues. Implement session storage (Redis, in-memory cache) to maintain user state between messages. Example: Store user preferences, shopping cart data, or conversation flow position.

  4. Interactive Buttons (Medium Priority) โ€“ Implement WhatsApp interactive messages with quick reply buttons and list pickers for better UX. Use MessageBird's interactive message types to present options (e.g., "Yes/No", "Select a product category").

  5. Natural Language Processing (Low Priority) โ€“ Integrate NLP services (Dialogflow, Wit.ai, OpenAI) for intelligent chatbot responses. Parse user intent and entities from incoming messages to provide contextual replies.

  6. Analytics and Monitoring (Low Priority) โ€“ Track message delivery rates, response times, and user engagement metrics. Integrate with APM tools (New Relic, Datadog) or build custom dashboards using database queries.

Additional Resources


Related Guides: