Learn to build a production-ready SMS marketing campaign system using MessageBird API, Node.js, Express, and MongoDB. This comprehensive tutorial covers keyword-based subscriptions (SUBSCRIBE/STOP), webhook integration for automated opt-in/opt-out, MongoDB subscriber management, bulk SMS broadcasting to 50+ recipients per batch, and TCPA compliance requirements.
You'll implement MessageBird webhooks to handle incoming SMS messages, create an admin dashboard for campaign broadcasting, set up MongoDB schemas for subscriber tracking, add error handling with retry mechanisms, and deploy your SMS marketing application with proper security and logging. By the end, you'll have a fully functional MessageBird SMS marketing system ready for production deployment.
What You'll Build: Production SMS Marketing System with MessageBird
Application Features:
Express.js Web Application: Listens for incoming SMS messages via MessageBird webhook endpoint
Keyword-Based Subscription Management: Automatically handles SUBSCRIBE and STOP keywords for user opt-in/opt-out
MongoDB Subscriber Database: Stores subscriber phone numbers, subscription status, and timestamps for compliance tracking
Admin Dashboard: Password-protected web interface for sending bulk SMS campaigns to active subscribers
MessageBird API Integration: Receives incoming SMS via webhooks and sends outgoing confirmation/campaign messages with retry logic
Problem Solved:
This MessageBird SMS marketing system helps businesses automate subscriber list management with TCPA and GDPR compliance. The application handles keyword-based opt-in/opt-out (SUBSCRIBE/STOP commands), stores subscriber data in MongoDB, and enables bulk SMS broadcasting to active subscribers—perfect for retail promotions, event notifications, appointment reminders, and customer engagement campaigns.
Technologies Used:
Node.js (v18+ recommended): JavaScript runtime for building the backend server application with async/await support
Express.js: Minimal, flexible Node.js web framework for handling routing, HTTP requests (admin interface, webhook endpoints)
MessageBird API & SDK: Send and receive SMS messages, manage virtual mobile numbers (VMN), configure webhook flows for inbound messaging
MongoDB: NoSQL database for storing subscriber information with the official mongodb Node.js driver
dotenv: Load environment variables from .env into process.env for secure credential management
EJS Templating: Render the admin web interface (alternative: Handlebars)
localtunnel (Development Only): Expose local development server for testing MessageBird webhooks. Warning: Not secure or stable for production webhook handling.
Purchased Virtual Mobile Number (VMN) with SMS capabilities from MessageBird
MongoDB instance (local or MongoDB Atlas for cloud hosting) - MongoDB v4.4+ recommended
Basic familiarity with JavaScript, Node.js, REST APIs, and SMS marketing concepts
Terminal or command prompt access
(Optional) Git for version control
(Optional) Postman or curl for testing API endpoints
Final Outcome:
A production-ready MessageBird SMS marketing application with automated subscription management, TCPA-compliant opt-in/opt-out handling, bulk broadcasting to unlimited subscribers (batched at 50 per API call), MongoDB persistence, Winston logging, retry mechanisms, and security features ready for deployment.
1. Set Up Your MessageBird Node.js SMS Campaign Project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory:
Open your terminal and create a new directory for your project, then navigate into it:
Bash
mkdir messagebird-sms-campaign
cd messagebird-sms-campaign
Initialize Node.js Project:
Create a package.json file to manage project dependencies and scripts:
Bash
npm init -y
Install Dependencies:
We need Express for the web server, the MessageBird SDK, the MongoDB driver, dotenv for environment variables, and ejs for templating.
Bash
npm install express messagebird mongodb dotenv ejs basic-auth # Added basic-auth here for Section 3
Install Development Dependencies:
We'll use nodemon for easier development (automatically restarts the server on file changes) and localtunnel for testing webhooks locally.
Bash
npm install --save-dev nodemon localtunnel
Create Project Structure:
Set up a basic folder structure for better organization:
Bash
mkdir views # For EJS templatestouch index.js .env .gitignore
Configure .gitignore:
Add node_modules and .env to your .gitignore file to prevent committing them to version control:
Text
# .gitignore
node_modules/
.env
*.log
Set up .env File:
Create placeholders for your MessageBird credentials, originator number, database connection string, and a basic password for the admin interface. Never commit this file to Git.
.env
# .env
MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY
MESSAGEBIRD_ORIGINATOR=YOUR_VIRTUAL_NUMBER_OR_SENDER_ID # e.g., +12005550199 or MarketingApp
MONGO_URI=mongodb://localhost:27017/sms_campaign # Or your MongoDB Atlas URI
ADMIN_PASSWORD=your_secret_password # Change this immediately! Example only.
PORT=8080 # Optional: default port
MESSAGEBIRD_API_KEY: Your Live API key from the MessageBird Dashboard.
MESSAGEBIRD_ORIGINATOR: The phone number (VMN) or alphanumeric Sender ID messages will come from. Check MessageBird's country restrictions for alphanumeric IDs.
MONGO_URI: Your MongoDB connection string.
ADMIN_PASSWORD: WARNING: This is a highly insecure example password. Change it immediately to a strong, unique password. For production, use robust authentication methods as detailed in Section 7.
PORT: The port your Express app will listen on.
Add npm Scripts:
Update the scripts section in your package.json for easier starting and development:
JSON
// package.json"scripts":{"start":"node index.js","dev":"nodemon index.js","tunnel":"lt --port 8080 --subdomain your-unique-sms-webhook"// Replace with a unique subdomain},
npm start: Runs the application normally.
npm run dev: Runs the application using nodemon for development.
npm run tunnel: Starts localtunnel to expose port 8080. Make sure to choose a unique subdomain.
2. Implement MessageBird Webhook Handler for SMS Opt-In/Opt-Out
The core logic resides in the webhook handler, which processes incoming SMS messages.
Basic Express Setup (index.js):
Start by setting up Express, loading environment variables, and initializing the MessageBird and MongoDB clients.
JavaScript
// index.jsrequire('dotenv').config(); // Load .env variables firstconst 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 3const ejs = require('ejs'); // Require EJS for Section 3const app = express();
const port = process.env.PORT || 8080;
const mongoUri = process.env.MONGO_URI;
const dbName = 'sms_campaign'; // Or extract from MONGO_URI if neededconst 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 ---asyncfunctionconnectDB() {
try {
const client = newMongoClient(mongoUri);
await client.connect();
db = client.db(dbName);
console.log(`Successfully connected to MongoDB: ${dbName}`);
// Ensure index on phone number for faster lookupsawait 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 connectedif (!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 validationif (!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 senderconst text = payload.trim().toLowerCase(); // Message contentconsole.log(`Webhook received from ${number}: "${text}"`);
const subscribersCollection = db.collection(collectionName);
try {
if (text === 'subscribe') {
// Add or update subscriber to subscribed: trueconst updateResult = await subscribersCollection.findOneAndUpdate(
{ number: number },
{ $set: { subscribed: true, updatedAt: newDate() }, $setOnInsert: { number: number, subscribedAt: newDate() } },
{ 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.');
} elseif (text === 'stop') {
// Update subscriber to subscribed: falseconst updateResult = await subscribersCollection.findOneAndUpdate(
{ number: number },
{ $set: { subscribed: false, unsubscribedAt: newDate(), updatedAt: newDate() } },
{ 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 keywordsconsole.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 5functionsendConfirmation(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 ---asyncfunctionstartServer() {
awaitconnectDB(); // 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 connectDB function establishes the connection to MongoDB and ensures a unique index on the number field for efficiency and data integrity.
The POST /webhook route handles incoming messages.
It extracts the sender's number (originator) and the message text (payload), assuming a specific payload structure that should be verified against current MessageBird documentation.
It converts the text to lowercase for case-insensitive keyword matching (subscribe, stop).
subscribe logic: Uses findOneAndUpdate with upsert: true. If the number exists, it sets subscribed: true. If not, it inserts a new document with the number, subscribed: true, and timestamps.
stop logic: Uses findOneAndUpdate (no upsert). If the number exists (regardless of current subscribed status), it attempts to set subscribed: false and records unsubscribedAt. The confirmation is sent if the user was found in the database, even if they were already unsubscribed.
A confirmation SMS is sent using the sendConfirmation helper function after processing the keyword. Consider replacing this with the retry version from Section 5.
Basic error handling and logging are included.
Crucially, the webhook responds with a 200 OK status to MessageBird to acknowledge receipt. Failure to respond quickly can cause MessageBird to retry the webhook.
3. Build Bulk SMS Broadcasting Interface with MessageBird API
Now, let's create a simple, password-protected web page for administrators to send messages.
Install basic-auth: (Already done in Section 1, Step 3 if followed sequentially)
If you haven't installed it yet:
Bash
npm install basic-auth
Create EJS Template (views/admin.ejs):
This file will contain the HTML form for sending messages.
Implement Admin Routes (index.js):
Add the authentication middleware and the GET/POST routes for the admin interface within your index.js file.
JavaScript
// index.js (add these parts or ensure they exist)// --- Basic Authentication Middleware ---constcheckAuth = (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 successfulreturnnext();
};
// --- 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 sendinglet messageInfo = null; // To pass feedback to the templatetry {
// Find active subscribersconst activeSubscribers = await subscribersCollection.find({ subscribed: true }).project({ number: 1, _id: 0 }).toArray();
subscriberCount = activeSubscribers.length; // Update countif (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 messagereturn 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 MessageBirdtry {
const response = awaitnewPromise((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 renderingtry {
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 checkAuth middleware intercepts requests to protected routes (/admin, /send). It checks Authorization headers and compares the provided password against ADMIN_PASSWORD from .env. Crucially, the code uses a simple !== comparison which is vulnerable to timing attacks; Section 7 discusses secure alternatives. If auth fails, it sends a 401 Unauthorized.
The GET /admin route (protected by checkAuth) fetches the count of active subscribers and renders the admin.ejs template.
The POST /send route (also protected) receives the message body.
It fetches active subscribers (subscribed: true) from MongoDB.
It implements batching, splitting recipients into chunks of 50 for separate MessageBird API calls.
It uses Promises for cleaner handling of asynchronous MessageBird calls within the loop.
Basic error handling is included for the sending process.
After sending, it re-renders the admin.ejs page with a success/error message and the updated subscriber count.
4. Configure MessageBird Virtual Mobile Number and Webhook Flow
This section details how to get your MessageBird credentials and configure the necessary components in their dashboard.
Get MessageBird API Key:
Log in to your MessageBird Dashboard (or Bird dashboard after February 2024 rebrand).
Navigate to the Developers section -> API access tab.
Copy the Live API key (create one if needed).
Paste this key into your .env file as MESSAGEBIRD_API_KEY. Keep this key secret.
Note: MessageBird rebranded as "Bird" in February 2024 with 90% price reductions. Existing MessageBird API continues to be supported.
Purchase a Virtual Mobile Number (VMN) for SMS:
Go to the Numbers section. Click "Buy a number".
Select country (US recommended for marketing campaigns), ensure SMS capability is checked, choose a number, and purchase.
Copy the purchased number in E.164 format (e.g., +12005550199) and paste into .env as MESSAGEBIRD_ORIGINATOR.
Marketing Compliance: Purchased numbers are required for two-way SMS. Alphanumeric sender IDs don't support inbound messages (STOP/SUBSCRIBE keywords won't work).
Configure MessageBird Flow for Inbound SMS Webhooks:
This connects your VMN to your application's webhook endpoint for automated subscription handling.
Go to the Numbers section, find your VMN.
Click the "Add flow" icon or go to the "Flows" tab for the number.
Click "Create Flow" -> "Create Custom Flow".
Trigger: Choose SMS.
Name: Give it a descriptive name (e.g., "SMS Campaign Webhook").
Steps:
The SMS trigger step (linked to your number) should be present.
Click + below the SMS step. Choose "Forward to URL".
Method: Select POST.
URL: Enter the public URL of your webhook endpoint.
For Development: Start localtunnel (npm run tunnel). Copy the https://your-unique-sms-webhook.loca.lt URL and append /webhook. Full URL: https://your-unique-sms-webhook.loca.lt/webhook. Remember localtunnel is not for production.
For Production: Use your deployed application's public domain/IP address followed by /webhook (e.g., https://yourdomain.com/webhook). Must be HTTPS.
Click Save.
(Optional but Recommended) Add Contact Step: Click +before "Forward to URL". Choose "Add Contact". Configure as needed.
Publish: Click "Publish changes" to activate the flow.
Your flow should essentially be: SMS (Trigger) -> [Optional Add Contact] -> Forward to URL (POST to your webhook).
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...catch around asynchronous operations.
Log errors clearly with context.
Respond appropriately to HTTP requests (500 for server errors, 400 for bad input). Avoid leaking sensitive details.
In /webhook, always try to send 200 OK to MessageBird upon receiving the request, even if internal processing fails (log the failure). Send 400 for malformed requests.
Logging:
Use console.log/console.error for simplicity here. For production, use structured loggers like Winston or Pino.
Log key events: Server start, DB connection, webhook receipt, subscribe/unsubscribe actions, admin sends, MessageBird API results, errors.
(See original article section for full Winston example config)
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).
Action Required: Replace the original sendConfirmation function in index.js with the improved sendConfirmationWithRetry version below for production reliability.
JavaScript
// index.js - Replace the original sendConfirmation with this:asyncfunctionsendConfirmationWithRetry(recipient, messageBody, maxRetries = 3, attempt = 1) {
const params = {
originator: process.env.MESSAGEBIRD_ORIGINATOR,
recipients: [recipient],
body: messageBody,
};
try {
const response = awaitnewPromise((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...`);
awaitnewPromise(resolve =>setTimeout(resolve, delay));
// Recursive call for retryreturnsendConfirmationWithRetry(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
}
}
}
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
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
Frequently asked questions
This article provides a step-by-step guide to build an SMS marketing app with Node.js, Express, and MessageBird. The app allows administrators to send bulk SMS messages to subscribed users via a simple, password-protected web interface. It uses the MessageBird API to handle sending the messages and MongoDB to manage the subscriber list.
The MessageBird API is a core component of the SMS campaign application, used for sending and receiving SMS messages. It manages virtual mobile numbers (VMNs), configures webhooks to receive incoming messages, and sends outgoing confirmation and campaign messages to subscribers.
MongoDB is a NoSQL database used to store subscriber information, including phone numbers and subscription status. The app uses the official MongoDB Node.js driver to interact with the database, ensuring efficient storage and retrieval of subscriber data.
Localtunnel is a development tool used to expose your local server to the internet for testing MessageBird webhooks during development. However, it's not secure or stable enough for production use and should only be used in a development environment.
Yes, you can use an alphanumeric sender ID for outgoing messages with MessageBird, but you should check for any country-specific restrictions. Incoming messages will still be directed to your purchased Virtual Mobile Number (VMN).
Set up a webhook by creating a custom flow in your MessageBird dashboard. The flow should be triggered by incoming SMS messages, optionally add contacts, and forward the message content via a POST request to your Node.js application's /webhook endpoint.
The keywords 'SUBSCRIBE' and 'STOP' are used to manage user subscriptions. When a user texts 'SUBSCRIBE' to your VMN, they are added to the subscriber list. 'STOP' unsubscribes them.
The MessageBird webhook sends data to your /webhook endpoint as a JSON payload. The example code provided in the article assumes it contains the sender's number ('originator') and message ('payload'), but you should verify this against the current MessageBird documentation.
You need Node.js, npm, a MessageBird account with a purchased Virtual Mobile Number (VMN), and basic familiarity with JavaScript, Node.js, and REST APIs. A MongoDB instance is also required.
The article recommends using try...catch blocks around asynchronous operations and implementing retry mechanisms with exponential backoff for critical API calls, such as sending confirmations and campaign messages.
Node.js version 18 or later is recommended for consistency with the deployment example provided in the article.
The example uses basic authentication, but it's crucial to replace the simple password comparison with a secure, constant-time comparison function to prevent timing attacks. More robust authentication methods are essential for production environments.
It's crucial to respond to MessageBird webhooks with a 200 OK status as quickly as possible, even if internal processing fails. Failure to do so can lead to MessageBird retrying the webhook, potentially causing duplicate actions.
Express.js acts as the web server framework, handling routing and HTTP requests for both the webhook endpoint (receiving SMS messages) and the admin interface (sending campaigns).
The article demonstrates basic logging using console.log and console.error, but for production, it recommends using structured loggers like Winston or Pino to log key events and errors for improved monitoring and debugging.
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:
SUBSCRIBEandSTOPkeywords for user opt-in/opt-outProblem 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:
mongodbNode.js driverdotenv: Load environment variables from.envintoprocess.envfor secure credential managementbasic-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:
(Note: The ASCII diagram above illustrates the basic flow. A graphical diagram might offer better clarity and maintainability.)
Prerequisites:
curlfor testing API endpointsFinal 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:
mkdir messagebird-sms-campaign cd messagebird-sms-campaignInitialize Node.js Project: Create a
package.jsonfile to manage project dependencies and scripts:Install Dependencies: We need Express for the web server, the MessageBird SDK, the MongoDB driver,
dotenvfor environment variables, andejsfor templating.npm install express messagebird mongodb dotenv ejs basic-auth # Added basic-auth here for Section 3Install Development Dependencies: We'll use
nodemonfor easier development (automatically restarts the server on file changes) andlocaltunnelfor testing webhooks locally.Create Project Structure: Set up a basic folder structure for better organization:
mkdir views # For EJS templates touch index.js .env .gitignoreConfigure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing them to version control: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.MESSAGEBIRD_API_KEY: Your Live API key from the MessageBird Dashboard.MESSAGEBIRD_ORIGINATOR: The phone number (VMN) or alphanumeric Sender ID messages will come from. Check MessageBird's country restrictions for alphanumeric IDs.MONGO_URI: Your MongoDB connection string.ADMIN_PASSWORD: WARNING: This is a highly insecure example password. Change it immediately to a strong, unique password. For production, use robust authentication methods as detailed in Section 7.PORT: The port your Express app will listen on.Add
npmScripts: Update thescriptssection in yourpackage.jsonfor easier starting and development:// 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.// 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 testsExplanation:
connectDBfunction establishes the connection to MongoDB and ensures a unique index on thenumberfield for efficiency and data integrity.POST /webhookroute handles incoming messages.originator) and the message text (payload), assuming a specific payload structure that should be verified against current MessageBird documentation.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.sendConfirmationhelper function after processing the keyword. Consider replacing this with the retry version from Section 5.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:Create EJS Template (
views/admin.ejs): This file will contain the HTML form for sending messages.<!-- 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.// 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 earlierExplanation:
basic-auth.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.GET /adminroute (protected bycheckAuth) fetches the count of active subscribers and renders theadmin.ejstemplate.POST /sendroute (also protected) receives the message body.subscribed: true) from MongoDB.admin.ejspage with a success/error message and the updated subscriber count.4. Configure MessageBird Virtual Mobile Number and Webhook Flow
This section details how to get your MessageBird credentials and configure the necessary components in their dashboard.
Get MessageBird API Key:
.envfile asMESSAGEBIRD_API_KEY. Keep this key secret.Purchase a Virtual Mobile Number (VMN) for SMS:
+12005550199) and paste into.envasMESSAGEBIRD_ORIGINATOR.Configure MessageBird Flow for Inbound SMS Webhooks: This connects your VMN to your application's webhook endpoint for automated subscription handling.
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./webhook(e.g.,https://yourdomain.com/webhook). Must be HTTPS.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..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:
try...catcharound asynchronous operations.500for server errors,400for bad input). Avoid leaking sensitive details./webhook, always try to send200 OKto MessageBird upon receiving the request, even if internal processing fails (log the failure). Send400for malformed requests.Logging:
console.log/console.errorfor simplicity here. For production, use structured loggers like Winston or Pino.winston):// 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)
Retry Mechanisms for Reliable SMS Delivery:
sendConfirmationfunction inindex.jswith the improvedsendConfirmationWithRetryversion below for production reliability.// 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
MongoDB for SMS Applications
Production Deployment Resources
Frequently Asked Questions
How do I test MessageBird webhooks locally?
Use
localtunnelorngrokto expose your local development server to the internet. Runnpm run tunnelto 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: