code examples
code examples
Build SMS Marketing Campaigns with MessageBird, Node.js & Express: Complete Guide
Step-by-step guide to building production-ready SMS marketing campaigns using MessageBird API, Node.js, and Express. Includes webhook integration, MongoDB setup, TCPA compliance, batch sending, and deployment best practices.
Build Production-Ready SMS Marketing Campaigns with MessageBird, Node.js & Express
Learn how to build a complete SMS marketing campaign system using MessageBird API, Node.js, and Express. This comprehensive tutorial shows you how to create a production-ready application where users subscribe and unsubscribe via SMS while administrators broadcast marketing messages to opted-in subscribers. You'll implement webhook integration for two-way messaging, MongoDB database management, TCPA compliance features, and deployment best practices for scaling your SMS marketing campaigns.
This SMS campaign application solves the common business need to manage SMS marketing lists efficiently while adhering to opt-in/opt-out regulations and TCPA compliance requirements. Leverage MessageBird's programmable virtual mobile numbers (VMNs) and real-time webhooks to create a seamless two-way messaging experience for both subscribers and administrators, enabling automated subscription management and bulk message broadcasting.
Technologies Used:
- Node.js: JavaScript runtime for building scalable network applications.
- Express: Minimal and flexible Node.js web application framework.
- MessageBird API: Communication platform providing APIs for SMS messaging and virtual numbers. (Node.js SDK v4.0.1, last updated 2022)
- MongoDB: NoSQL database for storing subscriber information. (Node.js driver v6+)
- dotenv: Module to load environment variables from a
.envfile. - Winston: Versatile logging library.
- Ngrok/Localtunnel: (For development) Tools to expose your local server to the internet for webhook testing.
System Architecture:
+----------+ SMS +-------------+ Webhook +-----------------+ DB Ops +-----------+
| End User | <-------------> | MessageBird | -----------------> | Node.js/Express | <---------------> | MongoDB |
+----------+ (SUBSCRIBE/STOP)+-------------+ (POST /webhook) | App | (Find/Update User)+-----------+
^ (VMN Config) +-----------------+ ^
| SMS | ^ |
| Admin UI | Write |
+----|-----+ API Call +-------------+ (POST /send) |
| Admin | ------------------> | MessageBird | <-----------------------+---------------------------+
+----------+ (Send Messages) +-------------+ (Read Subscribers)
(Web Form)Prerequisites:
- Node.js and npm installed. (Download Node.js)
- A MessageBird account. (Sign up for MessageBird)
- Access to a MongoDB database (local instance or a cloud service like MongoDB Atlas).
- (Optional but Recommended for Dev) Ngrok or Localtunnel installed.
By the end of this guide, you'll have a functional SMS subscription management and broadcasting application ready for further customization and deployment. For related topics, see our guides on E.164 phone number formatting and 10DLC SMS registration requirements for US businesses.
1. Set up the Node.js Project with MessageBird SDK
Initialize your Node.js project and install the necessary dependencies for your SMS marketing campaign application. This section covers project initialization, dependency installation, environment variable configuration, and basic Express server setup with security middleware.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
bashmkdir messagebird-sms-campaign cd messagebird-sms-campaign -
Initialize npm: Create a
package.jsonfile to manage dependencies.bashnpm init -y -
Install Dependencies: Install Express for the web server, the MessageBird SDK, the MongoDB driver,
dotenvfor environment variables, andwinstonfor logging. Modern Express versions include body parsing middleware, sobody-parseris not strictly required as a separate dependency.bashnpm install express messagebird mongodb dotenv winston express-rate-limit helmetexpress: Web framework (includes JSON and URL-encoded body parsing).messagebird: Official SDK for interacting with the MessageBird API.mongodb: Driver for connecting to and interacting with MongoDB.dotenv: Loads environment variables from.envintoprocess.env.winston: Robust logging library.express-rate-limit: Middleware for rate limiting requests (added in Security section).helmet: Middleware for setting secure HTTP headers (added in Security section).
-
Create
.gitignore: Prevent sensitive files and unnecessary directories from being committed to version control. Create a file named.gitignorewith the following content:text# Environment variables .env # Logs *.log logs/ # Dependency directories node_modules/ # Build artifacts dist/ build/ # OS generated files .DS_Store Thumbs.db -
Create
.envFile: Create a file named.envin the project root to store sensitive configuration. Never commit this file to Git.dotenv# MessageBird Credentials MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY MESSAGEBIRD_ORIGINATOR=YOUR_VIRTUAL_MOBILE_NUMBER_OR_SENDER_ID # MongoDB Connection MONGODB_URI=mongodb://localhost:27017/sms_campaign # Or your MongoDB Atlas URI # Application Port PORT=8080 # Basic Admin Auth (INSECURE – Development/Demo ONLY) ADMIN_PASSWORD=your_secret_passwordMESSAGEBIRD_API_KEY: Obtain your Live API key from the MessageBird Dashboard (Developers → API access).MESSAGEBIRD_ORIGINATOR: This is the virtual mobile number (VMN) you purchased from MessageBird (in E.164 format, e.g.,+12025550156) or an approved alphanumeric sender ID (max 11 characters, check country restrictions).MONGODB_URI: The connection string for your MongoDB database. Replace with your actual URI if using Atlas or a different setup.sms_campaignis the database name.PORT: The port your application will run on locally.ADMIN_PASSWORD: WARNING: This plain-text password is for demonstration purposes only. In a production environment, you must replace this with a secure authentication mechanism (e.g., hashed passwords with a proper login flow using libraries like Passport.js, or OAuth).
-
Basic
index.jsStructure: Create anindex.jsfile in the root directory. This will be the entry point of our application.javascript// index.js require('dotenv').config(); // Load environment variables first const express = require('express'); const { MongoClient } = require('mongodb'); const MessageBird = require('messagebird'); const winston = require('winston'); const helmet = require('helmet'); // For security headers const rateLimit = require('express-rate-limit'); // For rate limiting // --- Logger Setup --- const logger = winston.createLogger({ level: 'info', // Default log level format: winston.format.combine( winston.format.timestamp(), winston.format.json() // Log in JSON format for easier parsing ), transports: [ // - Write all logs with level `error` and below to `error.log` new winston.transports.File({ filename: 'error.log', level: 'error' }), // - Write all logs with level `info` and below to `combined.log` new winston.transports.File({ filename: 'combined.log' }), ], }); // If we're not in production, log to the `console` as well if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() // Simple format for console ), level: 'debug', // Log more verbosely in development })); } // Initialize MessageBird SDK const messagebird = MessageBird(process.env.MESSAGEBIRD_API_KEY); // MongoDB Client Setup const mongoClient = new MongoClient(process.env.MONGODB_URI); let db; let subscribersCollection; // Initialize Express App const app = express(); const port = process.env.PORT || 8080; // --- Security Middleware --- app.use(helmet()); // Set various security HTTP headers // --- Middleware --- // Parse JSON bodies for API requests (built-in Express middleware) app.use(express.json()); // Parse URL-encoded bodies (for form submissions and webhooks) app.use(express.urlencoded({ extended: true })); // --- Database Connection --- async function connectDB() { try { await mongoClient.connect(); logger.info('Successfully connected to MongoDB.'); db = mongoClient.db(); // Default DB from URI or specify name: mongoClient.db('sms_campaign') subscribersCollection = db.collection('subscribers'); // Ensure index on phone number for faster lookups await subscribersCollection.createIndex({ number: 1 }, { unique: true }); logger.info('Subscribers collection ready and indexed.'); } catch (error) { logger.error('Failed to connect to MongoDB', { message: error.message, stack: error.stack }); process.exit(1); // Exit if DB connection fails } } // --- Routes --- // (We will add routes in the next sections) // --- Start Server --- async function startServer() { await connectDB(); app.listen(port, () => { logger.info(`SMS Campaign App listening on port ${port}`); }); } startServer(); // Graceful shutdown process.on('SIGINT', async () => { logger.info('Shutting down server...'); try { await mongoClient.close(); logger.info('MongoDB connection closed.'); } catch (error) { logger.error('Error closing MongoDB connection during shutdown', { message: error.message, stack: error.stack }); } finally { process.exit(0); } });This sets up the basic Express server, loads environment variables, initializes the MessageBird SDK and Winston logger, establishes the MongoDB connection, applies basic security middleware, and prepares the
subscriberscollection with a unique index on thenumberfield.
2. Implement SMS Webhook Handler for Incoming Messages
Create a webhook endpoint to receive and process incoming SMS messages sent by users to your MessageBird virtual number. This enables two-way SMS messaging where users can text "SUBSCRIBE" to opt-in or "STOP" to opt-out from your marketing campaigns, with automatic confirmation messages and database updates.
-
Configure MessageBird Flow Builder:
- Go to the "Numbers" section in your MessageBird Dashboard.
- Find the virtual mobile number you're using as
MESSAGEBIRD_ORIGINATOR. - Click the "Flow" icon (often looks like branching lines or
</>) next to the number. If no flow exists, click "Add flow". - Choose "Create Custom Flow".
- Give your flow a name (e.g., "SMS Subscription Handler").
- Select "SMS" as the trigger. Click "Next".
- You'll see the "SMS" step connected to your number. Click the
+button below it. - Choose "Forward to URL" from the available steps.
- Method: Select
POST. - URL: This needs to be a publicly accessible URL pointing to your application's webhook endpoint.
- For Development: Start your local server (
node index.js). Then, use ngrok or localtunnel:ngrok http 8080orlt --port 8080- Copy the
httpsforwarding URL provided (e.g.,https://randomstring.ngrok.ioorhttps://yoursubdomain.loca.lt). - Append
/webhookto this URL (e.g.,https://randomstring.ngrok.io/webhook). Paste this full URL into the Flow Builder step.
- For Production: Use your deployed application's public URL (e.g.,
https://your-app-domain.com/webhook).
- For Development: Start your local server (
- Click "Save".
- Click "Publish" in the top right corner to activate the flow.
-
Create the Webhook Route (
/webhook): Add the following route handler insideindex.jswithin the// --- Routes ---section.javascript// index.js (continued) // --- Routes --- // Helper function to send confirmation SMS (fire-and-forget) function sendConfirmation(recipient, message) { const params = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: [recipient], body: message, }; messagebird.messages.create(params, (err, response) => { if (err) { // Log the error, potentially add to a retry queue for critical confirmations logger.error(`Error sending confirmation SMS to ${recipient}`, { error: err, recipient: recipient }); } else { const status = response?.recipients?.items[0]?.status; logger.info(`Confirmation SMS sent to ${recipient}. Status: ${status || 'Unknown'}`, { recipient: recipient, status: status }); } }); } app.post('/webhook', async (req, res) => { const { originator, payload } = req.body; const text = payload ? payload.trim().toLowerCase() : ''; // Handle potentially empty payload if (!originator || !text) { logger.warn('Webhook received invalid data: Missing originator or payload', { requestBody: req.body }); return res.status(400).send('Missing originator or payload'); } logger.info(`Webhook received: From ${originator}, Text: "${text}"`, { originator, text }); try { const existingSubscriber = await subscribersCollection.findOne({ number: originator }); if (text === 'subscribe') { if (!existingSubscriber) { // New subscriber await subscribersCollection.insertOne({ number: originator, subscribed: true, subscribedAt: new Date(), unsubscribedAt: null, }); logger.info(`New subscriber added: ${originator}`, { originator }); sendConfirmation(originator, 'Thanks for subscribing! Text STOP to unsubscribe.'); } else if (!existingSubscriber.subscribed) { // Re-subscribing await subscribersCollection.updateOne( { number: originator }, { $set: { subscribed: true, subscribedAt: new Date(), unsubscribedAt: null } } ); logger.info(`Subscriber re-subscribed: ${originator}`, { originator }); sendConfirmation(originator, 'Welcome back! You are now subscribed again. Text STOP to unsubscribe.'); } else { // Already subscribed logger.info(`Subscriber already subscribed: ${originator}`, { originator }); sendConfirmation(originator, 'You are already subscribed. Text STOP to unsubscribe.'); } } else if (text === 'stop') { if (existingSubscriber && existingSubscriber.subscribed) { // Opting out await subscribersCollection.updateOne( { number: originator }, { $set: { subscribed: false, unsubscribedAt: new Date() } } ); logger.info(`Subscriber opted out: ${originator}`, { originator }); sendConfirmation(originator, 'You have unsubscribed and will no longer receive messages.'); } else { // Not subscribed or already opted out logger.info(`Unsubscribe request from non-active subscriber: ${originator}`, { originator }); sendConfirmation(originator, 'You are not currently subscribed.'); } } else { // Optional: Handle unrecognized commands logger.info(`Unrecognized command from ${originator}: "${text}"`, { originator, text }); sendConfirmation(originator, 'Unrecognized command. Text SUBSCRIBE to join or STOP to leave.'); } // Acknowledge receipt to MessageBird immediately. // This is crucial to prevent MessageBird from retrying the webhook delivery // due to timeouts. The actual confirmation SMS sending happens asynchronously // in the background via the sendConfirmation function. res.status(200).send('OK'); } catch (error) { logger.error(`Error processing webhook for ${originator}`, { originator: originator, text: text, message: error.message, stack: error.stack, requestBody: req.body }); // Don't send detailed error back to MessageBird, just acknowledge failure internally. // Sending 500 might cause MessageBird to retry, consider if 200 is safer depending on error type. res.status(500).send('Internal Server Error'); } }); // (Add Admin routes below)This route handles incoming
POSTrequests from MessageBird. It parses the sender's number (originator) and the message text (payload), converts the text to lowercase, and performs database operations based on thesubscribeorstopkeywords. It uses theloggerfor detailed logging. It calls thesendConfirmationhelper function (which runs asynchronously) to send confirmation messages back to the user and responds to MessageBird with a200 OKimmediately to acknowledge receipt, explaining why this is done.
3. Build the MessageBird API Integration for Bulk SMS Broadcasting
Create an admin interface to send bulk SMS marketing messages to all subscribed users. This section implements batch processing to handle large subscriber lists efficiently, with built-in error handling and delivery tracking. You'll build a password-protected web form for campaign management.
-
Create Admin Form Route (
GET /admin): This route displays the HTML form. Add this within the// --- Routes ---section inindex.js.javascript// index.js (continued) app.get('/admin', async (req, res) => { let subscriberCount = 0; let countMessage = ''; try { subscriberCount = await subscribersCollection.countDocuments({ subscribed: true }); countMessage = `Current Active Subscribers: ${subscriberCount}`; } catch(error) { logger.error("Failed to get subscriber count for admin page", { message: error.message, stack: error.stack }); countMessage = "Could not retrieve subscriber count."; } // Simple HTML form res.send(` <!DOCTYPE html> <html> <head><title>Send Campaign Message</title></head> <body> <h1>Send SMS Campaign Message</h1> <p>${countMessage}</p> <form action="/send" method="POST"> <div> <label for="message">Message:</label><br> <textarea id="message" name="message" rows="4" cols="50" required></textarea> </div> <div> <label for="password">Admin Password:</label><br> <input type="password" id="password" name="password" required> </div> <br> <button type="submit">Send Message</button> </form> </body> </html> `); }); -
Create Send Message Route (
POST /send): This route handles the form submission, verifies the password, fetches subscribers, and sends the message in batches. Add this within the// --- Routes ---section.javascript// index.js (continued) app.post('/send', async (req, res) => { const { message, password } = req.body; // --- Basic Authentication (INSECURE - Replace in Production) --- if (password !== process.env.ADMIN_PASSWORD) { logger.warn('Admin send attempt failed: Invalid password.', { remoteAddress: req.ip }); return res.status(401).send('Unauthorized: Invalid password.'); } if (!message || message.trim() === '') { logger.warn('Admin send attempt failed: Empty message.', { remoteAddress: req.ip }); return res.status(400).send('Bad Request: Message content cannot be empty.'); } try { // Fetch active subscribers' numbers const subscribers = await subscribersCollection.find( { subscribed: true }, { projection: { number: 1, _id: 0 } } // Only get the number field ).toArray(); if (subscribers.length === 0) { logger.info('Admin send attempt: No active subscribers found.'); return res.send('No active subscribers to send message to.'); } const recipientNumbers = subscribers.map(sub => sub.number); logger.info(`Preparing to send message to ${recipientNumbers.length} subscribers.`, { count: recipientNumbers.length }); // --- Batch Sending (MessageBird limit: 50 recipients per API call) --- const batchSize = 50; let successfulSends = 0; let failedBatches = 0; const totalBatches = Math.ceil(recipientNumbers.length / batchSize); for (let i = 0; i < recipientNumbers.length; i += batchSize) { const batch = recipientNumbers.slice(i, i + batchSize); const batchNumber = Math.floor(i / batchSize) + 1; const params = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: batch, body: message, }; try { // Use await directly with the promise returned by the SDK (v3+) const response = await messagebird.messages.create(params); // Log success/partial success for the batch const firstStatus = response?.recipients?.items[0]?.status; logger.info(`Batch ${batchNumber}/${totalBatches} sent. Count: ${batch.length}. First status: ${firstStatus || 'Unknown'}`, { batchNumber, totalBatches, count: batch.length, firstStatus }); successfulSends += batch.length; // Assume success for count unless specific errors are handled per recipient } catch (batchError) { failedBatches++; logger.error(`Error sending batch ${batchNumber}/${totalBatches}. Count: ${batch.length}`, { batchNumber, totalBatches, count: batch.length, message: batchError.message, errors: batchError.errors // MessageBird SDK often includes detailed errors here }); // Decide if you want to stop or continue on batch failure } } const totalSent = successfulSends; // Adjust if tracking per-recipient status within batches logger.info(`Finished sending campaign. Total potentially sent: ${totalSent}. Failed batches: ${failedBatches}`, { totalSent, failedBatches }); res.send(`Campaign message processed for ${recipientNumbers.length} subscribers. ${failedBatches > 0 ? `Encountered errors in ${failedBatches} batches. Check logs for details.` : 'All batches submitted successfully.'}`); } catch (error) { logger.error('Error during message sending process (fetching subscribers or general failure)', { message: error.message, stack: error.stack }); res.status(500).send('Internal Server Error while sending messages.'); } }); // (Start Server function below)This route first checks the admin password (again, stressing this is INSECURE). If valid, it fetches all subscribed numbers from MongoDB. It then iterates through the numbers in batches of 50 and sends the message using
await messagebird.messages.create(params);, leveraging the native Promise returned by recent SDK versions. Detailed logging usingloggeris included for batch success and failure. -
Test with
curl: Test the/sendendpoint usingcurl(replace placeholders):bashcurl -X POST http://localhost:8080/send \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "message=Hello subscribers! Special offer today only!" \ -d "password=your_secret_password"(Response: A success or error message from the server)
4. Integrate MessageBird SMS API with Node.js (Summary)
You've already integrated MessageBird in the previous steps. Here's a summary of the key MessageBird API integration points:
- Installation:
npm install messagebird - Initialization:
const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY);placed early inindex.js. - API Key: Store securely in
.env(MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY) and load viadotenv. Obtain this from your MessageBird Dashboard → Developers → API access. Ensure it's a Live key for actual sending. - Originator: Configure in
.env(MESSAGEBIRD_ORIGINATOR=YOUR_VMN_OR_SENDER_ID). This must be a number purchased/verified in your MessageBird account or an approved alphanumeric ID. - Send Messages: Use
await messagebird.messages.create(params)(for Promise-based handling) ormessagebird.messages.create(params, callback)(for callback style). Theparamsobject must includeoriginator,recipients(an array of numbers), andbody. - Receive Messages (Webhook): Requires configuring a Flow in the MessageBird Dashboard ("Numbers" section) to forward incoming SMS for your
MESSAGEBIRD_ORIGINATORnumber viaPOSTto your application's/webhookURL. The webhook handler parsesreq.body.originatorandreq.body.payload.
5. Implement Error Handling, Logging, and SMS Retry Mechanisms
Production SMS applications need robust error handling and logging.
-
Logging Strategy: Integrate Winston for structured JSON logging to files (
error.log,combined.log) and optionally to the console in development.- Configuration: Done near the top of
index.js. - Usage: Replace all
console.*calls withlogger.info,logger.warn, andlogger.error. Log errors with message and stack trace where applicable. Include contextual information (likeoriginator,batchNumber) in log calls.
- Configuration: Done near the top of
-
Error Handling:
- Explicit
try...catch: Wrap database operations and MessageBird API calls intry...catchblocks. - Input Validation: Perform basic checks for required fields (
originator,payload,message,password), returning appropriate HTTP status codes (400, 401). - Consistent Responses: Send meaningful HTTP status codes (200, 400, 401, 500). Log internal error details but don't expose them in production responses. The webhook responds
200 OKquickly to MessageBird.
- Explicit
-
Retry Mechanisms (for Sending): MessageBird API calls might fail temporarily. Implement retries only for critical outgoing messages (like the main campaign send), not usually for webhook processing (to avoid duplicate actions).
-
Simple Manual Retry Example (Illustrative): The following demonstrates a basic retry loop within the
/sendroute's batch processing. For production, consider a more robust library likeasync-retry.javascript// Inside app.post('/send', ...) batch loop (Illustrative – Use a library for robustness) const MAX_RETRIES = 3; const RETRY_DELAY_MS = 1000; // Base delay for (let i = 0; i < recipientNumbers.length; i += batchSize) { const batch = recipientNumbers.slice(i, i + batchSize); const batchNumber = Math.floor(i / batchSize) + 1; const params = { /* ... */ }; let attempt = 0; let batchSuccess = false; while (attempt < MAX_RETRIES && !batchSuccess) { attempt++; try { const response = await messagebird.messages.create(params); batchSuccess = true; // Assume success if no exception logger.info(`Batch ${batchNumber}/${totalBatches}: Send attempt ${attempt} successful. Count: ${batch.length}.`, { batchNumber, totalBatches, attempt, count: batch.length }); successfulSends += batch.length; } catch (batchError) { logger.warn(`Batch ${batchNumber}/${totalBatches}: Send attempt ${attempt} failed.`, { batchNumber, totalBatches, attempt, count: batch.length, message: batchError.message, errors: batchError.errors }); if (attempt >= MAX_RETRIES) { logger.error(`Batch ${batchNumber}/${totalBatches}: Send failed after ${MAX_RETRIES} attempts. Giving up on this batch.`, { batchNumber, totalBatches, maxAttempts: MAX_RETRIES, count: batch.length, message: batchError.message, errors: batchError.errors }); failedBatches++; // Optionally: Log the specific numbers in the failed batch for manual follow-up } else { // Basic exponential backoff (1s, 2s, 4s…) const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1); logger.info(`Batch ${batchNumber}/${totalBatches}: Retrying attempt ${attempt+1} after ${delay}ms…`); await new Promise(resolve => setTimeout(resolve, delay)); } } } } // End of batch loop -
Note: Implement retry logic carefully. Retrying non-idempotent operations can cause issues. Sending SMS is generally safe to retry if the initial attempt failed definitively.
-
6. Design MongoDB Schema for SMS Subscriber Management
Use MongoDB with a simple schema defined by the documents you insert for SMS subscription tracking.
-
Collection:
subscribers -
Document Structure:
json{ "_id": "ObjectId(...)", "number": "+12025550156", "subscribed": true, "subscribedAt": "ISODate(...)", "unsubscribedAt": null }_id: Automatically generated by MongoDB.number: Phone number in E.164 format (Indexed, Unique).subscribed: Boolean indicating current subscription status.subscribedAt: Timestamp of the last subscription action.unsubscribedAt: Timestamp of the last unsubscription action (null if subscribed).
-
Data Access: Handle via the
mongodbdriver'sMongoClient. Set upmongoClient,db, andsubscribersCollectioninindex.js. -
Key Operations:
subscribersCollection.findOne({ number: ... })(Used in webhook)subscribersCollection.insertOne({ ... })(Used for new subscribers)subscribersCollection.updateOne({ number: ... }, { $set: { ... } })(Used for re-subscribing or unsubscribing)subscribersCollection.find({ subscribed: true }, { projection: { ... } }).toArray()(Used in/send)subscribersCollection.countDocuments({ subscribed: true })(Used in/admin)
-
Indexing: A unique index on the
numberfield (subscribersCollection.createIndex({ number: 1 }, { unique: true })) is crucial for performance and data integrity, ensuring you don't store duplicate numbers. Create this on application start. -
Migrations: For this simple schema, migrations aren't strictly necessary. For evolving schemas, tools like
migrate-mongocan manage database changes systematically. -
Sample Data (Manual Insertion via
mongosh):javascript// Connect using mongosh: mongosh "YOUR_MONGODB_URI" // use sms_campaign; // Select the database db.subscribers.insertOne({ number: "+12025550199", // Replace with a test number subscribed: true, subscribedAt: new Date(), unsubscribedAt: null });
7. Add Security Features for SMS Marketing Applications
Security is paramount when handling user data and sending SMS messages.
- Input Validation:
- Webhook: Check that
originatorandpayloadexist. Trimpayload. Consider adding E.164 format validation fororiginator. - Send Route: Ensure
messageis not empty. Validatepassword. Consider adding length limits or sanitization tomessage.
- Webhook: Check that
- Rate Limiting: Protect against brute-force attacks and abuse. Add
express-rate-limit.-
Implementation: Add configuration in
index.jsbefore routes.javascript// index.js (Add near other middleware) // Rate limiting for the webhook (adjust limits as needed) const webhookLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); app.use('/webhook', webhookLimiter); // Stricter rate limiting for the admin send endpoint const adminLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // Limit each IP to 5 send attempts per hour message: 'Too many send attempts from this IP, please try again later.', standardHeaders: true, legacyHeaders: false, }); app.use('/send', adminLimiter); app.use('/admin', adminLimiter); // Also limit access to the admin form page
-
- Security Headers: Protect against common web vulnerabilities (XSS, clickjacking, etc.). Add
helmet.- Implementation:
app.use(helmet());added early inindex.js.
- Implementation:
- Authentication:
- Admin: The current plain-text password check in
/sendis highly insecure. Replace this immediately in a production environment with a proper authentication system (e.g., Passport.js with username/hashed password stored securely, OAuth, JWT). - Webhook: MessageBird webhooks don't have built-in authentication beyond the obscurity of the URL. For higher security, consider implementing MessageBird Webhook Signature Verification. This involves comparing a signature header sent by MessageBird with a signature you compute using your webhook signing key and the request body.
- Admin: The current plain-text password check in
- Environment Variables: Keep sensitive data (API keys, DB URIs, passwords) out of the codebase using
.envanddotenv. Ensure the.envfile is never committed to version control (.gitignore). - Logging: Avoid logging excessively sensitive information (like full message bodies if they contain PII, or passwords). Review your current logging based on data sensitivity.
- Database Security:
- Use strong, unique credentials for your MongoDB instance.
- Configure network access rules (firewalls) to only allow connections from your application server's IP address.
- Consider encryption at rest if required by compliance standards.
- Dependency Management: Keep dependencies up-to-date (
npm audit,npm update) to patch known vulnerabilities. Use tools like Snyk or Dependabot for automated scanning.
8. Deploy Your SMS Campaign Application to Production
Deploy your Node.js SMS marketing application following these steps.
- Choose a Hosting Provider: Options include:
- PaaS (Platform as a Service): Heroku, Render, Fly.io, Google App Engine, AWS Elastic Beanstalk. Often simpler to manage.
- IaaS (Infrastructure as a Service): AWS EC2, Google Compute Engine, DigitalOcean Droplets. More control, more management overhead.
- Serverless: AWS Lambda, Google Cloud Functions, Vercel Serverless Functions. Good for event-driven workloads like webhooks, potentially cost-effective.
- Prepare for Production:
- Environment Variables: Set production values for
MESSAGEBIRD_API_KEY,MONGODB_URI,PORT, and crucially, replace the insecureADMIN_PASSWORDwith your secure authentication mechanism's configuration. Do not use a.envfile in production; use the hosting provider's mechanism for setting environment variables. - Set
NODE_ENV=production: This environment variable enables optimizations in Express and other libraries and disables development-only features (like verbose console logging in your setup). - Build Step (If using TypeScript/Babel): Transpile your code to JavaScript if necessary.
- Environment Variables: Set production values for
- Process Manager: Use a process manager like PM2 or Nodemon (in production mode) to:
- Keep the application running continuously.
- Restart automatically if it crashes.
- Manage clustering (running multiple instances) for better performance and availability.
- Example (using PM2):
bash
npm install pm2 -g # Install globally pm2 start index.js --name messagebird-sms-app -i max # Start clustered pm2 startup # Configure PM2 to start on system boot pm2 save # Save current process list
- Reverse Proxy (Recommended): Use Nginx or Apache in front of your Node.js application to:
- Handle SSL/TLS termination (HTTPS).
- Serve static assets efficiently.
- Perform load balancing if running multiple instances.
- Provide basic caching or security filtering.
- Database: Ensure your production MongoDB instance is properly secured, backed up, and scaled for your expected load. Use the production connection string.
- Webhook URL: Update the MessageBird Flow Builder ("Forward to URL" step) to use your production application's public URL (e.g.,
https://your-app-domain.com/webhook). Ensure this URL is accessible from the internet. - Monitoring & Logging:
- Set up monitoring tools (e.g., Prometheus/Grafana, Datadog, New Relic) to track application performance (CPU, memory, response times) and errors.
- Aggregate logs from your application (e.g., using the hosting provider's logging service, or tools like ELK Stack, Splunk, Logtail) for easier analysis and troubleshooting. Your Winston file transport is suitable for basic logging, but centralized logging is better for production.
9. SMS Marketing Compliance and Next Steps
You've successfully built a foundational SMS marketing campaign application using Node.js, Express, MongoDB, and MessageBird API. Users can subscribe and unsubscribe via SMS, and an administrator can broadcast messages to active subscribers.
Key achievements:
- Project setup with essential dependencies.
- Webhook implementation for handling incoming
SUBSCRIBEandSTOPmessages. - Database integration with MongoDB for subscriber management.
- Basic admin interface for sending broadcast messages.
- Integration of logging, basic error handling, and security considerations.
Next Steps:
- Secure Admin Authentication: Replace the insecure password check with a robust authentication system (Passport.js, OAuth, etc.).
- Improve Admin Interface: Build a proper web UI (using React, Vue, Angular, or server-side templating like EJS) instead of the basic HTML form. Include features like viewing subscribers, message history, etc.
- Webhook Security: Implement MessageBird webhook signature verification for enhanced security.
- Advanced SMS Marketing Features:
- Personalization: Use placeholders in messages (e.g.,
Hello {name}, …) for targeted campaigns. - Scheduling: Allow admins to schedule SMS campaigns for optimal delivery times.
- Analytics: Track delivery rates, click-through rates (via URL shorteners), and unsubscribe rates.
- A/B Testing: Test different message variations to optimize campaign performance.
- Personalization: Use placeholders in messages (e.g.,
- Scalability: Implement clustering (PM2) and optimize database queries for handling larger subscriber lists efficiently.
- More Robust Retries: Use libraries like
async-retryor implement a background job queue (e.g., BullMQ, Kue) for handling message sending failures more reliably. - Testing: Add unit tests (Jest, Mocha) and integration tests (Supertest) to ensure code quality and prevent regressions.
- Advanced Topics: Explore SMS delivery status tracking and OTP/2FA implementation for enhanced functionality.
- TCPA Compliance for SMS Marketing (United States): Ensure full compliance with the Telephone Consumer Protection Act (TCPA) for SMS marketing in the US:
- Express Written Consent: Obtain prior express written consent before sending promotional or automated SMS messages. Consumers must take a clear affirmative action to opt in – consent cannot be implied.
- Time Restrictions: Do not send messages before 8:00 AM or after 9:00 PM in the recipient's local time zone (TCPA requirement).
- Opt-Out Processing: Process opt-out requests within 24 hours. Under new TCPA rules effective April 11, 2025, honor revocations within 10 business days maximum through various methods including emails, voicemails, and informal messages like "Leave me alone".
- One-to-One Consent (Effective January 26, 2026): The new one-to-one consent rule requires marketers to obtain specific consent from each consumer to receive telemarketing calls and texts.
- Required Disclosures: Provide clear disclosures explicitly stating the recipient agrees to receive automated marketing messages from your specific company.
- Penalties: TCPA violations can result in $500 to $1,500 per violation, plus potential class-action lawsuits.
- Message Type Distinction: Transactional messages (order confirmations, shipping updates, etc.) do not require express written consent but must be strictly informational. Promotional messages always require express written consent.
- GDPR Compliance (European Union): If targeting EU residents, ensure compliance with data protection requirements including explicit consent, data access rights, and the right to be forgotten.
- Additional Compliance Considerations: Research and comply with local SMS marketing regulations in your target markets (e.g., CASL in Canada, various regulations in other countries).
Frequently Asked Questions
How to set up MessageBird webhook for SMS campaign?
Configure a Flow in your MessageBird Dashboard under the "Numbers" section. Select "Create Custom Flow," choose "SMS" as the trigger, and then "Forward to URL" as the next step. Set the method to POST and enter your application's public webhook URL (e.g., 'https://your-app.com/webhook'). For development, use ngrok or localtunnel to create a public URL and append '/webhook'.
What is the role of MessageBird in this SMS campaign application?
MessageBird is the communication platform providing the APIs for sending and receiving SMS messages, as well as managing virtual mobile numbers (VMNs). It acts as the bridge between your application and the mobile network, allowing you to send bulk messages and receive user replies via webhooks.
Why does the webhook respond with 200 OK immediately?
The immediate '200 OK' response to MessageBird's webhook POST request acknowledges receipt and prevents MessageBird from retrying the delivery due to potential timeouts. The actual SMS confirmations to users are handled asynchronously afterwards by a separate function, ensuring MessageBird doesn't retry the webhook unnecessarily.
When should I use a Live API key with MessageBird?
Always use a Live API key for production environments where you're sending real SMS messages. Test API keys are strictly for development and testing and shouldn't be used for actual campaigns due to rate limits and potential data inconsistencies.
Can I test the send functionality without sending real messages?
For initial development, you can test the send functionality by logging the outgoing messages instead of actually sending them via the MessageBird API. For more realistic testing, create test accounts within MessageBird with dedicated virtual numbers to avoid sending unexpected SMS to real users during development.
How to send SMS messages in batches with MessageBird?
The MessageBird API has a limit of 50 recipients per API call. The application code iterates through the subscriber list and sends messages in batches of 50 using a loop and array slicing to adhere to this limit. It's crucial to implement this batching to avoid errors from MessageBird.
What is the database schema for storing subscribers?
The MongoDB database uses the `subscribers` collection. Each document stores the subscriber's `number` (E.164 format), `subscribed` status (boolean), `subscribedAt` timestamp, and `unsubscribedAt` timestamp. A unique index on the `number` field ensures no duplicate numbers are stored.
Why is the provided admin password setup insecure?
Storing the admin password in plain text in the `.env` file is extremely insecure. For production, replace this with a proper authentication system like Passport.js, OAuth 2.0, or JWT, which should store password hashes securely and implement robust login flows.
How to improve the security of the webhook endpoint?
Implement MessageBird Webhook Signature Verification to ensure that incoming webhook requests are genuinely from MessageBird and not malicious actors. This involves calculating a signature hash and comparing it to the one provided in the 'MessageBird-Signature' header.
What are the recommended deployment options for this application?
Recommended deployment options include PaaS solutions like Heroku, Render, or Fly.io for ease of management, or IaaS like AWS EC2 or Google Compute Engine for more control. Serverless functions might be suitable for specific components but may not be ideal for the whole application due to webhook and long-running process needs.
How to implement retry mechanisms for sending SMS messages?
Use a library like `async-retry` or a job queue system to handle temporary failures when sending messages. This ensures better reliability for your SMS campaign and reduces the chance of messages not reaching subscribers.
What logging strategy is used in the application?
The application uses Winston for logging, configured to log in JSON format for easier parsing and analysis. Logs are written to 'error.log' for error messages and 'combined.log' for other log levels. In development, logs are also outputted to the console.
What are the next steps to enhance this application?
Implement secure admin authentication, improve the admin UI for better user experience, implement MessageBird webhook signature verification, add advanced messaging features like personalization and scheduling, implement robust retry mechanisms, and thorough testing are important next steps to consider.