code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / Node.js

MessageBird SMS Marketing with Node.js: Complete Campaign Builder Tutorial (2025)

Learn to build production-ready SMS marketing campaigns with MessageBird, Node.js & MongoDB. Step-by-step tutorial covers subscriber opt-in/opt-out, bulk broadcasting, webhook integration, TCPA compliance, and MongoDB setup with 50+ code examples.

Build SMS Marketing Campaign App with MessageBird, Node.js & MongoDB (2024-2025)

Learn to build a production-ready SMS marketing campaign system using MessageBird API, Node.js, Express, and MongoDB. This comprehensive tutorial covers keyword-based subscriptions (SUBSCRIBE/STOP), webhook integration for automated opt-in/opt-out, MongoDB subscriber management, bulk SMS broadcasting to 50+ recipients per batch, and TCPA compliance requirements.

You'll implement MessageBird webhooks to handle incoming SMS messages, create an admin dashboard for campaign broadcasting, set up MongoDB schemas for subscriber tracking, add error handling with retry mechanisms, and deploy your SMS marketing application with proper security and logging. By the end, you'll have a fully functional MessageBird SMS marketing system ready for production deployment.

What You'll Build: Production SMS Marketing System with MessageBird

Application Features:

  • Express.js Web Application: Listens for incoming SMS messages via MessageBird webhook endpoint
  • Keyword-Based Subscription Management: Automatically handles SUBSCRIBE and STOP keywords for user opt-in/opt-out
  • MongoDB Subscriber Database: Stores subscriber phone numbers, subscription status, and timestamps for compliance tracking
  • Admin Dashboard: Password-protected web interface for sending bulk SMS campaigns to active subscribers
  • MessageBird API Integration: Receives incoming SMS via webhooks and sends outgoing confirmation/campaign messages with retry logic

Problem Solved:

This MessageBird SMS marketing system helps businesses automate subscriber list management with TCPA and GDPR compliance. The application handles keyword-based opt-in/opt-out (SUBSCRIBE/STOP commands), stores subscriber data in MongoDB, and enables bulk SMS broadcasting to active subscribers—perfect for retail promotions, event notifications, appointment reminders, and customer engagement campaigns.

Technologies Used:

  • Node.js (v18+ recommended): JavaScript runtime for building the backend server application with async/await support
  • Express.js: Minimal, flexible Node.js web framework for handling routing, HTTP requests (admin interface, webhook endpoints)
  • MessageBird API & SDK: Send and receive SMS messages, manage virtual mobile numbers (VMN), configure webhook flows for inbound messaging
  • MongoDB: NoSQL database for storing subscriber information with the official mongodb Node.js driver
  • dotenv: Load environment variables from .env into process.env for secure credential management
  • EJS Templating: Render the admin web interface (alternative: Handlebars)
  • basic-auth: Simple HTTP Basic Authentication for admin interface (production requires stronger auth)
  • localtunnel (Development Only): Expose local development server for testing MessageBird webhooks. Warning: Not secure or stable for production webhook handling.

System Architecture:

text
+-----------------+      +----------------------+      +---------------------+      +-------------------+      +-------------+
|  User's Phone   |----->| MessageBird Platform |----->|    Webhook URL      |----->| Node.js/Express App |----->|  MongoDB    |
| (Sends Keyword) |      | (VMN, Flow)        |      | (via localtunnel/   |      | (/webhook endpoint) |      | (Subscribers)|
+-----------------+      +----------------------+      |  public domain)     |      +-------------------+      +-------------+
        ^                                                /|\       |                      |
        |                                                 |        |                      |
(Receives Confirmation/ |                                                 |        |                      |
   Campaign SMS)       |                                                 |        \----------------------/
        |                /|\                                                |                               |
+-----------------+  |  +----------------------+      +---------------------+         +-------------------+
| Admin Interface |----- | Node.js/Express App |----->| MessageBird Platform |         | (Sends campaign)  |
| (Web Browser)   |      | (/send endpoint)    |      | (API Call)          |         +-------------------+
+-----------------+      +---------------------+      +----------------------+

(Note: The ASCII diagram above illustrates the basic flow. A graphical diagram might offer better clarity and maintainability.)

Prerequisites:

  • Node.js v18 or later (v22 Active LTS recommended as of 2024-2025) and npm installed. Download Node.js
  • MessageBird account. Sign up for MessageBird or Bird (rebranded February 2024)
  • Purchased Virtual Mobile Number (VMN) with SMS capabilities from MessageBird
  • MongoDB instance (local or MongoDB Atlas for cloud hosting) - MongoDB v4.4+ recommended
  • Basic familiarity with JavaScript, Node.js, REST APIs, and SMS marketing concepts
  • Terminal or command prompt access
  • (Optional) Git for version control
  • (Optional) Postman or curl for testing API endpoints

Final Outcome:

A production-ready MessageBird SMS marketing application with automated subscription management, TCPA-compliant opt-in/opt-out handling, bulk broadcasting to unlimited subscribers (batched at 50 per API call), MongoDB persistence, Winston logging, retry mechanisms, and security features ready for deployment.


1. Set Up Your MessageBird Node.js SMS Campaign Project

Let's initialize our 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 messagebird-sms-campaign
    cd messagebird-sms-campaign
  2. Initialize Node.js Project: Create a package.json file to manage project dependencies and scripts:

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

    bash
    npm install express messagebird mongodb dotenv ejs basic-auth # Added basic-auth here for Section 3
  4. Install Development Dependencies: We'll use nodemon for easier development (automatically restarts the server on file changes) and localtunnel for testing webhooks locally.

    bash
    npm install --save-dev nodemon localtunnel
  5. Create Project Structure: Set up a basic folder structure for better organization:

    bash
    mkdir views # For EJS templates
    touch index.js .env .gitignore
  6. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing them to version control:

    text
    # .gitignore
    node_modules/
    .env
    *.log
  7. Set up .env File: Create placeholders for your MessageBird credentials, originator number, database connection string, and a basic password for the admin interface. Never commit this file to Git.

    dotenv
    # .env
    MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY
    MESSAGEBIRD_ORIGINATOR=YOUR_VIRTUAL_NUMBER_OR_SENDER_ID # e.g., +12005550199 or MarketingApp
    MONGO_URI=mongodb://localhost:27017/sms_campaign # Or your MongoDB Atlas URI
    ADMIN_PASSWORD=your_secret_password # Change this immediately! Example only.
    PORT=8080 # Optional: default port
    • MESSAGEBIRD_API_KEY: Your Live API key from the MessageBird Dashboard.
    • MESSAGEBIRD_ORIGINATOR: The phone number (VMN) or alphanumeric Sender ID messages will come from. Check MessageBird's country restrictions for alphanumeric IDs.
    • MONGO_URI: Your MongoDB connection string.
    • ADMIN_PASSWORD: WARNING: This is a highly insecure example password. Change it immediately to a strong, unique password. For production, use robust authentication methods as detailed in Section 7.
    • PORT: The port your Express app will listen on.
  8. Add npm Scripts: Update the scripts section in your package.json for easier starting and development:

    json
    // package.json
    "scripts": {
      "start": "node index.js",
      "dev": "nodemon index.js",
      "tunnel": "lt --port 8080 --subdomain your-unique-sms-webhook" // Replace with a unique subdomain
    },
    • npm start: Runs the application normally.
    • npm run dev: Runs the application using nodemon for development.
    • npm run tunnel: Starts localtunnel to expose port 8080. Make sure to choose a unique subdomain.

2. Implement MessageBird Webhook Handler for SMS Opt-In/Opt-Out

The core logic resides in the webhook handler, which processes incoming SMS messages.

  1. Basic Express Setup (index.js): Start by setting up Express, loading environment variables, and initializing the MessageBird and MongoDB clients.

    javascript
    // index.js
    require('dotenv').config(); // Load .env variables first
    const express = require('express');
    const { MongoClient } = require('mongodb');
    const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY);
    const auth = require('basic-auth'); // Require basic-auth for Section 3
    const ejs = require('ejs'); // Require EJS for Section 3
    
    const app = express();
    const port = process.env.PORT || 8080;
    const mongoUri = process.env.MONGO_URI;
    const dbName = 'sms_campaign'; // Or extract from MONGO_URI if needed
    const collectionName = 'subscribers';
    
    let db; // Database connection variable
    
    // Middleware to parse URL-encoded bodies (form submissions) and JSON
    app.use(express.urlencoded({ extended: true }));
    app.use(express.json()); // Needed for MessageBird webhook (assuming JSON payload)
    
    // --- Database Connection ---
    async function connectDB() {
        try {
            const client = new MongoClient(mongoUri);
            await client.connect();
            db = client.db(dbName);
            console.log(`Successfully connected to MongoDB: ${dbName}`);
            // Ensure index on phone number for faster lookups
            await db.collection(collectionName).createIndex({ number: 1 }, { unique: true });
            console.log(`Index created/ensured on 'number' field in ${collectionName}`);
        } catch (err) {
            console.error("Failed to connect to MongoDB", err);
            process.exit(1); // Exit if DB connection fails
        }
    }
    
    // --- Webhook Handler ---
    app.post('/webhook', async (req, res) => {
        // Check if DB is connected
        if (!db) {
            console.error("Webhook received but database not connected.");
            return res.status(500).send("Internal Server Error: Database not ready.");
        }
    
        // MessageBird sends webhook data in req.body.
        // NOTE: Verify the exact payload structure against current MessageBird documentation.
        // This code assumes { originator: 'sender_number', payload: 'message_text' }.
        const { originator, payload } = req.body;
    
        // Basic validation
        if (!originator || !payload) {
            console.warn('Received incomplete webhook payload:', req.body);
            return res.status(400).send('Bad Request: Missing originator or payload.');
        }
    
        const number = originator; // Phone number of the sender
        const text = payload.trim().toLowerCase(); // Message content
    
        console.log(`Webhook received from ${number}: "${text}"`);
    
        const subscribersCollection = db.collection(collectionName);
    
        try {
            if (text === 'subscribe') {
                // Add or update subscriber to subscribed: true
                const updateResult = await subscribersCollection.findOneAndUpdate(
                    { number: number },
                    { $set: { subscribed: true, updatedAt: new Date() }, $setOnInsert: { number: number, subscribedAt: new Date() } },
                    { upsert: true, returnDocument: 'after' } // Upsert: insert if not exists, return the updated doc
                );
    
                console.log('Subscription processed:', updateResult);
    
                // Send confirmation message (use retry version from Section 5 if implemented)
                sendConfirmation(number, 'Thanks for subscribing! Text STOP to unsubscribe.');
                // Example if using retry function: await sendConfirmationWithRetry(number, 'Thanks for subscribing! Text STOP to unsubscribe.');
    
    
            } else if (text === 'stop') {
                // Update subscriber to subscribed: false
                const updateResult = await subscribersCollection.findOneAndUpdate(
                    { number: number },
                    { $set: { subscribed: false, unsubscribedAt: new Date(), updatedAt: new Date() } },
                    { returnDocument: 'after' } // Only update if exists
                );
    
                 // Check if a document was found and updated (or found but already unsubscribed)
                 if (updateResult) {
                     console.log('Unsubscription processed:', updateResult);
                     // Send confirmation message (use retry version from Section 5 if implemented)
                     sendConfirmation(number, 'You have unsubscribed. Text SUBSCRIBE to join again.');
                    // Example if using retry function: await sendConfirmationWithRetry(number, 'You have unsubscribed. Text SUBSCRIBE to join again.');
                 } else {
                     console.log(`STOP received from non-subscriber: ${number}`);
                     // Optional: Send a message like "You are not currently subscribed."
                     // sendConfirmation(number, 'You are not currently subscribed.');
                 }
            } else {
                // Optional: Handle unrecognized keywords
                console.log(`Unrecognized keyword "${text}" from ${number}`);
                // Optional: Send help message
                // sendConfirmation(number, 'Unknown command. Text SUBSCRIBE to join or STOP to leave.');
            }
    
            // Acknowledge receipt to MessageBird
            res.status(200).send('Webhook processed successfully.');
    
        } catch (error) {
            console.error(`Error processing webhook for ${number}:`, error);
            // Don't send error details back, just acknowledge receipt if possible
            // Or send a 500 if it was a critical processing failure
             res.status(500).send('Internal Server Error');
        }
    });
    
    // --- Helper Function to Send Confirmation SMS ---
    // NOTE: Consider replacing this with the sendConfirmationWithRetry function from Section 5
    function sendConfirmation(recipient, messageBody) {
        const params = {
            originator: process.env.MESSAGEBIRD_ORIGINATOR,
            recipients: [recipient],
            body: messageBody,
        };
    
        messagebird.messages.create(params, (err, response) => {
            if (err) {
                console.error(`Error sending confirmation SMS to ${recipient}:`, err);
                // Implement retry logic if necessary (See Section 5)
            } else {
                console.log(`Confirmation SMS sent to ${recipient}:`, response.id);
                // console.log(response); // Log full response if needed
            }
        });
    }
    
    // --- Admin Interface Routes (Implement in next section) ---
    // Placeholder routes are defined in Section 3
    
    // --- Start Server ---
    async function startServer() {
        await connectDB(); // Wait for DB connection before starting server
        app.listen(port, () => {
            console.log(`SMS Campaign App listening on port ${port}`);
            console.log(`Webhook endpoint available at /webhook`);
            console.log(`Admin interface available at /admin`);
        });
    }
    
    startServer(); // Initialize DB connection and start the server
    
    // --- Export app for potential testing (See Section 13) ---
    // module.exports = app; // Uncomment if separating server start for tests
  2. Explanation:

    • We initialize Express, MongoDB client, and MessageBird SDK.
    • The connectDB function establishes the connection to MongoDB and ensures a unique index on the number field for efficiency and data integrity.
    • The POST /webhook route handles incoming messages.
    • It extracts the sender's number (originator) and the message text (payload), assuming a specific payload structure that should be verified against current MessageBird documentation.
    • It converts the text to lowercase for case-insensitive keyword matching (subscribe, stop).
    • subscribe logic: Uses findOneAndUpdate with upsert: true. If the number exists, it sets subscribed: true. If not, it inserts a new document with the number, subscribed: true, and timestamps.
    • stop logic: Uses findOneAndUpdate (no upsert). If the number exists (regardless of current subscribed status), it attempts to set subscribed: false and records unsubscribedAt. The confirmation is sent if the user was found in the database, even if they were already unsubscribed.
    • A confirmation SMS is sent using the sendConfirmation helper function after processing the keyword. Consider replacing this with the retry version from Section 5.
    • Basic error handling and logging are included.
    • Crucially, the webhook responds with a 200 OK status to MessageBird to acknowledge receipt. Failure to respond quickly can cause MessageBird to retry the webhook.

3. Build Bulk SMS Broadcasting Interface with MessageBird API

Now, let's create a simple, password-protected web page for administrators to send messages.

  1. Install basic-auth: (Already done in Section 1, Step 3 if followed sequentially) If you haven't installed it yet:

    bash
    npm install basic-auth
  2. Create EJS Template (views/admin.ejs): This file will contain the HTML form for sending messages.

    html
    <!-- views/admin.ejs -->
    <!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; }
            label { display: block; margin-bottom: 5px; }
            textarea { width: 100%; min-height: 100px; margin-bottom: 10px; }
            button { padding: 10px 15px; cursor: pointer; }
            .message { padding: 10px; margin-bottom: 15px; border-radius: 4px; }
            .success { background-color: #e6ffed; border: 1px solid #34d399; color: #065f46; }
            .error { background-color: #fee2e2; border: 1px solid #f87171; color: #991b1b; }
        </style>
    </head>
    <body>
        <h1>Send Campaign Message</h1>
        <p>Total Active Subscribers: <strong><%= subscriberCount %></strong></p>
    
        <% if (message) { %>
            <div class="message <%= message.type %>"><%= message.text %></div>
        <% } %>
    
        <form action="/send" method="POST">
            <div>
                <label for="message">Message Body (Max 160 chars recommended):</label>
                <textarea id="message" name="message" required maxlength="1000"></textarea> <!-- Maxlength prevents overly long messages -->
            </div>
            <button type="submit">Send to Subscribers</button>
        </form>
    </body>
    </html>
  3. Implement Admin Routes (index.js): Add the authentication middleware and the GET/POST routes for the admin interface within your index.js file.

    javascript
    // index.js (add these parts or ensure they exist)
    
    // --- Basic Authentication Middleware ---
    const checkAuth = (req, res, next) => {
        const credentials = auth(req);
        const expectedPassword = process.env.ADMIN_PASSWORD; // Loaded from .env
    
        // WARNING: This basic comparison is vulnerable to timing attacks.
        // For production, use a constant-time comparison function (e.g., from crypto module or bcrypt.compare).
        // See Section 7 for more details.
        if (!credentials || !credentials.pass || credentials.pass !== expectedPassword) {
             res.setHeader('WWW-Authenticate', 'Basic realm="Admin Area"');
            return res.status(401).send('Authentication required.');
        }
        // Authentication successful
        return next();
    };
    
    // --- Set EJS as the templating engine ---
    app.set('view engine', 'ejs');
    app.set('views', './views'); // Specify the directory for templates
    
    // --- Admin Interface Route (GET) ---
    app.get('/admin', checkAuth, async (req, res) => {
        if (!db) return res.status(503).send("Service Unavailable: Database not ready.");
    
        try {
            const subscribersCollection = db.collection(collectionName);
            const count = await subscribersCollection.countDocuments({ subscribed: true });
            // Pass null message initially
            res.render('admin', { subscriberCount: count, message: null });
        } catch (error) {
            console.error("Error fetching subscriber count:", error);
            res.status(500).send("Error loading admin page.");
        }
    });
    
    // --- Send Message Route (POST) ---
    app.post('/send', checkAuth, async (req, res) => {
        if (!db) return res.status(503).send("Service Unavailable: Database not ready.");
    
        const messageBody = req.body.message;
        if (!messageBody || messageBody.trim() === '') {
             return res.status(400).send('Message body cannot be empty.'); // Basic validation
        }
    
        const subscribersCollection = db.collection(collectionName);
        let subscriberCount = 0; // Recalculate count before sending
        let messageInfo = null; // To pass feedback to the template
    
        try {
            // Find active subscribers
            const activeSubscribers = await subscribersCollection.find({ subscribed: true }).project({ number: 1, _id: 0 }).toArray();
            subscriberCount = activeSubscribers.length; // Update count
    
            if (activeSubscribers.length === 0) {
                 console.log("No active subscribers found to send message to.");
                 messageInfo = { type: 'error', text: 'No active subscribers to send message to.' };
                 // Re-render admin page with current count and message
                 return res.render('admin', { subscriberCount, message: messageInfo });
            }
    
            const recipients = activeSubscribers.map(sub => sub.number);
            console.log(`Attempting to send message to ${recipients.length} recipients.`);
    
            // MessageBird API allows up to 50 recipients per call. Batch if necessary.
            const batchSize = 50;
            let sentCount = 0;
            let errorCount = 0;
    
            for (let i = 0; i < recipients.length; i += batchSize) {
                const batch = recipients.slice(i, i + batchSize);
                const params = {
                    originator: process.env.MESSAGEBIRD_ORIGINATOR,
                    recipients: batch,
                    body: messageBody,
                };
    
                // Use Promises for cleaner async handling with MessageBird
                try {
                    const response = await new Promise((resolve, reject) => {
                        messagebird.messages.create(params, (err, resp) => {
                            if (err) { reject(err); }
                            else { resolve(resp); }
                        });
                    });
                    console.log(`Batch sent (ID: ${response?.id}), count: ${batch.length}`);
                    sentCount += batch.length; // Approximate count, actual delivery depends on MessageBird
                } catch (batchError) {
                     console.error(`Error sending batch starting at index ${i}:`, batchError);
                     errorCount++;
                     // Decide how to handle batch errors - stop? continue? log?
                     // For now, we log and continue.
                }
            }
    
             if (errorCount > 0) {
                messageInfo = { type: 'error', text: `Message sent to approximately ${sentCount} subscribers, but ${errorCount} batches failed. Check logs.` };
            } else {
                messageInfo = { type: 'success', text: `Message successfully queued for ${sentCount} subscribers.` };
            }
            // Render the admin page again with the feedback message and updated count
             res.render('admin', { subscriberCount, message: messageInfo });
    
    
        } catch (error) {
            console.error("Error sending campaign message:", error);
             // Fetch count again in case of error before rendering
             try {
                 subscriberCount = await subscribersCollection.countDocuments({ subscribed: true });
             } catch (countError) {
                 console.error("Error fetching count after send error:", countError);
             }
             messageInfo = { type: 'error', text: 'An unexpected error occurred while sending messages. Check logs.' };
            // Render admin page with error message
             res.render('admin', { subscriberCount, message: messageInfo });
        }
    });
    
    // Ensure startServer() is called after all route definitions
    // async function startServer() { ... } defined earlier
    // startServer(); // called earlier
  4. Explanation:

    • We require basic-auth.
    • The checkAuth middleware intercepts requests to protected routes (/admin, /send). It checks Authorization headers and compares the provided password against ADMIN_PASSWORD from .env. Crucially, the code uses a simple !== comparison which is vulnerable to timing attacks; Section 7 discusses secure alternatives. If auth fails, it sends a 401 Unauthorized.
    • The GET /admin route (protected by checkAuth) fetches the count of active subscribers and renders the admin.ejs template.
    • The POST /send route (also protected) receives the message body.
    • It fetches active subscribers (subscribed: true) from MongoDB.
    • It implements batching, splitting recipients into chunks of 50 for separate MessageBird API calls.
    • It uses Promises for cleaner handling of asynchronous MessageBird calls within the loop.
    • Basic error handling is included for the sending process.
    • After sending, it re-renders the admin.ejs page with a success/error message and the updated subscriber count.

4. Configure MessageBird Virtual Mobile Number and Webhook Flow

This section details how to get your MessageBird credentials and configure the necessary components in their dashboard.

  1. Get MessageBird API Key:

    • Log in to your MessageBird Dashboard (or Bird dashboard after February 2024 rebrand).
    • Navigate to the Developers section -> API access tab.
    • Copy the Live API key (create one if needed).
    • Paste this key into your .env file as MESSAGEBIRD_API_KEY. Keep this key secret.
    • Note: MessageBird rebranded as "Bird" in February 2024 with 90% price reductions. Existing MessageBird API continues to be supported.
  2. Purchase a Virtual Mobile Number (VMN) for SMS:

    • Go to the Numbers section. Click "Buy a number".
    • Select country (US recommended for marketing campaigns), ensure SMS capability is checked, choose a number, and purchase.
    • Copy the purchased number in E.164 format (e.g., +12005550199) and paste into .env as MESSAGEBIRD_ORIGINATOR.
    • Marketing Compliance: Purchased numbers are required for two-way SMS. Alphanumeric sender IDs don't support inbound messages (STOP/SUBSCRIBE keywords won't work).
  3. Configure MessageBird Flow for Inbound SMS Webhooks: This connects your VMN to your application's webhook endpoint for automated subscription handling.

    • Go to the Numbers section, find your VMN.
    • Click the "Add flow" icon or go to the "Flows" tab for the number.
    • Click "Create Flow" -> "Create Custom Flow".
    • Trigger: Choose SMS.
    • Name: Give it a descriptive name (e.g., "SMS Campaign Webhook").
    • Steps:
      • The SMS trigger step (linked to your number) should be present.
      • Click + below the SMS step. Choose "Forward to URL".
      • Method: Select POST.
      • URL: Enter the public URL of your webhook endpoint.
        • For Development: Start localtunnel (npm run tunnel). Copy the https://your-unique-sms-webhook.loca.lt URL and append /webhook. Full URL: https://your-unique-sms-webhook.loca.lt/webhook. Remember localtunnel is not for production.
        • For Production: Use your deployed application's public domain/IP address followed by /webhook (e.g., https://yourdomain.com/webhook). Must be HTTPS.
      • Click Save.
    • (Optional but Recommended) Add Contact Step: Click + before "Forward to URL". Choose "Add Contact". Configure as needed.
    • Publish: Click "Publish changes" to activate the flow.

    Your flow should essentially be: SMS (Trigger) -> [Optional Add Contact] -> Forward to URL (POST to your webhook).

  4. Environment Variables Recap:

    • MESSAGEBIRD_API_KEY: Your live API key. Used by the SDK for authentication.
    • MESSAGEBIRD_ORIGINATOR: Your VMN or alphanumeric sender ID. Used as the 'From' for outgoing messages.
    • Ensure these are correctly set in .env (local) and configured securely in production.

5. Add Production Error Handling and SMS Retry Logic

Robust applications need proper error handling and logging.

  1. Consistent Error Handling:

    • Use try...catch around asynchronous operations.
    • Log errors clearly with context.
    • Respond appropriately to HTTP requests (500 for server errors, 400 for bad input). Avoid leaking sensitive details.
    • In /webhook, always try to send 200 OK to MessageBird upon receiving the request, even if internal processing fails (log the failure). Send 400 for malformed requests.
  2. Logging:

    • Use console.log/console.error for simplicity here. For production, use structured loggers like Winston or Pino.
    • Log key events: Server start, DB connection, webhook receipt, subscribe/unsubscribe actions, admin sends, MessageBird API results, errors.
    • Example Winston setup (install winston):
    javascript
    // Example: Setting up Winston (replace console.log/error)
    const winston = require('winston');
    
    const logger = winston.createLogger({ /* ... Winston config ... */ });
    // Replace console.log('Message') with logger.info('Message')
    // Replace console.error('Error', err) with logger.error('Error message', { error: err.message, stack: err.stack })

    (See original article section for full Winston example config)

  3. Retry Mechanisms for Reliable SMS Delivery:

    • Network issues or API glitches can cause SMS delivery failures. Implement retries with exponential backoff for critical sends (confirmations, campaign messages).
    • Use exponential backoff (wait 1s, 2s, 4s...). Limit retries (3-5 attempts max).
    • Action Required: Replace the original sendConfirmation function in index.js with the improved sendConfirmationWithRetry version below for production reliability.
    javascript
    // index.js - Replace the original sendConfirmation with this:
    async function sendConfirmationWithRetry(recipient, messageBody, maxRetries = 3, attempt = 1) {
        const params = {
            originator: process.env.MESSAGEBIRD_ORIGINATOR,
            recipients: [recipient],
            body: messageBody,
        };
    
        try {
            const response = await new Promise((resolve, reject) => {
                 messagebird.messages.create(params, (err, resp) => {
                    if (err) { reject(err); }
                    else { resolve(resp); }
                });
            });
            // Assuming 'logger' is defined (e.g., from Winston setup or use console.log)
            console.log(`Confirmation SMS sent to ${recipient} (Attempt ${attempt})`, { messageId: response?.id });
            return response; // Success
        } catch (err) {
            console.error(`Error sending confirmation SMS to ${recipient} (Attempt ${attempt})`, { error: err });
    
            if (attempt < maxRetries) {
                const delay = Math.pow(2, attempt -1) * 1000; // 1s, 2s, 4s...
                console.warn(`Retrying confirmation SMS to ${recipient} in ${delay}ms...`);
                await new Promise(resolve => setTimeout(resolve, delay));
                // Recursive call for retry
                return sendConfirmationWithRetry(recipient, messageBody, maxRetries, attempt + 1);
            } else {
                console.error(`Max retries reached for sending confirmation to ${recipient}. Giving up.`);
                throw err; // Re-throw the error after max retries
            }
        }
    }

MessageBird SMS Marketing Best Practices

MongoDB for SMS Applications

Production Deployment Resources


Frequently Asked Questions

How do I test MessageBird webhooks locally?

Use localtunnel or ngrok to expose your local development server to the internet. Run npm run tunnel to start localtunnel, copy the HTTPS URL it provides, append /webhook, and paste this full URL into your MessageBird Flow Builder's "Forward to URL" step. This allows MessageBird to send webhook requests to your local machine during development.

What's the difference between MessageBird Live and Test API keys?

Test API keys are for development/testing only and have rate limits. Live API keys are for production use and enable actual SMS sending. Always use Live keys when deploying your SMS marketing application to production environments.

How many SMS recipients can I send to per MessageBird API call?

MessageBird allows up to 50 recipients per API call. For larger subscriber lists, implement batching by splitting your recipient array into chunks of 50 and making multiple API calls. The tutorial code demonstrates this batching pattern in the bulk send functionality.

How do I handle MongoDB connection issues in production?

Implement connection retry logic, use MongoDB Atlas for managed hosting with automatic failover, monitor connection health, and always wrap database operations in try-catch blocks. Set up alerts for connection failures and maintain connection pooling for optimal performance.

What are TCPA compliance requirements for SMS marketing?

TCPA requires prior express written consent before sending promotional SMS, honoring opt-out requests within 24 hours (10 business days under 2025 rules), time restrictions (8 AM - 9 PM local time), clear disclosures, and proper opt-out processing. Violations can result in $500-$1,500 per message penalties.

How do I secure the admin interface for production use?

Replace the basic password authentication with proper user authentication systems like Passport.js, OAuth 2.0, or JWT tokens. Implement HTTPS, rate limiting, CSRF protection, session management, and consider multi-factor authentication for admin access. Never use plain-text passwords in production.

Can I use alphanumeric sender IDs for two-way SMS?

No, alphanumeric sender IDs (like "MyBrand") do not support inbound SMS messages. You must use a purchased Virtual Mobile Number (VMN) to receive SUBSCRIBE/STOP keywords and enable two-way communication for your SMS marketing campaigns.

How do I monitor SMS delivery rates with MessageBird?

Implement delivery status callbacks in your MessageBird Flow, log delivery receipts from webhook responses, track MessageBird API response codes, use MessageBird's dashboard analytics, and implement your own analytics database to track sent vs. delivered messages over time.

Store phone numbers in E.164 format with a unique index, track subscription status (boolean), include timestamps for opt-in and opt-out events, add metadata fields for compliance tracking, and consider adding fields for segmentation (tags, preferences) and campaign history.

How do I implement retry logic for failed SMS sends?

Use exponential backoff (1s, 2s, 4s delays), limit retry attempts (3-5 max), implement async retry patterns with setTimeout or libraries like async-retry, log all retry attempts, and consider using job queues (BullMQ, Kue) for more robust retry handling in high-volume scenarios.


Next Steps and Production Considerations

You've successfully built a foundational MessageBird SMS marketing campaign application with Node.js, Express, and MongoDB. Users can subscribe and unsubscribe via SMS keywords, and administrators can broadcast messages to active subscribers with proper batching and error handling.

Production Enhancements:

  • Implement Webhook Signature Verification: Add MessageBird webhook signature verification to ensure incoming requests are legitimate
  • Upgrade Authentication: Replace basic auth with Passport.js, OAuth, or JWT-based authentication
  • Add Advanced Analytics: Track delivery rates, conversion metrics, and subscriber engagement patterns
  • Implement Message Scheduling: Allow administrators to schedule campaigns for optimal send times
  • Add Subscriber Segmentation: Enable targeted campaigns based on subscriber attributes and behavior
  • Set Up Monitoring: Implement application monitoring with tools like Datadog, New Relic, or Prometheus
  • Deploy with CI/CD: Set up automated testing and deployment pipelines with GitHub Actions or CircleCI
  • Scale with Redis: Add Redis for session management, caching, and job queue processing
  • Implement Rate Limiting: Add per-user rate limits to prevent abuse of the admin interface
  • Add Message Templates: Create reusable message templates with variable substitution for personalization