code examples
code examples
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
SUBSCRIBEandSTOPkeywords 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
mongodbNode.js driver dotenv: Load environment variables from.envintoprocess.envfor 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:
+-----------------+ +----------------------+ +---------------------+ +-------------------+ +-------------+
| 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
curlfor 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.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it:
bashmkdir messagebird-sms-campaign cd messagebird-sms-campaign -
Initialize Node.js Project: Create a
package.jsonfile to manage project dependencies and scripts:bashnpm init -y -
Install Dependencies: We need Express for the web server, the MessageBird SDK, the MongoDB driver,
dotenvfor environment variables, andejsfor templating.bashnpm install express messagebird mongodb dotenv ejs basic-auth # Added basic-auth here for Section 3 -
Install Development Dependencies: We'll use
nodemonfor easier development (automatically restarts the server on file changes) andlocaltunnelfor testing webhooks locally.bashnpm install --save-dev nodemon localtunnel -
Create Project Structure: Set up a basic folder structure for better organization:
bashmkdir views # For EJS templates touch index.js .env .gitignore -
Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing them to version control:text# .gitignore node_modules/ .env *.log -
Set up
.envFile: 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 portMESSAGEBIRD_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.
-
Add
npmScripts: Update thescriptssection in yourpackage.jsonfor 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 usingnodemonfor development.npm run tunnel: Startslocaltunnelto 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.
-
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 -
Explanation:
- We initialize Express, MongoDB client, and MessageBird SDK.
- The
connectDBfunction establishes the connection to MongoDB and ensures a unique index on thenumberfield for efficiency and data integrity. - The
POST /webhookroute 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). subscribelogic: UsesfindOneAndUpdatewithupsert: true. If the number exists, it setssubscribed: true. If not, it inserts a new document with the number,subscribed: true, and timestamps.stoplogic: UsesfindOneAndUpdate(no upsert). If the number exists (regardless of currentsubscribedstatus), it attempts to setsubscribed: falseand recordsunsubscribedAt. 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
sendConfirmationhelper 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 OKstatus 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.
-
Install
basic-auth: (Already done in Section 1, Step 3 if followed sequentially) If you haven't installed it yet:bashnpm install basic-auth -
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> -
Implement Admin Routes (
index.js): Add the authentication middleware and the GET/POST routes for the admin interface within yourindex.jsfile.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 -
Explanation:
- We require
basic-auth. - The
checkAuthmiddleware intercepts requests to protected routes (/admin,/send). It checksAuthorizationheaders and compares the provided password againstADMIN_PASSWORDfrom.env. Crucially, the code uses a simple!==comparison which is vulnerable to timing attacks; Section 7 discusses secure alternatives. If auth fails, it sends a401 Unauthorized. - The
GET /adminroute (protected bycheckAuth) fetches the count of active subscribers and renders theadmin.ejstemplate. - The
POST /sendroute (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.ejspage with a success/error message and the updated subscriber count.
- We require
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.
-
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
.envfile asMESSAGEBIRD_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.
-
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.envasMESSAGEBIRD_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).
-
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 thehttps://your-unique-sms-webhook.loca.ltURL and append/webhook. Full URL:https://your-unique-sms-webhook.loca.lt/webhook. Rememberlocaltunnelis 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.
- For Development: Start
- 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).
-
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.
-
Consistent Error Handling:
- Use
try...catcharound asynchronous operations. - Log errors clearly with context.
- Respond appropriately to HTTP requests (
500for server errors,400for bad input). Avoid leaking sensitive details. - In
/webhook, always try to send200 OKto MessageBird upon receiving the request, even if internal processing fails (log the failure). Send400for malformed requests.
- Use
-
Logging:
- Use
console.log/console.errorfor 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)
- Use
-
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
sendConfirmationfunction inindex.jswith the improvedsendConfirmationWithRetryversion 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 } } }
Related Resources
MessageBird SMS Marketing Best Practices
- MessageBird official SMS marketing documentation
- SMS marketing compliance guide (TCPA, GDPR)
- Node.js SMS integration patterns
MongoDB for SMS Applications
- MongoDB schema design best practices
- MongoDB Atlas setup guide
- Indexing strategies for high-volume SMS apps
Production Deployment Resources
- Node.js production best practices
- Express.js security best practices
- Webhook security and verification
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.
What's the recommended database schema for SMS subscribers?
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