code examples

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

MessageBird SMS Marketing Campaign Tutorial: Node.js, Express & MongoDB Implementation

Learn how to build compliant SMS marketing campaigns with MessageBird API, Node.js, and Express. Complete guide covering subscriber opt-in/opt-out, broadcast messaging, MongoDB integration, and TCPA/GDPR compliance.

Build SMS Marketing Campaigns with MessageBird, Node.js, Express & MongoDB

⚠️ Important: This guide covers Express.js and MongoDB. The filename references Next.js and Supabase but the implementation uses Express and MongoDB stack.

🔒 Security Notice: This guide includes authentication placeholders. Implement proper authentication before deploying to production. Never expose admin endpoints without authentication.

<!-- GAP: Missing specific compliance requirements for different jurisdictions (Type: Critical) --> <!-- EXPAND: Could benefit from compliance checklist table (Type: Enhancement) -->

⚖️ Compliance Warning: Ensure compliance with local SMS marketing regulations (TCPA in the US, GDPR in the EU, CTIA guidelines). This guide provides technical implementation, not legal advice. Consult legal counsel for compliance requirements in your jurisdiction.

Build a production-ready SMS marketing campaign application using Node.js, Express, and the MessageBird API. This comprehensive tutorial shows you how to implement compliant SMS marketing with subscriber opt-in/opt-out management, broadcast messaging, and MongoDB database integration.

Learn how to create an SMS subscription system where customers can text "SUBSCRIBE" or "STOP" to manage their preferences, while administrators send targeted marketing campaigns through a web interface. This guide covers MessageBird API integration, webhook handling for inbound SMS, database design for subscriber management, and compliance with TCPA and GDPR regulations.

<!-- DEPTH: Feature list lacks detail on implementation complexity and limitations (Priority: Medium) -->

Key Features:

  • User opt-in via SUBSCRIBE keyword
  • User opt-out via STOP keyword
  • Confirmation messages for subscription changes
  • Admin interface to send broadcast messages
  • Database storage for subscribers
<!-- EXPAND: Could add comparison of Express+MongoDB vs Next.js+Supabase approaches (Type: Enhancement) -->

Technology Stack:

  • Node.js: JavaScript runtime environment (v14+ recommended)
  • Express: Web application framework for Node.js (v4.18+)
  • MessageBird: Communications Platform as a Service (CPaaS) for sending/receiving SMS
  • MongoDB: NoSQL database for storing subscriber information (v4.4+)
  • dotenv: Module to load environment variables from a .env file

Note: Development tools like localtunnel help test webhooks locally but aren't part of the production stack.

System Architecture:

mermaid
graph LR
    UserMobile -- SMS (SUBSCRIBE/STOP) --> MessageBirdVMN[MessageBird Virtual Number]
    MessageBirdVMN -- Webhook --> ExpressApp[Node.js/Express App]
    ExpressApp -- DB Query/Update --> MongoDB[MongoDB Database]
    ExpressApp -- Send Confirmation SMS --> MessageBirdAPI[MessageBird API]
    MessageBirdAPI -- SMS --> UserMobile

    AdminBrowser -- HTTP Request --> ExpressApp
    ExpressApp -- DB Query (Get Subscribers) --> MongoDB
    ExpressApp -- Send Broadcast SMS --> MessageBirdAPI
    MessageBirdAPI -- SMS --> ActiveSubscribers[Subscribed User Mobiles]
<!-- GAP: Missing cost estimation and pricing considerations (Type: Substantive) -->

Prerequisites:

  • Node.js and npm: Install v14 or later from nodejs.org
  • Git: For cloning repositories (optional)
  • MessageBird Account: Sign up for free at messagebird.com
  • MongoDB Instance: A running MongoDB server v4.4+ (local or cloud-based like MongoDB Atlas). Obtain the connection string (URI)
  • A Mobile Phone: For testing SMS sending and receiving

1. Set Up Your Node.js SMS Marketing Project

Initialize the project, install dependencies, and set up the basic structure.

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

    bash
    mkdir node-messagebird-campaign
    cd node-messagebird-campaign
  2. Initialize Node.js Project: Create a package.json file.

    bash
    npm init -y
  3. Install Dependencies: Install Express for the web server, the MessageBird SDK, the MongoDB driver, and dotenv for managing environment variables.

    bash
    npm install express messagebird mongodb dotenv
  4. Create Project Files: Create the main application file, environment file example, and a .gitignore file.

    bash
    touch index.js .env .env.example .gitignore
  5. Configure .gitignore: Prevent sensitive files and generated folders from being committed to version control. Add the following to .gitignore:

    text
    # Environment variables
    .env
    
    # Node dependencies
    node_modules/
    
    # Log files
    *.log
  6. Set Up Environment Variables (.env.example and .env): Define the structure in .env.example. This file can be committed safely.

    ini
    # .env.example
    # MessageBird Credentials
    MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_API_KEY
    MESSAGEBIRD_ORIGINATOR=YOUR_MESSAGEBIRD_VIRTUAL_NUMBER_OR_SENDER_ID
    
    # MongoDB Connection
    MONGODB_URI=YOUR_MONGODB_CONNECTION_STRING
    
    # Application Port
    PORT=8080

    Now, create your actual .env file by copying .env.example. Never commit .env to Git. Fill it with your actual credentials (obtain these in the next steps).

    bash
    cp .env.example .env
    • MESSAGEBIRD_API_KEY: Your Live API key from the MessageBird Dashboard
    • MESSAGEBIRD_ORIGINATOR: The virtual mobile number (VMN) you purchase from MessageBird (in E.164 format, e.g., +12025550135) or an alphanumeric Sender ID (if supported in your target countries). You'll obtain this soon.
    • MONGODB_URI: Your MongoDB connection string (e.g., mongodb://localhost:27017/sms_campaign)
    • PORT: The port your application will run on (defaults to 8080)
<!-- DEPTH: Code examples lack explanation of security implications (Priority: High) -->
  1. Create Basic Express Server (index.js): Set up the initial Express application structure.

    javascript
    // index.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    const { MongoClient } = require('mongodb');
    const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY);
    
    const app = express();
    const port = process.env.PORT || 8080;
    
    // Parse URL-encoded bodies (as sent by HTML forms)
    app.use(express.urlencoded({ extended: true }));
    // Parse JSON bodies (as sent by API clients or MessageBird webhooks)
    app.use(express.json());
    
    let db; // Database connection variable
    const SUBSCRIBERS_COLLECTION = 'subscribers';
    
    async function connectDB() {
      try {
        const client = new MongoClient(process.env.MONGODB_URI);
        await client.connect();
        db = client.db(); // Uses database name from URI or defaults to 'test'
        console.log("Successfully connected to MongoDB.");
    
        // Ensure the unique index on 'number' exists
        await db.collection(SUBSCRIBERS_COLLECTION).createIndex({ number: 1 }, { unique: true });
        console.log(`Index on 'number' in '${SUBSCRIBERS_COLLECTION}' collection ensured.`);
    
      } catch (err) {
        console.error("Failed to connect to MongoDB or ensure index", err);
        process.exit(1); // Exit if DB connection or initial setup fails
      }
    }
    
    // --- Routes Go Here ---
    
    // Start the server after connecting to the DB
    connectDB().then(() => {
      app.listen(port, () => {
        console.log(`Server listening at http://localhost:${port}`);
      });
    });

You've now set up the basic project structure, dependencies, environment variable handling, a simple Express server, and initial database connection logic including index creation.

<!-- GAP: Missing MessageBird pricing information and cost estimation (Type: Substantive) -->

2. Configure MessageBird API for SMS Marketing

Set up your MessageBird account to send SMS campaigns and receive subscriber responses through webhooks.

  1. Obtain MessageBird API Key:
    • Log in to your MessageBird Dashboard
    • Navigate to Developers > API access
    • If you don't have a LIVE API key, click "Add access key"
    • Copy the Live API Key
    • Paste this key into your .env file as MESSAGEBIRD_API_KEY
<!-- DEPTH: VMN purchase process lacks detail on number capabilities and regional differences (Priority: Medium) -->
  1. Purchase a Virtual Mobile Number (VMN): You need a dedicated number to receive incoming messages (SUBSCRIBE/STOP).

    • In the MessageBird Dashboard, go to Numbers
    • Click Buy a number
    • Select your desired country
    • Ensure the SMS capability is checked
    • Choose a number from the list
    • Select the billing duration and confirm the purchase
    • Copy the purchased number (in E.164 format, e.g., +12025550135)
    • Paste this number into your .env file as MESSAGEBIRD_ORIGINATOR. This will also be the sender ID for outgoing messages.

    Note: Some countries support alphanumeric sender IDs (e.g., "MyBrand"). If using one, ensure it's compliant and supported. For receiving messages, you must use a number.

<!-- EXPAND: Could add alternative webhook testing solutions comparison (Type: Enhancement) -->
  1. Set Up Localtunnel for Webhook Testing: MessageBird needs a publicly accessible URL to send incoming message notifications (webhooks). During development, localtunnel exposes your local server.
    • Install localtunnel globally:
      bash
      npm install -g localtunnel
    • Run your Node.js application (ensure it runs on the port specified in .env, e.g., 8080):
      bash
      node index.js
    • Open a new terminal window/tab in the same project directory
    • Start localtunnel, pointing it to your local application port:
      bash
      lt --port 8080
    • localtunnel will output a public URL (e.g., https://your-subdomain.loca.lt). Copy this URL. You'll need it in the next step. Keep this tunnel running during testing. Note: This URL changes every time you restart localtunnel unless you use advanced options.
<!-- DEPTH: Flow Builder configuration lacks screenshots and troubleshooting steps (Priority: Medium) -->
  1. Configure MessageBird Flow Builder for Incoming Messages: Connect your purchased VMN to your application's webhook endpoint.

    • Go back to the Numbers section in the MessageBird Dashboard
    • Find the number you purchased and click the Flow icon (looks like connected dots) next to it, or click "Attach flow"
    • Select Create Custom Flow
    • Give your flow a descriptive name (e.g., "SMS Campaign Handler")
    • Choose SMS as the trigger. Click Next
    • The flow editor will open with the "SMS" step already added. Click the + button below it
    • Choose Forward to URL as the next step
    • Set the Method to POST
    • In the URL field, paste the localtunnel URL you copied, and append /webhook (e.g., https://your-subdomain.loca.lt/webhook). This is the route you'll define in Express to handle incoming messages
    • Click Save
    • Click Publish (top right) to activate the flow

    Your flow should look like: SMSForward to URL. Now, when someone sends an SMS to your VMN, MessageBird forwards the details to your application's /webhook endpoint via a POST request.

<!-- GAP: Missing webhook payload structure documentation (Type: Critical) -->

3. Build SMS Subscriber Opt-In/Opt-Out Management

Implement the database layer and webhook endpoint to handle SMS subscription commands like SUBSCRIBE and STOP.

  1. Add Database Helper Functions (index.js): Add functions to interact with the subscribers collection in MongoDB.

    javascript
    // index.js (Add these functions after connectDB definition)
    
    /**
     * Find a subscriber by phone number.
     * @param {string} number - The subscriber's phone number in E.164 format.
     * @returns {Promise<object|null>} - The subscriber document or null if not found.
     */
    async function findSubscriber(number) {
      if (!db) throw new Error("Database not connected");
      return db.collection(SUBSCRIBERS_COLLECTION).findOne({ number: number });
    }
    
    /**
     * Add a new subscriber or update their status to subscribed.
     * @param {string} number - The subscriber's phone number.
     * @returns {Promise<object>} - The result of the update operation.
     */
    async function addOrUpdateSubscriber(number) {
      if (!db) throw new Error("Database not connected");
      return db.collection(SUBSCRIBERS_COLLECTION).updateOne(
        { number: number },
        { $set: { number: number, subscribed: true, updatedAt: new Date() } },
        { upsert: true } // Creates the document if it doesn't exist
      );
    }
    
    /**
     * Update a subscriber's status (subscribed: true/false).
     * @param {string} number - The subscriber's phone number.
     * @param {boolean} status - The new subscription status (true/false).
     * @returns {Promise<object>} - The result of the update operation.
     */
    async function updateSubscriberStatus(number, status) {
      if (!db) throw new Error("Database not connected");
      return db.collection(SUBSCRIBERS_COLLECTION).updateOne(
        { number: number },
        { $set: { subscribed: status, updatedAt: new Date() } }
      );
    }
    
    /**
     * Retrieve all active subscribers.
     * @returns {Promise<Array<object>>} - An array of active subscriber documents.
     */
    async function getActiveSubscribers() {
      if (!db) throw new Error("Database not connected");
      return db.collection(SUBSCRIBERS_COLLECTION).find({ subscribed: true }).toArray();
    }
    
    /**
     * Send SMS using MessageBird SDK (async/await).
     * @param {string} recipient - The recipient phone number.
     * @param {string} body - The message body.
     * @returns {Promise<object>} - The MessageBird API response object.
     */
    async function sendSms(recipient, body) {
        const params = {
            originator: process.env.MESSAGEBIRD_ORIGINATOR,
            recipients: [recipient],
            body: body,
        };
    
        try {
            console.log(`Sending SMS to ${recipient}: "${body}"`);
            const response = await messagebird.messages.create(params);
            console.log(`MessageBird response for ${recipient}: Status ${response.recipients.items[0].status}`);
            return response;
        } catch (error) {
            console.error(`MessageBird API Error sending to ${recipient}:`, error);
            throw error;
        }
    }
<!-- DEPTH: Webhook handler lacks detailed error handling scenarios (Priority: High) --> <!-- GAP: Missing webhook security validation (signature verification) (Type: Critical) -->
  1. Create Webhook Handler (index.js): Create the /webhook route to process incoming SMS messages from MessageBird.

    javascript
    // index.js (Add this route definition before connectDB().then(...))
    
    app.post('/webhook', async (req, res) => {
      // Extract necessary data from MessageBird webhook payload
      const { originator, payload } = req.body;
      if (!originator || payload === undefined) {
        console.warn('Received incomplete webhook payload:', req.body);
        return res.sendStatus(400); // Bad Request
      }
    
      const command = payload.trim().toLowerCase();
      console.log(`Received command '${command}' from ${originator}`);
    
      try {
        // Find existing subscriber
        const subscriber = await findSubscriber(originator);
    
        // Process commands: SUBSCRIBE and STOP
        if (command === 'subscribe') {
          if (subscriber && subscriber.subscribed) {
            console.log(`${originator} is already subscribed.`);
            // Optional: Send "You're already subscribed" message
            // await sendSms(originator, "You are already subscribed!");
          } else {
            await addOrUpdateSubscriber(originator);
            console.log(`Subscribed ${originator}`);
            await sendSms(originator, 'Thanks for subscribing! Text STOP anytime to unsubscribe.');
          }
        } else if (command === 'stop') {
          if (!subscriber || !subscriber.subscribed) {
            console.log(`${originator} is not currently subscribed.`);
            // Optional: Send "You weren't subscribed" message
            // await sendSms(originator, "You were not subscribed to this list.");
          } else {
            await updateSubscriberStatus(originator, false);
            console.log(`Unsubscribed ${originator}`);
            await sendSms(originator, 'You have been unsubscribed. Text SUBSCRIBE to join again.');
          }
        } else {
          // Handle unknown commands
          console.log(`Ignoring unknown command '${command}' from ${originator}`);
          // Optional: Send a help message
          // await sendSms(originator, 'Unknown command. Text SUBSCRIBE or STOP.');
        }
    
        // Acknowledge receipt to MessageBird
        res.sendStatus(200);
    
      } catch (error) {
        console.error(`Error processing webhook for ${originator}:`, error);
        res.sendStatus(500); // Internal Server Error
      }
    });

    This route extracts the sender's number (originator) and message content (payload), converts the command to lowercase, checks the database, updates the subscriber's status accordingly, and sends a confirmation SMS. It then sends a 200 OK status back to MessageBird to acknowledge receipt.

<!-- GAP: Missing authentication implementation examples (Type: Critical) -->

4. Create Admin Interface for SMS Broadcast Campaigns

🔒 Security Critical: Implement authentication before deploying these routes to production.

Build a web-based admin panel for sending SMS broadcast messages to your subscriber list.

<!-- EXPAND: Could benefit from UI/UX best practices for admin interfaces (Type: Enhancement) -->
  1. Add Admin Form Route (index.js): Add a GET route to display the HTML form. Secure this route before production deployment.

    javascript
    // index.js (Add this route definition)
    // ⚠️ SECURITY WARNING: Implement authentication middleware before production
    
    app.get('/', async (req, res) => { // ⚠️ SECURE THIS ROUTE
      try {
        const activeSubscribers = await getActiveSubscribers();
        const subscriberCount = activeSubscribers.length;
    
        // Simple HTML form – use a template engine (EJS, Handlebars) for production
        res.send(`
          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>SMS Campaign Admin</title>
            <style>
              body { font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto; }
              textarea { width: 100%; min-height: 100px; margin-bottom: 10px; box-sizing: border-box; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
              button { padding: 10px 20px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px; }
              button:hover { background-color: #0056b3; }
              .status { margin-top: 20px; padding: 10px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; }
              pre { background-color: #eee; padding: 10px; border: 1px solid #ccc; white-space: pre-wrap; word-wrap: break-word; border-radius: 4px; }
              label { display: block; margin-bottom: 5px; font-weight: bold; }
            </style>
          </head>
          <body>
            <h1>Send Broadcast SMS</h1>
            <p>Current active subscribers: <strong>${subscriberCount}</strong></p>
            <form action="/send" method="POST">
              <div>
                <label for="message">Message:</label>
                <textarea id="message" name="message" required></textarea>
              </div>
              <button type="submit">Send to Subscribers</button>
            </form>
            <div id="status" class="status" style="display: none;"></div>
            <script>
              const form = document.querySelector('form');
              const statusDiv = document.getElementById('status');
              if (form && statusDiv) {
                  form.addEventListener('submit', function(event) {
                      statusDiv.style.display = 'block';
                      statusDiv.textContent = 'Sending... please wait.';
                  });
              }
            </script>
          </body>
          </html>
        `);
      } catch (error) {
        console.error("Error loading admin page:", error);
        res.status(500).send("Error loading admin page.");
      }
    });
<!-- DEPTH: Broadcast implementation lacks delivery status tracking (Priority: High) --> <!-- GAP: Missing message queuing system for large broadcasts (Type: Substantive) -->
  1. Add Broadcast Sending Route (index.js): Add a POST route to handle the form submission and send the broadcast. Secure this route before production deployment.

    javascript
    // index.js (Add this route definition)
    // ⚠️ SECURITY WARNING: Implement authentication middleware before production
    
    app.post('/send', async (req, res) => { // ⚠️ SECURE THIS ROUTE
      const messageBody = req.body.message;
      if (!messageBody || messageBody.trim() === '') {
        return res.status(400).send("Message body cannot be empty.");
      }
    
      try {
        const subscribers = await getActiveSubscribers();
        if (subscribers.length === 0) {
          return res.send(`
            <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Broadcast Result</title><style>body{font-family:sans-serif;padding:20px;}</style></head><body>
            <h1>Broadcast Result</h1>
            <p>No active subscribers to send to.</p>
            <a href="/">Back to Admin</a>
            </body></html>
          `);
        }
    
        const recipients = subscribers.map(sub => sub.number);
        console.log(`Attempting to send broadcast to ${recipients.length} recipients.`);
    
        // MessageBird API allows up to 50 recipients per request
        // Batch the requests if you have more than 50 subscribers
        const batchSize = 50;
        let successfulSends = 0;
        let failedSends = 0;
        const totalRecipients = recipients.length;
    
        for (let i = 0; i < totalRecipients; i += batchSize) {
          const batch = recipients.slice(i, i + batchSize);
          const params = {
            originator: process.env.MESSAGEBIRD_ORIGINATOR,
            recipients: batch,
            body: messageBody,
          };
    
          try {
              console.log(`Sending batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(totalRecipients / batchSize)} (${batch.length} recipients)...`);
              const response = await messagebird.messages.create(params);
              const sentInBatch = response.recipients.totalSentCount || batch.length;
              successfulSends += sentInBatch;
              console.log(`Batch ${Math.floor(i / batchSize) + 1} queued. Response indicates ~${sentInBatch} sent.`);
          } catch (batchError) {
              console.error(`Error sending batch starting at index ${i}:`, batchError);
              failedSends += batch.length;
          }
        }
    
        console.log(`Broadcast finished. Attempted: ${totalRecipients}, Queued (estimated): ${successfulSends}, Failed Batches (estimated): ${failedSends}`);
    
        // Send feedback to the admin
        res.send(`
          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <title>Broadcast Result</title>
            <style>
              body { font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto; }
              pre { background-color: #eee; padding: 10px; border: 1px solid #ccc; white-space: pre-wrap; word-wrap: break-word; border-radius: 4px; }
              .error { color: red; font-weight: bold; }
              a { color: #007bff; text-decoration: none; }
              a:hover { text-decoration: underline; }
            </style>
          </head>
          <body>
            <h1>Broadcast Result</h1>
            <p>Message queuing initiated for approximately ${successfulSends} out of ${totalRecipients} active subscribers.</p>
            ${failedSends > 0 ? `<p class="error">Failed to queue messages for ${failedSends} recipients due to batch errors. Check server logs for details.</p>` : ''}
            <hr>
            <p><strong>Message Sent:</strong></p>
            <pre id="message-sent"></pre>
            <a href="/">Back to Admin</a>
    
            <script>
              const messageBody = ${JSON.stringify(messageBody)};
              const preTag = document.getElementById('message-sent');
              if (preTag) {
                  preTag.textContent = messageBody;
              }
            </script>
          </body>
          </html>
        `);
    
      } catch (error) {
        console.error("Error sending broadcast:", error);
        res.status(500).send(`
            <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Broadcast Failed</title><style>body{font-family:sans-serif;padding:20px;}</style></head><body>
            <h1>Broadcast Failed</h1>
            <p>An internal error occurred while trying to send the broadcast. Check server logs.</p>
            <a href="/">Back to Admin</a>
            </body></html>
          `);
      }
    });

    This route retrieves active subscribers, batches them (max 50 per request), uses async/await with messagebird.messages.create for each batch, and provides feedback.

<!-- EXPAND: Could add structured logging library comparison table (Type: Enhancement) -->

5. Implement Error Handling and Logging for SMS Operations

Robust handling is crucial for production.

  • Database Connection: The connectDB function handles initial connection errors.
  • Webhook Errors: The /webhook route uses try...catch. Log errors server-side. Respond 200 OK to MessageBird unless you want retries for specific, transient errors.
  • Broadcast Errors: The /send route uses try...catch for overall errors and within the batch loop. Log errors and provide admin feedback.
  • Logging: console.* is basic. Integrate a structured logging library like Winston or Pino:
    bash
    npm install winston
    Conceptual Winston Setup:
    javascript
    // logger.js (Example - configure further)
    const winston = require('winston');
    
    const logger = winston.createLogger({
      level: 'info', // Log info and above (warn, error)
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json() // Log in JSON format
      ),
      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 not in production then log to the `console`
    if (process.env.NODE_ENV !== 'production') {
      logger.add(new winston.transports.Console({
        format: winston.format.simple(),
      }));
    }
    
    module.exports = logger;
    
    // In index.js:
    // const logger = require('./logger');
    // Replace console.log with logger.info, console.error with logger.error etc.
    // e.g., logger.info(`Subscribed ${originator}`);
    //       logger.error(`Error processing webhook for ${originator}:`, error);
<!-- DEPTH: Retry logic lacks specific error code handling (Priority: Medium) -->
  • Retries: For critical operations like sending SMS, implement retries for transient errors (network issues, temporary API limits). Libraries like async-retry can help. Conceptual Retry for sendSms:
    javascript
    // npm install async-retry
    // const retry = require('async-retry');
    // const logger = require('./logger'); // Assuming logger setup
    //
    // async function sendSmsWithRetry(recipient, body) {
    //   await retry(async bail => {
    //     try {
    //       // Try sending
    //       await sendSms(recipient, body);
    //     } catch (error) {
    //       // If it's an error MessageBird won't recover from (e.g., invalid number), bail out
    //       // This requires inspecting the error structure from the SDK
    //       // Example check (adjust based on actual SDK error structure):
    //       // if (error.statusCode === 400 && error.errors && error.errors[0].code === 21) {
    //       //   bail(new Error('Non-retryable error: Invalid recipient'));
    //       //   return;
    //       // }
    //       // Rethrow other errors to trigger retry
    //       throw error;
    //     }
    //   }, {
    //     retries: 3, // Number of retries
    //     factor: 2, // Exponential backoff factor
    //     minTimeout: 1000, // Initial delay ms
    //     onRetry: (error, attempt) => {
    //       logger.warn(`Retrying sendSms to ${recipient} (attempt ${attempt}) due to error:`, error.message);
    //     }
    //   });
    // }
    // // Use sendSmsWithRetry instead of sendSms in webhook/broadcast logic
    (Full integration of advanced logging and retries requires careful setup beyond this core guide but is highly recommended.)
<!-- GAP: Missing schema migration strategy (Type: Substantive) -->

6. Design MongoDB Schema for SMS Subscriber Management

  • Schema: We use a simple MongoDB document structure in the subscribers collection:
    json
    {
      ""_id"": ""ObjectId(...)"",
      ""number"": ""+12025550135"",
      ""subscribed"": true,
      ""updatedAt"": ""ISODate(...)""
    }
    • _id: Auto-generated by MongoDB.
    • number: Subscriber phone number (E.164 format) - Indexed, Unique.
    • subscribed: Boolean indicating opt-in status.
    • updatedAt: Timestamp of last status change.
<!-- EXPAND: Could add additional subscriber metadata fields (Type: Enhancement) -->
  • Indexing: A unique index on the number field is crucial for performance and data integrity. This is now automatically created or verified on application startup within the connectDB function.
  • Data Layer: The helper functions (findSubscriber, addOrUpdateSubscriber, etc.) provide a basic data access layer, abstracting database operations.
<!-- GAP: Missing OWASP security checklist (Type: Critical) -->

7. Implement Security Best Practices for SMS Marketing

  • Input Validation/Sanitization:
    • Webhook: We trim() and toLowerCase() commands. The originator format is generally reliable from MessageBird.
    • Admin Form: The message body is used. The result page now uses textContent for safer display. Validate message length/content server-side.
<!-- DEPTH: Authentication middleware examples lack JWT and OAuth implementations (Priority: High) -->
  • Protect Admin Endpoints: The / (admin form) and /send (broadcast action) routes must be protected. Implement authentication and authorization. This is critical for production.
    • Strategies: Session-based auth (using express-session and a login form), JSON Web Tokens (JWT), Basic Authentication (less secure, suitable only for internal tools with HTTPS), or OAuth integration.
    • Conceptual Middleware:
      javascript
      // Example: Placeholder authentication middleware
      function ensureAuthenticated(req, res, next) {
        // Replace with actual authentication check (e.g., check session, validate JWT)
        // Example: Check if user is logged in via session
        // const isAuthenticated = req.session && req.session.userId;
      
        const isAuthenticated = false; // <<< --- IMPLEMENT YOUR ACTUAL LOGIC HERE ---
      
        if (isAuthenticated) {
          return next(); // User is authenticated, proceed
        } else {
          // User not authenticated
          // Option 1: Redirect to login page
          // res.redirect('/login');
          // Option 2: Send 401 Unauthorized or 403 Forbidden
          res.status(401).send('Unauthorized: Please log in.');
        }
      }
      
      // Apply the middleware to protected routes:
      app.get('/', ensureAuthenticated, async (req, res) => { /* ... route handler ... */ });
      app.post('/send', ensureAuthenticated, async (req, res) => { /* ... route handler ... */ });
      
      // You would also need routes for login/logout, user management etc.
<!-- GAP: Missing HTTPS/TLS configuration guide (Type: Critical) -->
  • Rate Limiting:
    • Webhooks: Use express-rate-limit on /webhook to prevent abuse.
    • Admin: Apply rate limiting to /send and login endpoints.
    bash
    npm install express-rate-limit
    Conceptual Rate Limiting:
    javascript
    // const rateLimit = require('express-rate-limit');
    //
    // 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
    // });
    //
    // const adminLimiter = rateLimit({
    //    windowMs: 60 * 60 * 1000, // 1 hour
    //    max: 50, // Limit each IP to 50 admin actions per hour
    //    message: 'Too many admin actions from this IP, please try again after an hour',
    //    standardHeaders: true,
    //    legacyHeaders: false,
    // });
    //
    // // Apply to specific routes
    // app.use('/webhook', webhookLimiter);
    // app.use('/', adminLimiter); // Apply to both GET / and POST /send
    // app.use('/send', adminLimiter); // Explicitly apply to /send as well if needed
<!-- EXPAND: Could add i18n implementation example (Type: Enhancement) -->

8. Handle Edge Cases in SMS Subscription Management

  • Case Insensitivity: Handled via .toLowerCase() for commands.
  • Duplicate Subscriptions: addOrUpdateSubscriber handles this gracefully via upsert.
  • Unsubscribe Non-subscriber: Logic checks state before unsubscribing.
  • Unknown Commands: Ignored; optionally send a help message.
  • Number Formatting: Assumes E.164 from MessageBird.
  • Internationalization: Requires storing language preferences and loading localized templates/messages.
<!-- DEPTH: Performance optimization lacks benchmarking examples (Priority: Medium) -->

9. Optimize SMS Campaign Performance and Delivery

  • Database Indexing: Unique index on number is implemented.
  • Batch Sending: Implemented in /send (50 recipients/request).
  • Asynchronous Operations: async/await used for non-blocking I/O.
<!-- GAP: Missing load testing methodology and tools comparison (Type: Substantive) -->
  • Load Testing: Use tools like k6, Artillery to test /webhook and /send under load.
  • Caching: Consider Redis for caching active subscribers in high-read scenarios, ensuring proper cache invalidation on subscription changes.
<!-- GAP: Missing metrics collection examples and dashboard setup (Type: Substantive) -->

10. Monitor SMS Campaign Delivery and Performance

  • Health Checks: Add a robust health check endpoint. Using a database ping command is more reliable than just checking the connection variable.
    javascript
    // index.js (Add/Update this route)
    app.get('/health', async (req, res) => {
      try {
        if (!db) {
          throw new Error('Database connection not established');
        }
        // Ping the database admin database to check connection status
        await db.admin().ping();
        res.status(200).send('OK');
      } catch (error) {
        console.error("Health check failed:", error);
        res.status(503).send('Service Unavailable - DB Connection issue');
      }
    });
  • Logging: Centralized, structured logging (Section 5).
<!-- EXPAND: Could add APM tool comparison and configuration guide (Type: Enhancement) -->
  • Metrics: Use libraries like prom-client to expose Prometheus metrics (request counts, latency, errors).
  • Error Tracking: Integrate services like Sentry, Bugsnag, or Datadog APM for real-time error reporting and analysis.
  • MessageBird Dashboard: Monitor SMS delivery rates, errors, and costs directly within the MessageBird platform.
<!-- DEPTH: Troubleshooting section lacks diagnostic commands and log analysis (Priority: Medium) -->

11. Troubleshoot Common MessageBird SMS Issues

  • Webhook Not Firing: Check localtunnel status/URL, ensure your Node.js server is running and accessible, check server logs for errors on startup or during requests, verify firewall settings, and review the MessageBird Flow Builder logs for errors or delivery attempts.
  • Messages Not Sending: Verify MESSAGEBIRD_API_KEY and MESSAGEBIRD_ORIGINATOR in .env, check MessageBird account balance/credits, ensure recipient numbers are valid E.164 format, check server logs for API errors from the sendSms function, and look at MessageBird Dashboard logs for delivery status.
  • Database Errors: Ensure MongoDB is running and accessible, verify the MONGODB_URI is correct, check database server logs, and monitor database performance. The unique index prevents duplicate numbers but errors during upsert should be logged.
<!-- EXPAND: Could add production deployment checklist (Type: Enhancement) -->
  • Localtunnel Instability: localtunnel URLs can change or tunnels can drop. For more stable development, consider ngrok or deploying to a staging environment with a permanent URL. For production, deploy to a hosting provider (PaaS like Heroku/Render, or IaaS like AWS/GCP/Azure) with a public IP/domain.
  • Rate Limits: Be aware of MessageBird API rate limits and your own application's rate limits (Section 7). Implement backoff/retry strategies (Section 5).

For additional MessageBird SMS tutorials and guides, check out:

Questions? Contact MessageBird support or explore the MessageBird Developer Documentation for comprehensive API references and additional SMS marketing resources.

Frequently Asked Questions

How to subscribe to SMS campaign?

Text "SUBSCRIBE" to the designated virtual mobile number (VMN) provided by the campaign organizer. The system will add your number to the subscriber database and send you a confirmation message. You can unsubscribe at any time by texting "STOP".

How to unsubscribe from SMS campaign texts?

To opt out of the SMS campaign, simply text "STOP" to the VMN. The application will update your subscription status in the database to "unsubscribed" and confirm your opt-out via SMS. You can resubscribe by texting "SUBSCRIBE".

What is MessageBird used for in SMS campaign?

MessageBird is the Communications Platform as a Service (CPaaS) that handles sending and receiving SMS messages in this application. It provides the API and infrastructure for SMS communication, including webhooks to notify your application about incoming messages.

How to send bulk SMS messages using Node.js?

The admin interface allows broadcasting messages to all active subscribers. The application batches recipients in groups of 50 (MessageBird's limit per request) and sends the message via the MessageBird API using Node.js. The result page provides feedback on the queuing status.

How to integrate MessageBird API with Express app?

Install the 'messagebird' npm package (`npm install messagebird`). Then, require the package, initialize it with your API key, and use the SDK's `messages.create` method to send messages. Incoming messages are handled by configuring a webhook endpoint with MessageBird that points to a route in your Express app (e.g., `/webhook`).

How to store subscriber data for SMS campaign?

Subscriber data is stored in a MongoDB database. Each subscriber document contains the phone number, subscription status, and the timestamp of the last status update. A unique index on the phone number field ensures data integrity and efficient lookups.

What is the purpose of Localtunnel in MessageBird setup?

Localtunnel creates a publicly accessible URL for your locally running application during development, allowing MessageBird webhooks to reach your server. This is necessary because webhooks require a public URL, which your local development environment doesn't have directly.

How to set up MessageBird webhook for SMS replies?

In the MessageBird Dashboard, go to "Numbers", find your number, and click the "Flow" icon. Create a "Custom Flow" with "SMS" as the trigger. Add a "Forward to URL" step, set the method to "POST", and paste your Localtunnel URL (e.g., https://your-subdomain.loca.lt/webhook) as the destination URL. Publish the flow to activate it.

Why does MessageBird need a virtual mobile number?

A Virtual Mobile Number (VMN) is required to receive incoming SMS messages, such as "SUBSCRIBE" and "STOP" commands, and can also serve as the sender ID for your outgoing messages. You'll purchase this from the "Numbers" section in the MessageBird dashboard.

How to build an SMS campaign admin panel?

The admin panel is built using a simple HTML form within the Express app. The GET '/' route displays this form, allowing administrators to enter a message to broadcast. Submitting the form triggers a POST '/send' request, which then handles broadcasting the message.

Can I use an alphanumeric sender ID with MessageBird?

Some countries allow using an alphanumeric Sender ID (like 'MyBrand') instead of a VMN for outgoing messages. Check MessageBird's documentation for supported countries and regulations. You still need a VMN to *receive* messages.

When should I use ngrok instead of Localtunnel?

While Localtunnel is suitable for initial testing, Ngrok offers more stable tunnels and URLs, making it a better choice for more extensive development or when dealing with webhooks that require a consistent URL during testing.

What is the technology stack for this SMS campaign app?

The application uses Node.js for the backend runtime, Express as the web framework, MongoDB for data storage, and MessageBird for SMS communication. It also utilizes 'dotenv' for managing environment variables. Utilities like 'localtunnel' are only used for development purposes.

How to handle errors in MessageBird webhook integration?

Implement robust error handling using `try...catch` blocks within your webhook route handler and within individual steps involving interactions with both the database and the MessageBird API. Log errors server-side. Return a 200 OK to MessageBird unless you want it to retry specific transient errors.

What are best practices for securing an SMS campaign application?

Secure the admin endpoints ('/' and '/send') with proper authentication and authorization. Implement input validation and sanitization to prevent injection attacks. Use environment variables for sensitive information, enforce HTTPS, apply rate limiting, and ensure adherence to compliance rules and regulations like GDPR and TCPA.