code examples
code examples
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
SUBSCRIBEkeyword - User opt-out via
STOPkeyword - Confirmation messages for subscription changes
- Admin interface to send broadcast messages
- Database storage for subscribers
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
.envfile
Note: Development tools like localtunnel help test webhooks locally but aren't part of the production stack.
System Architecture:
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]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.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
bashmkdir node-messagebird-campaign cd node-messagebird-campaign -
Initialize Node.js Project: Create a
package.jsonfile.bashnpm init -y -
Install Dependencies: Install Express for the web server, the MessageBird SDK, the MongoDB driver, and
dotenvfor managing environment variables.bashnpm install express messagebird mongodb dotenv -
Create Project Files: Create the main application file, environment file example, and a
.gitignorefile.bashtouch index.js .env .env.example .gitignore -
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 -
Set Up Environment Variables (
.env.exampleand.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=8080Now, create your actual
.envfile by copying.env.example. Never commit.envto Git. Fill it with your actual credentials (obtain these in the next steps).bashcp .env.example .envMESSAGEBIRD_API_KEY: Your Live API key from the MessageBird DashboardMESSAGEBIRD_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)
-
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.
- 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
.envfile asMESSAGEBIRD_API_KEY
-
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
.envfile asMESSAGEBIRD_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.
- Set Up Localtunnel for Webhook Testing:
MessageBird needs a publicly accessible URL to send incoming message notifications (webhooks). During development,
localtunnelexposes your local server.- Install
localtunnelglobally:bashnpm install -g localtunnel - Run your Node.js application (ensure it runs on the port specified in
.env, e.g., 8080):bashnode index.js - Open a new terminal window/tab in the same project directory
- Start
localtunnel, pointing it to your local application port:bashlt --port 8080 localtunnelwill 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 restartlocaltunnelunless you use advanced options.
- Install
-
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
localtunnelURL 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:
SMS→Forward to URL. Now, when someone sends an SMS to your VMN, MessageBird forwards the details to your application's/webhookendpoint via a POST request.
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.
-
Add Database Helper Functions (
index.js): Add functions to interact with thesubscriberscollection 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; } }
-
Create Webhook Handler (
index.js): Create the/webhookroute 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 a200 OKstatus back to MessageBird to acknowledge receipt.
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) -->-
Add Admin Form Route (
index.js): Add aGETroute 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."); } });
-
Add Broadcast Sending Route (
index.js): Add aPOSTroute 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/awaitwithmessagebird.messages.createfor each batch, and provides feedback.
5. Implement Error Handling and Logging for SMS Operations
Robust handling is crucial for production.
- Database Connection: The
connectDBfunction handles initial connection errors. - Webhook Errors: The
/webhookroute usestry...catch. Log errors server-side. Respond200 OKto MessageBird unless you want retries for specific, transient errors. - Broadcast Errors: The
/sendroute usestry...catchfor 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:Conceptual Winston Setup:bashnpm install winstonjavascript// 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);
- Retries: For critical operations like sending SMS, implement retries for transient errors (network issues, temporary API limits). Libraries like
async-retrycan help. Conceptual Retry forsendSms:(Full integration of advanced logging and retries requires careful setup beyond this core guide but is highly recommended.)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
6. Design MongoDB Schema for SMS Subscriber Management
- Schema: We use a simple MongoDB document structure in the
subscriberscollection: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.
- Indexing: A unique index on the
numberfield is crucial for performance and data integrity. This is now automatically created or verified on application startup within theconnectDBfunction. - Data Layer: The helper functions (
findSubscriber,addOrUpdateSubscriber, etc.) provide a basic data access layer, abstracting database operations.
7. Implement Security Best Practices for SMS Marketing
- Input Validation/Sanitization:
- Webhook: We
trim()andtoLowerCase()commands. Theoriginatorformat is generally reliable from MessageBird. - Admin Form: The message body is used. The result page now uses
textContentfor safer display. Validate message length/content server-side.
- Webhook: We
- 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-sessionand 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.
- Strategies: Session-based auth (using
- Rate Limiting:
- Webhooks: Use
express-rate-limiton/webhookto prevent abuse. - Admin: Apply rate limiting to
/sendand login endpoints.
Conceptual Rate Limiting:bashnpm install express-rate-limitjavascript// 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 - Webhooks: Use
8. Handle Edge Cases in SMS Subscription Management
- Case Insensitivity: Handled via
.toLowerCase()for commands. - Duplicate Subscriptions:
addOrUpdateSubscriberhandles this gracefully viaupsert. - 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.
9. Optimize SMS Campaign Performance and Delivery
- Database Indexing: Unique index on
numberis implemented. - Batch Sending: Implemented in
/send(50 recipients/request). - Asynchronous Operations:
async/awaitused for non-blocking I/O.
- Load Testing: Use tools like
k6,Artilleryto test/webhookand/sendunder load. - Caching: Consider Redis for caching active subscribers in high-read scenarios, ensuring proper cache invalidation on subscription changes.
10. Monitor SMS Campaign Delivery and Performance
- Health Checks: Add a robust health check endpoint. Using a database
pingcommand 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).
- Metrics: Use libraries like
prom-clientto 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.
11. Troubleshoot Common MessageBird SMS Issues
- Webhook Not Firing: Check
localtunnelstatus/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_KEYandMESSAGEBIRD_ORIGINATORin.env, check MessageBird account balance/credits, ensure recipient numbers are valid E.164 format, check server logs for API errors from thesendSmsfunction, and look at MessageBird Dashboard logs for delivery status. - Database Errors: Ensure MongoDB is running and accessible, verify the
MONGODB_URIis correct, check database server logs, and monitor database performance. The unique index prevents duplicate numbers but errors duringupsertshould be logged.
- Localtunnel Instability:
localtunnelURLs 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).
Related Resources
For additional MessageBird SMS tutorials and guides, check out:
- Send SMS with MessageBird and Node.js - Basic SMS sending guide
- MessageBird Two-Way SMS Messaging - Build interactive SMS conversations
- MessageBird SMS OTP and 2FA - Implement SMS authentication
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.