code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Build SMS Marketing Campaigns with MessageBird, Node.js & Express: Complete Guide

Step-by-step guide to building production-ready SMS marketing campaigns using MessageBird API, Node.js, and Express. Includes webhook integration, MongoDB setup, TCPA compliance, batch sending, and deployment best practices.

Build Production-Ready SMS Marketing Campaigns with MessageBird, Node.js & Express

Learn how to build a complete SMS marketing campaign system using MessageBird API, Node.js, and Express. This comprehensive tutorial shows you how to create a production-ready application where users subscribe and unsubscribe via SMS while administrators broadcast marketing messages to opted-in subscribers. You'll implement webhook integration for two-way messaging, MongoDB database management, TCPA compliance features, and deployment best practices for scaling your SMS marketing campaigns.

This SMS campaign application solves the common business need to manage SMS marketing lists efficiently while adhering to opt-in/opt-out regulations and TCPA compliance requirements. Leverage MessageBird's programmable virtual mobile numbers (VMNs) and real-time webhooks to create a seamless two-way messaging experience for both subscribers and administrators, enabling automated subscription management and bulk message broadcasting.

Technologies Used:

  • Node.js: JavaScript runtime for building scalable network applications.
  • Express: Minimal and flexible Node.js web application framework.
  • MessageBird API: Communication platform providing APIs for SMS messaging and virtual numbers. (Node.js SDK v4.0.1, last updated 2022)
  • MongoDB: NoSQL database for storing subscriber information. (Node.js driver v6+)
  • dotenv: Module to load environment variables from a .env file.
  • Winston: Versatile logging library.
  • Ngrok/Localtunnel: (For development) Tools to expose your local server to the internet for webhook testing.

System Architecture:

text
+----------+       SMS       +-------------+      Webhook       +-----------------+      DB Ops       +-----------+
| End User | <-------------> | MessageBird | -----------------> | Node.js/Express | <---------------> | MongoDB   |
+----------+ (SUBSCRIBE/STOP)+-------------+  (POST /webhook)   |      App        | (Find/Update User)+-----------+
     ^                                        (VMN Config)       +-----------------+                   ^
     | SMS                                                             |   ^                           |
     |                                                            Admin UI | Write                     |
+----|-----+       API Call      +-------------+                   (POST /send)                        |
|  Admin   | ------------------> | MessageBird | <-----------------------+---------------------------+
+----------+   (Send Messages)   +-------------+                  (Read Subscribers)
 (Web Form)

Prerequisites:

  • Node.js and npm installed. (Download Node.js)
  • A MessageBird account. (Sign up for MessageBird)
  • Access to a MongoDB database (local instance or a cloud service like MongoDB Atlas).
  • (Optional but Recommended for Dev) Ngrok or Localtunnel installed.

By the end of this guide, you'll have a functional SMS subscription management and broadcasting application ready for further customization and deployment. For related topics, see our guides on E.164 phone number formatting and 10DLC SMS registration requirements for US businesses.


1. Set up the Node.js Project with MessageBird SDK

Initialize your Node.js project and install the necessary dependencies for your SMS marketing campaign application. This section covers project initialization, dependency installation, environment variable configuration, and basic Express server setup with security middleware.

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

    bash
    mkdir messagebird-sms-campaign
    cd messagebird-sms-campaign
  2. Initialize npm: Create a package.json file to manage dependencies.

    bash
    npm init -y
  3. Install Dependencies: Install Express for the web server, the MessageBird SDK, the MongoDB driver, dotenv for environment variables, and winston for logging. Modern Express versions include body parsing middleware, so body-parser is not strictly required as a separate dependency.

    bash
    npm install express messagebird mongodb dotenv winston express-rate-limit helmet
    • express: Web framework (includes JSON and URL-encoded body parsing).
    • messagebird: Official SDK for interacting with the MessageBird API.
    • mongodb: Driver for connecting to and interacting with MongoDB.
    • dotenv: Loads environment variables from .env into process.env.
    • winston: Robust logging library.
    • express-rate-limit: Middleware for rate limiting requests (added in Security section).
    • helmet: Middleware for setting secure HTTP headers (added in Security section).
  4. Create .gitignore: Prevent sensitive files and unnecessary directories from being committed to version control. Create a file named .gitignore with the following content:

    text
    # Environment variables
    .env
    
    # Logs
    *.log
    logs/
    
    # Dependency directories
    node_modules/
    
    # Build artifacts
    dist/
    build/
    
    # OS generated files
    .DS_Store
    Thumbs.db
  5. Create .env File: Create a file named .env in the project root to store sensitive configuration. Never commit this file to Git.

    dotenv
    # MessageBird Credentials
    MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY
    MESSAGEBIRD_ORIGINATOR=YOUR_VIRTUAL_MOBILE_NUMBER_OR_SENDER_ID
    
    # MongoDB Connection
    MONGODB_URI=mongodb://localhost:27017/sms_campaign # Or your MongoDB Atlas URI
    
    # Application Port
    PORT=8080
    
    # Basic Admin Auth (INSECURE – Development/Demo ONLY)
    ADMIN_PASSWORD=your_secret_password
    • MESSAGEBIRD_API_KEY: Obtain your Live API key from the MessageBird Dashboard (Developers → API access).
    • MESSAGEBIRD_ORIGINATOR: This is the virtual mobile number (VMN) you purchased from MessageBird (in E.164 format, e.g., +12025550156) or an approved alphanumeric sender ID (max 11 characters, check country restrictions).
    • MONGODB_URI: The connection string for your MongoDB database. Replace with your actual URI if using Atlas or a different setup. sms_campaign is the database name.
    • PORT: The port your application will run on locally.
    • ADMIN_PASSWORD: WARNING: This plain-text password is for demonstration purposes only. In a production environment, you must replace this with a secure authentication mechanism (e.g., hashed passwords with a proper login flow using libraries like Passport.js, or OAuth).
  6. Basic index.js Structure: Create an index.js file in the root directory. This will be the entry point of our application.

    javascript
    // index.js
    require('dotenv').config(); // Load environment variables first
    const express = require('express');
    const { MongoClient } = require('mongodb');
    const MessageBird = require('messagebird');
    const winston = require('winston');
    const helmet = require('helmet'); // For security headers
    const rateLimit = require('express-rate-limit'); // For rate limiting
    
    // --- Logger Setup ---
    const logger = winston.createLogger({
      level: 'info', // Default log level
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json() // Log in JSON format for easier parsing
      ),
      transports: [
        // - Write all logs with level `error` and below to `error.log`
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        // - Write all logs with level `info` and below to `combined.log`
        new winston.transports.File({ filename: 'combined.log' }),
      ],
    });
    
    // If we're not in production, log to the `console` as well
    if (process.env.NODE_ENV !== 'production') {
      logger.add(new winston.transports.Console({
        format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple() // Simple format for console
        ),
        level: 'debug', // Log more verbosely in development
      }));
    }
    
    // Initialize MessageBird SDK
    const messagebird = MessageBird(process.env.MESSAGEBIRD_API_KEY);
    
    // MongoDB Client Setup
    const mongoClient = new MongoClient(process.env.MONGODB_URI);
    let db;
    let subscribersCollection;
    
    // Initialize Express App
    const app = express();
    const port = process.env.PORT || 8080;
    
    // --- Security Middleware ---
    app.use(helmet()); // Set various security HTTP headers
    
    // --- Middleware ---
    // Parse JSON bodies for API requests (built-in Express middleware)
    app.use(express.json());
    // Parse URL-encoded bodies (for form submissions and webhooks)
    app.use(express.urlencoded({ extended: true }));
    
    // --- Database Connection ---
    async function connectDB() {
        try {
            await mongoClient.connect();
            logger.info('Successfully connected to MongoDB.');
            db = mongoClient.db(); // Default DB from URI or specify name: mongoClient.db('sms_campaign')
            subscribersCollection = db.collection('subscribers');
            // Ensure index on phone number for faster lookups
            await subscribersCollection.createIndex({ number: 1 }, { unique: true });
            logger.info('Subscribers collection ready and indexed.');
        } catch (error) {
            logger.error('Failed to connect to MongoDB', { message: error.message, stack: error.stack });
            process.exit(1); // Exit if DB connection fails
        }
    }
    
    // --- Routes ---
    // (We will add routes in the next sections)
    
    // --- Start Server ---
    async function startServer() {
        await connectDB();
        app.listen(port, () => {
            logger.info(`SMS Campaign App listening on port ${port}`);
        });
    }
    
    startServer();
    
    // Graceful shutdown
    process.on('SIGINT', async () => {
        logger.info('Shutting down server...');
        try {
            await mongoClient.close();
            logger.info('MongoDB connection closed.');
        } catch (error) {
             logger.error('Error closing MongoDB connection during shutdown', { message: error.message, stack: error.stack });
        } finally {
             process.exit(0);
        }
    });

    This sets up the basic Express server, loads environment variables, initializes the MessageBird SDK and Winston logger, establishes the MongoDB connection, applies basic security middleware, and prepares the subscribers collection with a unique index on the number field.


2. Implement SMS Webhook Handler for Incoming Messages

Create a webhook endpoint to receive and process incoming SMS messages sent by users to your MessageBird virtual number. This enables two-way SMS messaging where users can text "SUBSCRIBE" to opt-in or "STOP" to opt-out from your marketing campaigns, with automatic confirmation messages and database updates.

  1. Configure MessageBird Flow Builder:

    • Go to the "Numbers" section in your MessageBird Dashboard.
    • Find the virtual mobile number you're using as MESSAGEBIRD_ORIGINATOR.
    • Click the "Flow" icon (often looks like branching lines or </>) next to the number. If no flow exists, click "Add flow".
    • Choose "Create Custom Flow".
    • Give your flow a name (e.g., "SMS Subscription Handler").
    • Select "SMS" as the trigger. Click "Next".
    • You'll see the "SMS" step connected to your number. Click the + button below it.
    • Choose "Forward to URL" from the available steps.
    • Method: Select POST.
    • URL: This needs to be a publicly accessible URL pointing to your application's webhook endpoint.
      • For Development: Start your local server (node index.js). Then, use ngrok or localtunnel:
        • ngrok http 8080 or lt --port 8080
        • Copy the https forwarding URL provided (e.g., https://randomstring.ngrok.io or https://yoursubdomain.loca.lt).
        • Append /webhook to this URL (e.g., https://randomstring.ngrok.io/webhook). Paste this full URL into the Flow Builder step.
      • For Production: Use your deployed application's public URL (e.g., https://your-app-domain.com/webhook).
    • Click "Save".
    • Click "Publish" in the top right corner to activate the flow.
  2. Create the Webhook Route (/webhook): Add the following route handler inside index.js within the // --- Routes --- section.

    javascript
    // index.js (continued)
    
    // --- Routes ---
    
    // Helper function to send confirmation SMS (fire-and-forget)
    function sendConfirmation(recipient, message) {
        const params = {
            originator: process.env.MESSAGEBIRD_ORIGINATOR,
            recipients: [recipient],
            body: message,
        };
    
        messagebird.messages.create(params, (err, response) => {
            if (err) {
                // Log the error, potentially add to a retry queue for critical confirmations
                logger.error(`Error sending confirmation SMS to ${recipient}`, { error: err, recipient: recipient });
            } else {
                const status = response?.recipients?.items[0]?.status;
                logger.info(`Confirmation SMS sent to ${recipient}. Status: ${status || 'Unknown'}`, { recipient: recipient, status: status });
            }
        });
    }
    
    app.post('/webhook', async (req, res) => {
        const { originator, payload } = req.body;
        const text = payload ? payload.trim().toLowerCase() : ''; // Handle potentially empty payload
    
        if (!originator || !text) {
            logger.warn('Webhook received invalid data: Missing originator or payload', { requestBody: req.body });
            return res.status(400).send('Missing originator or payload');
        }
    
        logger.info(`Webhook received: From ${originator}, Text: "${text}"`, { originator, text });
    
        try {
            const existingSubscriber = await subscribersCollection.findOne({ number: originator });
    
            if (text === 'subscribe') {
                if (!existingSubscriber) {
                    // New subscriber
                    await subscribersCollection.insertOne({
                        number: originator,
                        subscribed: true,
                        subscribedAt: new Date(),
                        unsubscribedAt: null,
                    });
                    logger.info(`New subscriber added: ${originator}`, { originator });
                    sendConfirmation(originator, 'Thanks for subscribing! Text STOP to unsubscribe.');
                } else if (!existingSubscriber.subscribed) {
                    // Re-subscribing
                    await subscribersCollection.updateOne(
                        { number: originator },
                        { $set: { subscribed: true, subscribedAt: new Date(), unsubscribedAt: null } }
                    );
                    logger.info(`Subscriber re-subscribed: ${originator}`, { originator });
                    sendConfirmation(originator, 'Welcome back! You are now subscribed again. Text STOP to unsubscribe.');
                } else {
                    // Already subscribed
                    logger.info(`Subscriber already subscribed: ${originator}`, { originator });
                    sendConfirmation(originator, 'You are already subscribed. Text STOP to unsubscribe.');
                }
            } else if (text === 'stop') {
                if (existingSubscriber && existingSubscriber.subscribed) {
                    // Opting out
                    await subscribersCollection.updateOne(
                        { number: originator },
                        { $set: { subscribed: false, unsubscribedAt: new Date() } }
                    );
                    logger.info(`Subscriber opted out: ${originator}`, { originator });
                    sendConfirmation(originator, 'You have unsubscribed and will no longer receive messages.');
                } else {
                    // Not subscribed or already opted out
                    logger.info(`Unsubscribe request from non-active subscriber: ${originator}`, { originator });
                     sendConfirmation(originator, 'You are not currently subscribed.');
                }
            } else {
                // Optional: Handle unrecognized commands
                logger.info(`Unrecognized command from ${originator}: "${text}"`, { originator, text });
                 sendConfirmation(originator, 'Unrecognized command. Text SUBSCRIBE to join or STOP to leave.');
            }
    
            // Acknowledge receipt to MessageBird immediately.
            // This is crucial to prevent MessageBird from retrying the webhook delivery
            // due to timeouts. The actual confirmation SMS sending happens asynchronously
            // in the background via the sendConfirmation function.
            res.status(200).send('OK');
    
        } catch (error) {
            logger.error(`Error processing webhook for ${originator}`, {
                originator: originator,
                text: text,
                message: error.message,
                stack: error.stack,
                requestBody: req.body
            });
            // Don't send detailed error back to MessageBird, just acknowledge failure internally.
            // Sending 500 might cause MessageBird to retry, consider if 200 is safer depending on error type.
            res.status(500).send('Internal Server Error');
        }
    });
    
    // (Add Admin routes below)

    This route handles incoming POST requests from MessageBird. It parses the sender's number (originator) and the message text (payload), converts the text to lowercase, and performs database operations based on the subscribe or stop keywords. It uses the logger for detailed logging. It calls the sendConfirmation helper function (which runs asynchronously) to send confirmation messages back to the user and responds to MessageBird with a 200 OK immediately to acknowledge receipt, explaining why this is done.


3. Build the MessageBird API Integration for Bulk SMS Broadcasting

Create an admin interface to send bulk SMS marketing messages to all subscribed users. This section implements batch processing to handle large subscriber lists efficiently, with built-in error handling and delivery tracking. You'll build a password-protected web form for campaign management.

  1. Create Admin Form Route (GET /admin): This route displays the HTML form. Add this within the // --- Routes --- section in index.js.

    javascript
    // index.js (continued)
    
    app.get('/admin', async (req, res) => {
        let subscriberCount = 0;
        let countMessage = '';
        try {
             subscriberCount = await subscribersCollection.countDocuments({ subscribed: true });
             countMessage = `Current Active Subscribers: ${subscriberCount}`;
        } catch(error) {
            logger.error("Failed to get subscriber count for admin page", { message: error.message, stack: error.stack });
            countMessage = "Could not retrieve subscriber count.";
        }
    
        // Simple HTML form
        res.send(`
            <!DOCTYPE html>
            <html>
            <head><title>Send Campaign Message</title></head>
            <body>
                <h1>Send SMS Campaign Message</h1>
                <p>${countMessage}</p>
                <form action="/send" method="POST">
                    <div>
                        <label for="message">Message:</label><br>
                        <textarea id="message" name="message" rows="4" cols="50" required></textarea>
                    </div>
                    <div>
                        <label for="password">Admin Password:</label><br>
                        <input type="password" id="password" name="password" required>
                    </div>
                    <br>
                    <button type="submit">Send Message</button>
                </form>
            </body>
            </html>
        `);
    });
  2. Create Send Message Route (POST /send): This route handles the form submission, verifies the password, fetches subscribers, and sends the message in batches. Add this within the // --- Routes --- section.

    javascript
    // index.js (continued)
    
    app.post('/send', async (req, res) => {
        const { message, password } = req.body;
    
        // --- Basic Authentication (INSECURE - Replace in Production) ---
        if (password !== process.env.ADMIN_PASSWORD) {
            logger.warn('Admin send attempt failed: Invalid password.', { remoteAddress: req.ip });
            return res.status(401).send('Unauthorized: Invalid password.');
        }
    
        if (!message || message.trim() === '') {
             logger.warn('Admin send attempt failed: Empty message.', { remoteAddress: req.ip });
             return res.status(400).send('Bad Request: Message content cannot be empty.');
        }
    
        try {
            // Fetch active subscribers' numbers
            const subscribers = await subscribersCollection.find(
                { subscribed: true },
                { projection: { number: 1, _id: 0 } } // Only get the number field
            ).toArray();
    
            if (subscribers.length === 0) {
                logger.info('Admin send attempt: No active subscribers found.');
                return res.send('No active subscribers to send message to.');
            }
    
            const recipientNumbers = subscribers.map(sub => sub.number);
            logger.info(`Preparing to send message to ${recipientNumbers.length} subscribers.`, { count: recipientNumbers.length });
    
            // --- Batch Sending (MessageBird limit: 50 recipients per API call) ---
            const batchSize = 50;
            let successfulSends = 0;
            let failedBatches = 0;
            const totalBatches = Math.ceil(recipientNumbers.length / batchSize);
    
            for (let i = 0; i < recipientNumbers.length; i += batchSize) {
                const batch = recipientNumbers.slice(i, i + batchSize);
                const batchNumber = Math.floor(i / batchSize) + 1;
                const params = {
                    originator: process.env.MESSAGEBIRD_ORIGINATOR,
                    recipients: batch,
                    body: message,
                };
    
                try {
                    // Use await directly with the promise returned by the SDK (v3+)
                    const response = await messagebird.messages.create(params);
                    // Log success/partial success for the batch
                    const firstStatus = response?.recipients?.items[0]?.status;
                    logger.info(`Batch ${batchNumber}/${totalBatches} sent. Count: ${batch.length}. First status: ${firstStatus || 'Unknown'}`, { batchNumber, totalBatches, count: batch.length, firstStatus });
                    successfulSends += batch.length; // Assume success for count unless specific errors are handled per recipient
                } catch (batchError) {
                    failedBatches++;
                    logger.error(`Error sending batch ${batchNumber}/${totalBatches}. Count: ${batch.length}`, {
                        batchNumber,
                        totalBatches,
                        count: batch.length,
                        message: batchError.message,
                        errors: batchError.errors // MessageBird SDK often includes detailed errors here
                    });
                    // Decide if you want to stop or continue on batch failure
                }
            }
    
            const totalSent = successfulSends; // Adjust if tracking per-recipient status within batches
            logger.info(`Finished sending campaign. Total potentially sent: ${totalSent}. Failed batches: ${failedBatches}`, { totalSent, failedBatches });
            res.send(`Campaign message processed for ${recipientNumbers.length} subscribers. ${failedBatches > 0 ? `Encountered errors in ${failedBatches} batches. Check logs for details.` : 'All batches submitted successfully.'}`);
    
        } catch (error) {
            logger.error('Error during message sending process (fetching subscribers or general failure)', {
                message: error.message,
                stack: error.stack
            });
            res.status(500).send('Internal Server Error while sending messages.');
        }
    });
    
    // (Start Server function below)

    This route first checks the admin password (again, stressing this is INSECURE). If valid, it fetches all subscribed numbers from MongoDB. It then iterates through the numbers in batches of 50 and sends the message using await messagebird.messages.create(params);, leveraging the native Promise returned by recent SDK versions. Detailed logging using logger is included for batch success and failure.

  3. Test with curl: Test the /send endpoint using curl (replace placeholders):

    bash
    curl -X POST http://localhost:8080/send \
         -H "Content-Type: application/x-www-form-urlencoded" \
         -d "message=Hello subscribers! Special offer today only!" \
         -d "password=your_secret_password"

    (Response: A success or error message from the server)


4. Integrate MessageBird SMS API with Node.js (Summary)

You've already integrated MessageBird in the previous steps. Here's a summary of the key MessageBird API integration points:

  • Installation: npm install messagebird
  • Initialization: const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY); placed early in index.js.
  • API Key: Store securely in .env (MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY) and load via dotenv. Obtain this from your MessageBird Dashboard → Developers → API access. Ensure it's a Live key for actual sending.
  • Originator: Configure in .env (MESSAGEBIRD_ORIGINATOR=YOUR_VMN_OR_SENDER_ID). This must be a number purchased/verified in your MessageBird account or an approved alphanumeric ID.
  • Send Messages: Use await messagebird.messages.create(params) (for Promise-based handling) or messagebird.messages.create(params, callback) (for callback style). The params object must include originator, recipients (an array of numbers), and body.
  • Receive Messages (Webhook): Requires configuring a Flow in the MessageBird Dashboard ("Numbers" section) to forward incoming SMS for your MESSAGEBIRD_ORIGINATOR number via POST to your application's /webhook URL. The webhook handler parses req.body.originator and req.body.payload.

5. Implement Error Handling, Logging, and SMS Retry Mechanisms

Production SMS applications need robust error handling and logging.

  1. Logging Strategy: Integrate Winston for structured JSON logging to files (error.log, combined.log) and optionally to the console in development.

    • Configuration: Done near the top of index.js.
    • Usage: Replace all console.* calls with logger.info, logger.warn, and logger.error. Log errors with message and stack trace where applicable. Include contextual information (like originator, batchNumber) in log calls.
  2. Error Handling:

    • Explicit try...catch: Wrap database operations and MessageBird API calls in try...catch blocks.
    • Input Validation: Perform basic checks for required fields (originator, payload, message, password), returning appropriate HTTP status codes (400, 401).
    • Consistent Responses: Send meaningful HTTP status codes (200, 400, 401, 500). Log internal error details but don't expose them in production responses. The webhook responds 200 OK quickly to MessageBird.
  3. Retry Mechanisms (for Sending): MessageBird API calls might fail temporarily. Implement retries only for critical outgoing messages (like the main campaign send), not usually for webhook processing (to avoid duplicate actions).

    • Simple Manual Retry Example (Illustrative): The following demonstrates a basic retry loop within the /send route's batch processing. For production, consider a more robust library like async-retry.

      javascript
      // Inside app.post('/send', ...) batch loop (Illustrative – Use a library for robustness)
      
      const MAX_RETRIES = 3;
      const RETRY_DELAY_MS = 1000; // Base delay
      
      for (let i = 0; i < recipientNumbers.length; i += batchSize) {
          const batch = recipientNumbers.slice(i, i + batchSize);
          const batchNumber = Math.floor(i / batchSize) + 1;
          const params = { /* ... */ };
          let attempt = 0;
          let batchSuccess = false;
      
          while (attempt < MAX_RETRIES && !batchSuccess) {
              attempt++;
              try {
                  const response = await messagebird.messages.create(params);
                  batchSuccess = true; // Assume success if no exception
                  logger.info(`Batch ${batchNumber}/${totalBatches}: Send attempt ${attempt} successful. Count: ${batch.length}.`, { batchNumber, totalBatches, attempt, count: batch.length });
                  successfulSends += batch.length;
              } catch (batchError) {
                  logger.warn(`Batch ${batchNumber}/${totalBatches}: Send attempt ${attempt} failed.`, {
                      batchNumber, totalBatches, attempt, count: batch.length, message: batchError.message, errors: batchError.errors
                  });
                  if (attempt >= MAX_RETRIES) {
                      logger.error(`Batch ${batchNumber}/${totalBatches}: Send failed after ${MAX_RETRIES} attempts. Giving up on this batch.`, {
                           batchNumber, totalBatches, maxAttempts: MAX_RETRIES, count: batch.length, message: batchError.message, errors: batchError.errors
                      });
                      failedBatches++;
                      // Optionally: Log the specific numbers in the failed batch for manual follow-up
                  } else {
                      // Basic exponential backoff (1s, 2s, 4s…)
                      const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1);
                      logger.info(`Batch ${batchNumber}/${totalBatches}: Retrying attempt ${attempt+1} after ${delay}ms…`);
                      await new Promise(resolve => setTimeout(resolve, delay));
                  }
              }
          }
      } // End of batch loop
    • Note: Implement retry logic carefully. Retrying non-idempotent operations can cause issues. Sending SMS is generally safe to retry if the initial attempt failed definitively.


6. Design MongoDB Schema for SMS Subscriber Management

Use MongoDB with a simple schema defined by the documents you insert for SMS subscription tracking.

  • Collection: subscribers

  • Document Structure:

    json
    {
      "_id": "ObjectId(...)",
      "number": "+12025550156",
      "subscribed": true,
      "subscribedAt": "ISODate(...)",
      "unsubscribedAt": null
    }
    • _id: Automatically generated by MongoDB.
    • number: Phone number in E.164 format (Indexed, Unique).
    • subscribed: Boolean indicating current subscription status.
    • subscribedAt: Timestamp of the last subscription action.
    • unsubscribedAt: Timestamp of the last unsubscription action (null if subscribed).
  • Data Access: Handle via the mongodb driver's MongoClient. Set up mongoClient, db, and subscribersCollection in index.js.

  • Key Operations:

    • subscribersCollection.findOne({ number: ... }) (Used in webhook)
    • subscribersCollection.insertOne({ ... }) (Used for new subscribers)
    • subscribersCollection.updateOne({ number: ... }, { $set: { ... } }) (Used for re-subscribing or unsubscribing)
    • subscribersCollection.find({ subscribed: true }, { projection: { ... } }).toArray() (Used in /send)
    • subscribersCollection.countDocuments({ subscribed: true }) (Used in /admin)
  • Indexing: A unique index on the number field (subscribersCollection.createIndex({ number: 1 }, { unique: true })) is crucial for performance and data integrity, ensuring you don't store duplicate numbers. Create this on application start.

  • Migrations: For this simple schema, migrations aren't strictly necessary. For evolving schemas, tools like migrate-mongo can manage database changes systematically.

  • Sample Data (Manual Insertion via mongosh):

    javascript
    // Connect using mongosh: mongosh "YOUR_MONGODB_URI"
    // use sms_campaign; // Select the database
    
    db.subscribers.insertOne({
      number: "+12025550199", // Replace with a test number
      subscribed: true,
      subscribedAt: new Date(),
      unsubscribedAt: null
    });

7. Add Security Features for SMS Marketing Applications

Security is paramount when handling user data and sending SMS messages.

  1. Input Validation:
    • Webhook: Check that originator and payload exist. Trim payload. Consider adding E.164 format validation for originator.
    • Send Route: Ensure message is not empty. Validate password. Consider adding length limits or sanitization to message.
  2. Rate Limiting: Protect against brute-force attacks and abuse. Add express-rate-limit.
    • Implementation: Add configuration in index.js before routes.

      javascript
      // index.js (Add near other middleware)
      
      // Rate limiting for the webhook (adjust limits as needed)
      const webhookLimiter = rateLimit({
          windowMs: 15 * 60 * 1000, // 15 minutes
          max: 100, // Limit each IP to 100 requests per windowMs
          message: 'Too many requests from this IP, please try again after 15 minutes',
          standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
          legacyHeaders: false, // Disable the `X-RateLimit-*` headers
      });
      app.use('/webhook', webhookLimiter);
      
      // Stricter rate limiting for the admin send endpoint
      const adminLimiter = rateLimit({
          windowMs: 60 * 60 * 1000, // 1 hour
          max: 5, // Limit each IP to 5 send attempts per hour
          message: 'Too many send attempts from this IP, please try again later.',
          standardHeaders: true,
          legacyHeaders: false,
      });
      app.use('/send', adminLimiter);
      app.use('/admin', adminLimiter); // Also limit access to the admin form page
  3. Security Headers: Protect against common web vulnerabilities (XSS, clickjacking, etc.). Add helmet.
    • Implementation: app.use(helmet()); added early in index.js.
  4. Authentication:
    • Admin: The current plain-text password check in /send is highly insecure. Replace this immediately in a production environment with a proper authentication system (e.g., Passport.js with username/hashed password stored securely, OAuth, JWT).
    • Webhook: MessageBird webhooks don't have built-in authentication beyond the obscurity of the URL. For higher security, consider implementing MessageBird Webhook Signature Verification. This involves comparing a signature header sent by MessageBird with a signature you compute using your webhook signing key and the request body.
  5. Environment Variables: Keep sensitive data (API keys, DB URIs, passwords) out of the codebase using .env and dotenv. Ensure the .env file is never committed to version control (.gitignore).
  6. Logging: Avoid logging excessively sensitive information (like full message bodies if they contain PII, or passwords). Review your current logging based on data sensitivity.
  7. Database Security:
    • Use strong, unique credentials for your MongoDB instance.
    • Configure network access rules (firewalls) to only allow connections from your application server's IP address.
    • Consider encryption at rest if required by compliance standards.
  8. Dependency Management: Keep dependencies up-to-date (npm audit, npm update) to patch known vulnerabilities. Use tools like Snyk or Dependabot for automated scanning.

8. Deploy Your SMS Campaign Application to Production

Deploy your Node.js SMS marketing application following these steps.

  1. Choose a Hosting Provider: Options include:
    • PaaS (Platform as a Service): Heroku, Render, Fly.io, Google App Engine, AWS Elastic Beanstalk. Often simpler to manage.
    • IaaS (Infrastructure as a Service): AWS EC2, Google Compute Engine, DigitalOcean Droplets. More control, more management overhead.
    • Serverless: AWS Lambda, Google Cloud Functions, Vercel Serverless Functions. Good for event-driven workloads like webhooks, potentially cost-effective.
  2. Prepare for Production:
    • Environment Variables: Set production values for MESSAGEBIRD_API_KEY, MONGODB_URI, PORT, and crucially, replace the insecure ADMIN_PASSWORD with your secure authentication mechanism's configuration. Do not use a .env file in production; use the hosting provider's mechanism for setting environment variables.
    • Set NODE_ENV=production: This environment variable enables optimizations in Express and other libraries and disables development-only features (like verbose console logging in your setup).
    • Build Step (If using TypeScript/Babel): Transpile your code to JavaScript if necessary.
  3. Process Manager: Use a process manager like PM2 or Nodemon (in production mode) to:
    • Keep the application running continuously.
    • Restart automatically if it crashes.
    • Manage clustering (running multiple instances) for better performance and availability.
    • Example (using PM2):
      bash
      npm install pm2 -g # Install globally
      pm2 start index.js --name messagebird-sms-app -i max # Start clustered
      pm2 startup # Configure PM2 to start on system boot
      pm2 save # Save current process list
  4. Reverse Proxy (Recommended): Use Nginx or Apache in front of your Node.js application to:
    • Handle SSL/TLS termination (HTTPS).
    • Serve static assets efficiently.
    • Perform load balancing if running multiple instances.
    • Provide basic caching or security filtering.
  5. Database: Ensure your production MongoDB instance is properly secured, backed up, and scaled for your expected load. Use the production connection string.
  6. Webhook URL: Update the MessageBird Flow Builder ("Forward to URL" step) to use your production application's public URL (e.g., https://your-app-domain.com/webhook). Ensure this URL is accessible from the internet.
  7. Monitoring & Logging:
    • Set up monitoring tools (e.g., Prometheus/Grafana, Datadog, New Relic) to track application performance (CPU, memory, response times) and errors.
    • Aggregate logs from your application (e.g., using the hosting provider's logging service, or tools like ELK Stack, Splunk, Logtail) for easier analysis and troubleshooting. Your Winston file transport is suitable for basic logging, but centralized logging is better for production.

9. SMS Marketing Compliance and Next Steps

You've successfully built a foundational SMS marketing campaign application using Node.js, Express, MongoDB, and MessageBird API. Users can subscribe and unsubscribe via SMS, and an administrator can broadcast messages to active subscribers.

Key achievements:

  • Project setup with essential dependencies.
  • Webhook implementation for handling incoming SUBSCRIBE and STOP messages.
  • Database integration with MongoDB for subscriber management.
  • Basic admin interface for sending broadcast messages.
  • Integration of logging, basic error handling, and security considerations.

Next Steps:

  • Secure Admin Authentication: Replace the insecure password check with a robust authentication system (Passport.js, OAuth, etc.).
  • Improve Admin Interface: Build a proper web UI (using React, Vue, Angular, or server-side templating like EJS) instead of the basic HTML form. Include features like viewing subscribers, message history, etc.
  • Webhook Security: Implement MessageBird webhook signature verification for enhanced security.
  • Advanced SMS Marketing Features:
    • Personalization: Use placeholders in messages (e.g., Hello {name}, …) for targeted campaigns.
    • Scheduling: Allow admins to schedule SMS campaigns for optimal delivery times.
    • Analytics: Track delivery rates, click-through rates (via URL shorteners), and unsubscribe rates.
    • A/B Testing: Test different message variations to optimize campaign performance.
  • Scalability: Implement clustering (PM2) and optimize database queries for handling larger subscriber lists efficiently.
  • More Robust Retries: Use libraries like async-retry or implement a background job queue (e.g., BullMQ, Kue) for handling message sending failures more reliably.
  • Testing: Add unit tests (Jest, Mocha) and integration tests (Supertest) to ensure code quality and prevent regressions.
  • Advanced Topics: Explore SMS delivery status tracking and OTP/2FA implementation for enhanced functionality.
  • TCPA Compliance for SMS Marketing (United States): Ensure full compliance with the Telephone Consumer Protection Act (TCPA) for SMS marketing in the US:
    • Express Written Consent: Obtain prior express written consent before sending promotional or automated SMS messages. Consumers must take a clear affirmative action to opt in – consent cannot be implied.
    • Time Restrictions: Do not send messages before 8:00 AM or after 9:00 PM in the recipient's local time zone (TCPA requirement).
    • Opt-Out Processing: Process opt-out requests within 24 hours. Under new TCPA rules effective April 11, 2025, honor revocations within 10 business days maximum through various methods including emails, voicemails, and informal messages like "Leave me alone".
    • One-to-One Consent (Effective January 26, 2026): The new one-to-one consent rule requires marketers to obtain specific consent from each consumer to receive telemarketing calls and texts.
    • Required Disclosures: Provide clear disclosures explicitly stating the recipient agrees to receive automated marketing messages from your specific company.
    • Penalties: TCPA violations can result in $500 to $1,500 per violation, plus potential class-action lawsuits.
    • Message Type Distinction: Transactional messages (order confirmations, shipping updates, etc.) do not require express written consent but must be strictly informational. Promotional messages always require express written consent.
  • GDPR Compliance (European Union): If targeting EU residents, ensure compliance with data protection requirements including explicit consent, data access rights, and the right to be forgotten.
  • Additional Compliance Considerations: Research and comply with local SMS marketing regulations in your target markets (e.g., CASL in Canada, various regulations in other countries).

Frequently Asked Questions

How to set up MessageBird webhook for SMS campaign?

Configure a Flow in your MessageBird Dashboard under the "Numbers" section. Select "Create Custom Flow," choose "SMS" as the trigger, and then "Forward to URL" as the next step. Set the method to POST and enter your application's public webhook URL (e.g., 'https://your-app.com/webhook'). For development, use ngrok or localtunnel to create a public URL and append '/webhook'.

What is the role of MessageBird in this SMS campaign application?

MessageBird is the communication platform providing the APIs for sending and receiving SMS messages, as well as managing virtual mobile numbers (VMNs). It acts as the bridge between your application and the mobile network, allowing you to send bulk messages and receive user replies via webhooks.

Why does the webhook respond with 200 OK immediately?

The immediate '200 OK' response to MessageBird's webhook POST request acknowledges receipt and prevents MessageBird from retrying the delivery due to potential timeouts. The actual SMS confirmations to users are handled asynchronously afterwards by a separate function, ensuring MessageBird doesn't retry the webhook unnecessarily.

When should I use a Live API key with MessageBird?

Always use a Live API key for production environments where you're sending real SMS messages. Test API keys are strictly for development and testing and shouldn't be used for actual campaigns due to rate limits and potential data inconsistencies.

Can I test the send functionality without sending real messages?

For initial development, you can test the send functionality by logging the outgoing messages instead of actually sending them via the MessageBird API. For more realistic testing, create test accounts within MessageBird with dedicated virtual numbers to avoid sending unexpected SMS to real users during development.

How to send SMS messages in batches with MessageBird?

The MessageBird API has a limit of 50 recipients per API call. The application code iterates through the subscriber list and sends messages in batches of 50 using a loop and array slicing to adhere to this limit. It's crucial to implement this batching to avoid errors from MessageBird.

What is the database schema for storing subscribers?

The MongoDB database uses the `subscribers` collection. Each document stores the subscriber's `number` (E.164 format), `subscribed` status (boolean), `subscribedAt` timestamp, and `unsubscribedAt` timestamp. A unique index on the `number` field ensures no duplicate numbers are stored.

Why is the provided admin password setup insecure?

Storing the admin password in plain text in the `.env` file is extremely insecure. For production, replace this with a proper authentication system like Passport.js, OAuth 2.0, or JWT, which should store password hashes securely and implement robust login flows.

How to improve the security of the webhook endpoint?

Implement MessageBird Webhook Signature Verification to ensure that incoming webhook requests are genuinely from MessageBird and not malicious actors. This involves calculating a signature hash and comparing it to the one provided in the 'MessageBird-Signature' header.

What are the recommended deployment options for this application?

Recommended deployment options include PaaS solutions like Heroku, Render, or Fly.io for ease of management, or IaaS like AWS EC2 or Google Compute Engine for more control. Serverless functions might be suitable for specific components but may not be ideal for the whole application due to webhook and long-running process needs.

How to implement retry mechanisms for sending SMS messages?

Use a library like `async-retry` or a job queue system to handle temporary failures when sending messages. This ensures better reliability for your SMS campaign and reduces the chance of messages not reaching subscribers.

What logging strategy is used in the application?

The application uses Winston for logging, configured to log in JSON format for easier parsing and analysis. Logs are written to 'error.log' for error messages and 'combined.log' for other log levels. In development, logs are also outputted to the console.

What are the next steps to enhance this application?

Implement secure admin authentication, improve the admin UI for better user experience, implement MessageBird webhook signature verification, add advanced messaging features like personalization and scheduling, implement robust retry mechanisms, and thorough testing are important next steps to consider.