Stay informed about your SMS message delivery in real-time. This guide walks you through building a robust Node.js application using the Express framework to send SMS messages via MessageBird and process delivery status updates using webhooks (callbacks).
We'll build a system that not only sends SMS but also listens for status updates from MessageBird – such as delivered
, sent
, failed
– and updates our application's internal state accordingly. This provides crucial visibility into message delivery success and enables automated actions based on status changes.
Key Features We'll Implement:
- Sending SMS messages using the MessageBird Node.js SDK.
- Storing message details, including the MessageBird message ID.
- Setting up a webhook endpoint in Express to receive delivery status updates from MessageBird.
- Processing incoming status updates to track message delivery.
- Securely handling API keys using environment variables.
- Basic error handling and logging.
Technology Stack:
- Node.js: JavaScript runtime environment.
- Express: Minimalist web framework for Node.js.
- MessageBird: Communications Platform as a Service (CPaaS) for sending SMS and receiving status updates.
- dotenv: Module to load environment variables from a
.env
file. - nodemon (Development): Utility to automatically restart the node application when file changes are detected.
- ngrok (Development): Tool to expose local servers to the internet for webhook testing.
System Architecture:
+---------------------+ +---------------------+ +----------------+
| Your Node.js App |------->| MessageBird API |------->| User's Phone |
| (Express Server) | | (Send SMS) | | (Receives SMS) |
+---------------------+ +---------------------+ +----------------+
^ |
| (Webhook POST Request) | (Delivery Status Update)
| v
+---------------------+ +---------------------+
| Webhook Handler |<-------| MessageBird Platform|
| (/messagebird-status| | (Tracks Status) |
+---------------------+ +---------------------+
Prerequisites:
- Node.js and npm (or yarn) installed. Install Node.js and npm
- A MessageBird account. Sign up for MessageBird
- Basic understanding of JavaScript_ Node.js_ and REST APIs.
- A tool to make HTTP requests (like
curl
or Postman).
Expected Outcome:
By the end of this guide_ you will have a running Node.js Express application capable of sending SMS messages via MessageBird and accurately tracking their delivery status through webhooks. You'll also have a foundational understanding for building more complex notification systems.
GitHub Repository:
The complete working code for this tutorial should be placed in a version control repository_ for example_ on GitHub.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
1. Create Project Directory:
Open your terminal or command prompt and create a new directory for the project_ then navigate into it:
mkdir nodejs-messagebird-status
cd nodejs-messagebird-status
2. Initialize npm:
Initialize the project using npm. This creates a package.json
file to manage dependencies and project metadata.
npm init -y
(The -y
flag accepts the default settings)
3. Install Dependencies:
We need Express for our web server_ the MessageBird SDK_ dotenv for environment variables_ and nodemon for development convenience.
npm install express messagebird dotenv
npm install --save-dev nodemon
express
: The web framework.messagebird
: The official Node.js SDK for interacting with the MessageBird API.dotenv
: Loads environment variables from a.env
file intoprocess.env
. Essential for keeping API keys out of source code.nodemon
: Monitors for changes in your source and automatically restarts the server (used only during development).
4. Configure package.json
Scripts:
Open your package.json
file and add a start
and dev
script to the scripts
section. This makes running the application easier.
// package.json
{
"name": "nodejs-messagebird-status"_
"version": "1.0.0"_
"description": "Node.js app for MessageBird SMS delivery status"_
"main": "index.js"_
"scripts": {
"start": "node index.js"_
"dev": "nodemon index.js"_
"test": "echo \"Error: no test specified\" && exit 1"
}_
"keywords": [
"messagebird"_
"sms"_
"delivery"_
"status"_
"webhook"_
"node"_
"express"
]_
"author": "Your Name"_
"license": "ISC"_
"dependencies": {
"dotenv": "^16.4.5"_
"express": "^4.19.2"_
"messagebird": "^4.0.1"
}_
"devDependencies": {
"nodemon": "^3.1.0"
}
}
(Note: Dependency versions might differ based on when you install them.)
5. Create Core Files:
Create the main application file and the environment configuration file.
touch index.js .env .gitignore
index.js
: This will contain our main application logic (Express server_ routes_ MessageBird integration)..env
: This file will store sensitive information like your MessageBird API key. Never commit this file to version control..gitignore
: Specifies intentionally untracked files that Git should ignore.
6. Configure .gitignore
:
Add node_modules
and .env
to your .gitignore
file to prevent them from being committed to Git.
# .gitignore
node_modules/
.env
npm-debug.log
*.log
7. Basic Express Server Setup:
Add the following initial code to index.js
to set up a basic Express server.
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const app = express();
const port = process.env.PORT || 3000; // Use port from env or default to 3000
// Middleware to parse JSON and URL-encoded request bodies
// MessageBird webhooks might send urlencoded data_ so we need both
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Simple route for testing the server
app.get('/'_ (req_ res) => {
res.send('MessageBird Delivery Status App is running!');
});
// Start the server
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Explanation:
require('dotenv').config();
: Loads variables from the.env
file as early as possible.require('express')
: Imports the Express framework.express()
: Creates an Express application instance.process.env.PORT || 3000
: Sets the port, allowing configuration via environment variables (common in deployment).app.use(express.json())
/app.use(express.urlencoded(...))
: Middleware essential for parsing incoming request bodies, particularly JSON and form data which webhooks often use.app.get('/')
: Defines a simple route for the root URL (/
) to confirm the server is running.app.listen()
: Starts the server and makes it listen for incoming requests on the specified port.
You can now run npm run dev
in your terminal. You should see Server listening on port 3000
. Visiting http://localhost:3000
in your browser should show "MessageBird Delivery Status App is running!". Press CTRL+C
to stop the server for now.
2. MessageBird Configuration
Before sending messages or receiving status updates, we need to configure the MessageBird SDK with our API key.
1. Obtain MessageBird API Key:
- Log in to your MessageBird Dashboard.
- Navigate to the Developers section in the left-hand sidebar.
- Go to the API access tab.
- You will see Live API Key and Test API Key.
- Live Key: Used for sending real messages to actual phone numbers (costs apply).
- Test Key: Used for testing API integration without sending actual messages or incurring costs. API calls will succeed, and you'll see simulated responses and webhook payloads in logs, but no SMS will be delivered.
- Copy your Live API Key. For initial development and testing the webhook mechanism without sending real SMS, you might start with the Test Key. However, to verify actual delivery, you'll need the Live Key.
2. Store API Key in .env
:
Open the .env
file you created earlier and add your MessageBird API key.
# .env
# Replace YOUR_API_KEY with the key copied from the MessageBird Dashboard
MESSAGEBIRD_API_KEY=YOUR_API_KEY
# Optional: Define a default Sender ID (Originator)
# This can be an alphanumeric string (max 11 chars, check country restrictions)
# or a purchased Virtual Mobile Number / Verified Caller ID in E.164 format (e.g., +12025550181)
MESSAGEBIRD_ORIGINATOR=YourAppName
Why .env
? Hardcoding credentials directly into your source code (index.js
) is a significant security risk. Using environment variables keeps secrets separate from code, making your application more secure and configurable across different environments (development, staging, production). The dotenv
package makes this easy for local development. In production, you'll typically set these environment variables directly on your hosting platform.
3. Initialize MessageBird SDK:
In index.js
, initialize the MessageBird SDK using the API key loaded from the environment variables.
// index.js
require('dotenv').config();
const express = require('express');
const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY); // Initialize SDK
const app = express();
const port = process.env.PORT || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// --- Add SDK initialization above other routes ---
// In-memory storage for message statuses (DEMO ONLY)
// WARNING: This is for demonstration purposes ONLY and is unsuitable for production.
// If the server restarts, all message data will be lost.
// A persistent database (like MongoDB, PostgreSQL, Redis) is MANDATORY for real applications.
// See Section 6 for a persistent storage example using MongoDB.
const messageStore = {}; // { messageId: { status: 'pending', recipient: '+1...' } }
app.get('/', (req, res) => {
res.send('MessageBird Delivery Status App is running!');
});
// --- Routes for sending and receiving status updates will go here ---
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Explanation:
require('messagebird')(process.env.MESSAGEBIRD_API_KEY)
: Imports the SDK and immediately initializes it by passing the API key retrieved from the environment variables. Thismessagebird
object will be used for all API interactions.messageStore
: We add a simple JavaScript object to act as an in-memory database. Crucially, this is only for demonstration. A real application must use a persistent database (like PostgreSQL, MongoDB, Redis, etc.) as detailed later in Section 6. Relying onmessageStore
will lead to data loss on server restarts.
3. Sending SMS Messages
Now, let's create an endpoint to trigger sending an SMS message. We'll store the message ID returned by MessageBird, which is crucial for correlating delivery status updates later.
1. Create Sending Route:
Add a new POST
route in index.js
, for example, /send-sms
. This route will expect a recipient phone number and a message body.
// index.js
// ... (keep existing code: requires, initialization, app setup, messageStore)
// POST route to send an SMS
app.post('/send-sms', (req, res) => {
const { recipient, message } = req.body;
// Basic validation
if (!recipient || !message) {
return res.status(400).json({ error: 'Recipient and message body are required.' });
}
// Message parameters
const params = {
// originator: The sender ID displayed. Types include:
// - Alphanumeric (e.g., "YourApp", max 11 chars, limited country support, no replies)
// - Short Code (e.g., 12345, requires pre-approval, good for high volume)
// - Virtual Mobile Number (VMN) / Long Code (e.g., +12025550181, purchased from MessageBird, allows replies, often best deliverability)
// Ensure compliance with MessageBird originator rules & country restrictions: https://support.messagebird.com/hc/en-us/sections/201753929-Originators-Sender-ID-
originator: process.env.MESSAGEBIRD_ORIGINATOR || 'MessageBird', // Use configured originator or default
recipients: [recipient], // Array of recipients in E.164 format (+countrycode...)
body: message,
// Optional: Request a status report webhook for this message
// reportUrl: `YOUR_PUBLIC_WEBHOOK_URL/messagebird-status` // We'll set this up later
};
console.log(`Attempting to send SMS to ${recipient} from ${params.originator}`);
// Send the message using the MessageBird SDK
messagebird.messages.create(params, (err, response) => {
if (err) {
// Handle API errors
console.error('MessageBird API Error:', err);
return res.status(500).json({ error: 'Failed to send SMS via MessageBird.', details: err });
}
// Message submitted successfully to MessageBird
console.log('MessageBird API Response:', response);
// IMPORTANT: Store the message ID to track status later
const messageId = response.id;
// Storing in our temporary in-memory store (replace with DB in production!)
messageStore[messageId] = {
recipient: recipient,
status: 'sent_to_provider', // Initial status
last_update: new Date().toISOString(),
details: response // Store the initial response if needed
};
console.log(`Message submitted with ID: ${messageId}. Stored initial status.`);
console.log('Current messageStore:', messageStore);
// Respond to the client
res.status(201).json({
message: 'SMS submitted successfully.',
messageId: messageId,
details: response
});
});
});
// ... (keep existing code: root route, app.listen)
Explanation:
- Route Definition:
app.post('/send-sms', ...)
defines an endpoint that listens for POST requests at/send-sms
. - Request Body Parsing: It expects a JSON body with
recipient
(e.g.,+12025550123
) andmessage
properties. Basic validation checks if these exist. - Parameters (
params
):originator
: The sender ID. Added comments explain common types (alphanumeric, shortcode, VMN) and link to rules. Using a purchased MessageBird number (VMN) is often the most reliable for global deliverability and two-way communication.recipients
: An array containing the recipient phone number(s) in E.164 format.body
: The text content of the SMS.reportUrl
(Commented Out): This tells MessageBird where to send delivery status updates (webhooks) for this specific message. We haven't set up our public webhook URL yet. Alternatively, configure a default webhook URL account-wide in the MessageBird dashboard (see Section 4).
messagebird.messages.create(params, callback)
: Sends the SMS. It's asynchronous.- Callback Function:
(err, response)
executes upon completion.err
holds errors,response
holds success details including the crucialresponse.id
. - Storing the Message ID:
messageStore[messageId] = { ... }
saves themessageId
and initial status. This linkage is essential for processing callbacks. Remember this uses the temporary in-memory store. - Response: Responds with
201 Created
and themessageId
.
Testing the Sending Endpoint:
-
Ensure your server is running (
npm run dev
). -
Use
curl
or Postman to send a POST request:curl -X POST http://localhost:3000/send-sms \ -H "Content-Type: application/json" \ -d '{ "recipient": "+1xxxxxxxxxx", "message": "Hello from Node.js & MessageBird! (Test 1)" }'
(Replace
+1xxxxxxxxxx
with a real phone number in E.164 format if using your Live API Key. If using the Test Key, the number format still matters, but no SMS will be sent). -
Check Console Logs: See logs for the attempt, the MessageBird response (with
id
), and the updatedmessageStore
. -
Check API Response: The client receives JSON like:
{ "message": "SMS submitted successfully.", "messageId": "mbid_a1b2c3d4e5f6a7b8c9d0e1f2", "details": { ... } }
(Example
messageId
) -
(Live Key Only): Check your phone for the SMS message.
4. Setting up Delivery Status Webhooks
To receive delivery status updates (Delivery Reports or DLRs), MessageBird needs a publicly accessible URL (a webhook endpoint) in our application to send HTTP POST requests to.
Development Setup using ngrok
:
During development, your localhost
server isn't accessible from the internet. We need a tool like ngrok
to create a secure tunnel.
-
Install
ngrok
: Download and install ngrok from ngrok.com. Follow their setup instructions. -
Start
ngrok
: If your Node.js app runs on port 3000, open a new terminal window and run:ngrok http 3000
-
Get Public URL:
ngrok
will display output like:Session Status online [...] Forwarding https://<random-string>.ngrok.io -> http://localhost:3000
Copy the
https://<random-string>.ngrok.io
URL. This is your temporary public URL. Note: Free ngrok accounts generate a new random URL every time you restart ngrok.ngrok
is primarily a development tool. Production webhooks require a stable, publicly hosted URL on your deployed server.
Configuring the Webhook in MessageBird:
You have two main options:
Option A: Account-Wide Webhook (Recommended for Simplicity)
Set a default URL where MessageBird sends all delivery status updates.
- Go to your MessageBird Dashboard.
- Navigate to Flow Builder -> Create new flow -> Webhook trigger (or find Status Reports under Developers -> API Settings if available).
- Give your flow a name (e.g., ""Global Delivery Status Handler"").
- Add a ""Forward to URL"" step.
- Configure the step:
- URL: Paste your
ngrok
HTTPS URL + webhook path (e.g.,/messagebird-status
). Example:https://<random-string>.ngrok.io/messagebird-status
- Method:
POST
- Data Format: Choose
Form Data (application/x-www-form-urlencoded)
orJSON
. Our Express setup handles both.
- URL: Paste your
- Save and Publish the flow.
Option B: Per-Message Webhook (reportUrl
)
Specify the reportUrl
in the messagebird.messages.create
call.
-
Uncomment and update the
reportUrl
line in the/send-sms
route inindex.js
:// index.js - inside app.post('/send-sms', ...) // ... const params = { // ... other params // Set the specific webhook URL for this message's status reports reportUrl: `https://<random-string>.ngrok.io/messagebird-status` // Replace with YOUR ngrok URL + path }; // ...
-
Ensure
ngrok
is running with the correct public URL.
Recommendation: Start with Option A (Account-Wide Webhook) for simplicity.
5. Implementing the Webhook Handler
Create the Express route to listen for and process incoming POST requests from MessageBird.
1. Create Webhook Route:
Add the following POST
route to index.js
, matching the path (/messagebird-status
) configured in ngrok
and MessageBird.
// index.js
// ... (keep existing code: requires, initialization, app setup, messageStore, /send-sms route)
// POST route to handle incoming MessageBird status webhooks
app.post('/messagebird-status', (req, res) => {
// Log the incoming request for debugging
console.log('Received MessageBird Status Webhook:');
console.log('Headers:', req.headers);
console.log('Body:', req.body);
// Extract key information (verify field names from your logs)
// Common fields: id, status, statusDatetime, recipient, error
const messageId = req.body.id;
const status = req.body.status; // e.g., 'delivered', 'sent', 'expired', 'delivery_failed'
const statusDatetime = req.body.statusDatetime; // Timestamp string
const recipient = req.body.recipient;
const errorCode = req.body.error; // Error code if failed (might be null/absent)
if (!messageId || !status) {
console.warn('Webhook received without message ID or status. Body:', req.body);
// Acknowledge receipt even if invalid to prevent MessageBird retries
return res.status(200).send('OK');
}
console.log(`Processing status update for Message ID: ${messageId}, New Status: ${status}`);
// Find the original message in our store (DEMO ONLY - Use DB in production)
if (messageStore[messageId]) {
// Update the status
messageStore[messageId].status = status;
messageStore[messageId].last_update = statusDatetime || new Date().toISOString();
if (errorCode !== undefined) { // Check if error field exists
messageStore[messageId].errorCode = errorCode;
}
console.log(`Updated status for ${messageId} to ${status}.`);
console.log('Current messageStore:', messageStore);
// --- Add further application logic here based on status ---
if (status === 'delivered') {
console.log(`Message ${messageId} successfully delivered to ${recipient}.`);
// Add logic for successful delivery (e.g., update order status)
} else if (status === 'delivery_failed') {
console.error(`Message ${messageId} failed for recipient ${recipient}. Error Code: ${errorCode}`);
// Add logic for failed delivery (e.g., alert, retry strategy)
}
} else {
// Status update for an unknown message ID (e.g., due to server restart wiping in-memory store)
// This highlights the critical need for a persistent database!
console.warn(`Received status for unknown Message ID: ${messageId}. This message was likely lost due to using the in-memory store.`);
}
// IMPORTANT: Respond to MessageBird with 2xx status (e.g., 200 OK)
// This acknowledges receipt. Failure may cause MessageBird to retry.
res.status(200).send('OK');
});
// ... (keep existing code: root route, app.listen)
Explanation:
- Route Definition:
app.post('/messagebird-status', ...)
defines the endpoint. - Logging: Logs incoming
headers
andbody
to help verify the payload structure. Verify field names (id
,status
, etc.) against your logs. MessageBird payload structure can vary slightly. - Data Extraction: Extracts key fields from
req.body
. - Validation: Checks if
messageId
andstatus
exist. Responds200 OK
even on failure to prevent retries. - Lookup & Update:
if (messageStore[messageId])
: Looks up the ID in the temporary in-memory store. This check will fail for messages sent before a server restart if you haven't implemented persistent storage yet.- If found, updates status, timestamp, and error code.
- Further Logic: Placeholder comments indicate where to add application-specific actions based on the status.
- Handling Unknown IDs: Logs a warning if the ID isn't found, emphasizing the limitation of the demo store.
- Acknowledgement Response:
res.status(200).send('OK');
is critical to acknowledge receipt to MessageBird and prevent unnecessary retries.
Testing the Webhook:
- Ensure Node.js (
npm run dev
) andngrok
are running. - Ensure the webhook URL is correctly configured in MessageBird (either account-wide or via
reportUrl
). - Send a new message using the
/send-sms
endpoint with your Live API Key. - Watch Node.js console logs: See logs from
/send-sms
, then shortly after, logs from/messagebird-status
as updates arrive (e.g.,sent
,delivered
). Check the loggedreq.body
. - Watch
ngrok
console: See incomingPOST /messagebird-status
requests. - Check the
messageStore
output in logs to see status updates (if the message was sent after the server started).
6. Storing and Tracking Status (Beyond In-Memory)
The in-memory messageStore
is unsuitable for production due to data loss on restarts. You need a persistent database like MongoDB, PostgreSQL, etc.
Here's an outline using MongoDB.
1. Install MongoDB Driver:
npm install mongodb
2. Update .env
:
Add your MongoDB connection string.
# .env
MESSAGEBIRD_API_KEY=YOUR_API_KEY
MESSAGEBIRD_ORIGINATOR=YourAppName
# Replace with your actual MongoDB connection string (local or cloud)
MONGODB_URI=mongodb://localhost:27017/messagebird_status_db
3. Database Connection Module (Recommended):
Create config/database.js
:
// config/database.js
const { MongoClient } = require('mongodb');
const uri = process.env.MONGODB_URI;
if (!uri) {
throw new Error('Missing MONGODB_URI environment variable');
}
const client = new MongoClient(uri);
let db;
async function connectDB() {
if (db) return db; // Return existing connection
try {
await client.connect();
console.log('Connected successfully to MongoDB');
db = client.db(); // Use DB name from URI or specify client.db(""dbName"")
return db;
} catch (err) {
console.error('Failed to connect to MongoDB', err);
process.exit(1); // Exit if DB connection fails
}
}
module.exports = { connectDB };
4. Refactor index.js
to Use MongoDB:
Modify routes to interact with MongoDB. Ensure you have a config
directory for the database.js
file.
// index.js
require('dotenv').config();
const express = require('express');
const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY);
const { connectDB } = require('./config/database'); // Import DB connector
const app = express();
const port = process.env.PORT || 3000;
let db; // Database connection variable
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Connect to Database on startup
connectDB()
.then(database => {
db = database; // Assign DB connection
// Start server only after DB connection is successful
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
console.log(`Connected to database: ${db.databaseName}`);
});
})
.catch(err => {
console.error(""Failed to start server due to DB connection error:"", err);
process.exit(1);
});
app.get('/', (req, res) => {
res.send('MessageBird Delivery Status App is running!');
});
// POST route to send an SMS (Updated for MongoDB)
app.post('/send-sms', async (req, res) => { // Async handler
if (!db) {
console.error(""Send SMS request received but DB not connected"");
return res.status(503).json({ error: 'Service temporarily unavailable (DB connection)' });
}
const { recipient, message } = req.body;
if (!recipient || !message) {
return res.status(400).json({ error: 'Recipient and message body are required.' });
}
// Ensure your public URL is correctly set here or configured account-wide
const reportUrl = process.env.WEBHOOK_BASE_URL ? `${process.env.WEBHOOK_BASE_URL}/messagebird-status` : undefined;
// Example: WEBHOOK_BASE_URL=https://<your-random-string>.ngrok.io in .env
const params = {
originator: process.env.MESSAGEBIRD_ORIGINATOR || 'MessageBird',
recipients: [recipient],
body: message,
reportUrl: reportUrl // Use dynamic URL or omit if using account-wide webhook
};
console.log(`Attempting to send SMS to ${recipient}`);
try {
// Use async/await with a Promise wrapper for the callback-based SDK method
const response = await new Promise((resolve, reject) => {
messagebird.messages.create(params, (err, resp) => {
if (err) return reject(err);
resolve(resp);
});
});
console.log('MessageBird API Response:', response);
const messageId = response.id;
// Insert initial record into MongoDB
const messageCollection = db.collection('messages');
const insertResult = await messageCollection.insertOne({
_id: messageId, // Use MessageBird ID as primary key
recipient: recipient,
body: message,
status: 'sent_to_provider', // Initial status based on successful API call
createdAt: new Date(),
lastUpdatedAt: new Date(),
providerResponse: response, // Store initial MessageBird response
statusHistory: [ { status: 'sent_to_provider', timestamp: new Date() } ] // Start history
});
console.log(`Message record inserted into DB for ID: ${messageId}, MongoDB ID: ${insertResult.insertedId}`);
res.status(201).json({
message: 'SMS submitted successfully.',
messageId: messageId,
details: response
});
} catch (err) {
console.error('Error sending SMS or saving to DB:', err);
// Check if it's a MessageBird specific error structure
if (err && err.errors) {
return res.status(400).json({ error: 'MessageBird API request error.', details: err.errors });
}
// Generic server error
return res.status(500).json({ error: 'Failed to process SMS request.', details: err.message || 'Unknown error' });
}
});
// POST route to handle incoming MessageBird status webhooks (Updated for MongoDB)
app.post('/messagebird-status', async (req, res) => { // Async handler
if (!db) {
console.error(""Webhook received but DB not connected"");
// Still acknowledge to MessageBird to prevent retries for this specific issue
return res.status(200).send('OK (Internal Server Error - DB)');
}
console.log('Received MessageBird Status Webhook Body:', req.body);
const messageId = req.body.id;
const status = req.body.status;
// Parse timestamp safely, default to now if invalid/missing
const statusDatetime = req.body.statusDatetime ? new Date(req.body.statusDatetime) : new Date();
const errorCode = req.body.error || null; // Use null if error field is absent
if (!messageId || !status) {
console.warn('Webhook received without message ID or status.');
return res.status(200).send('OK (Invalid Payload)'); // Acknowledge
}
console.log(`Processing DB update for Message ID: ${messageId}, New Status: ${status}`);
const messageCollection = db.collection('messages');
try {
// Find and update the message record in MongoDB
const updateResult = await messageCollection.updateOne(
{ _id: messageId }, // Find by MessageBird ID (_id)
{
$set: { // Update these fields
status: status,
lastUpdatedAt: statusDatetime,
errorCode: errorCode // Set or clear error code
},
$push: { // Add this status change to the history array
statusHistory: {
$each: [{ // Use $each to add object
status: status,
timestamp: statusDatetime,
errorCode: errorCode
}],
$sort: { timestamp: 1 } // Optional: keep history sorted
}
}
}
);
if (updateResult.matchedCount === 0) {
// Message ID from webhook not found in our DB
console.warn(`Received status for unknown Message ID (not in DB): ${messageId}. Status: ${status}`);
// Possible reasons: DB cleared, message sent before DB logging, wrong ID format.
} else if (updateResult.modifiedCount > 0) {
// Successfully updated the record
console.log(`Updated DB status for ${messageId} to ${status}.`);
// --- Add further application logic based on status update ---
// Example: Trigger notifications, update other systems, etc.
if (status === 'delivered') {
console.log(`DB confirms delivery for ${messageId}.`);
} else if (status === 'delivery_failed') {
console.error(`DB confirms failure for ${messageId}. Error: ${errorCode}`);
}
} else {
// Found the record, but nothing was changed (e.g., duplicate webhook)
console.log(`Status update for ${messageId} (${status}) received, but DB record was not modified (likely duplicate webhook or same status).`);
}
} catch (err) {
console.error(`Error updating database for Message ID ${messageId}:`, err);
// Acknowledge to MessageBird even if DB update fails internally
return res.status(200).send('OK (DB Update Error)');
}
// Acknowledge receipt to MessageBird successfully
res.status(200).send('OK');
});
// Note: Root route app.get('/') remains.
// app.listen is moved inside connectDB().then(...) block.
Schema Considerations (MongoDB):
_id
: UsemessageId
from MessageBird as the primary key.recipient
,body
: Original message details.status
: Latest known status string (e.g., 'sent_to_provider', 'sent', 'delivered', 'delivery_failed').createdAt
: Timestamp when the message record was created in your system.lastUpdatedAt
: Timestamp of the latest status update received.providerResponse
: The initial response object frommessagebird.messages.create
.errorCode
: The error code provided by MessageBird if the status isdelivery_failed
or similar.statusHistory
: An array of objects, each containing{ status, timestamp, errorCode }
, providing an audit trail of status changes.
This MongoDB setup provides persistent storage. Remember to run a MongoDB server locally or use a cloud service (like MongoDB Atlas) and configure the MONGODB_URI
in your .env
file.
7. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Production applications need robust error handling and logging.
Error Handling Strategy:
- API Call Errors (
/send-sms
):- Use
try...catch
blocks withasync/await
or check theerr
object in callbacks. - Log detailed errors, especially
err.errors
from MessageBird for validation issues. - Return appropriate HTTP status codes (
400
for client errors like missing parameters,500
for server/API errors) to the client initiating the send request. Provide informative error messages in the JSON response.
- Use
- Webhook Processing Errors (
/messagebird-status
):- Wrap database operations and subsequent application logic within
try...catch
blocks. - Crucially: Always aim to return a
200 OK
response to MessageBird quickly to acknowledge receipt, even if your internal processing fails. Log the internal error thoroughly for later investigation. Failure to acknowledge might cause MessageBird to retry sending the webhook, potentially leading to duplicate processing if not handled carefully (e.g., using the database update logic shown which prevents modification if status is unchanged). - Handle invalid or unexpected webhook payloads gracefully (log warnings, return
200 OK
).
- Wrap database operations and subsequent application logic within
- Database Errors:
- Handle database connection errors on startup (as shown with
process.exit(1)
). - Handle errors during database operations (insert, update) within the relevant routes. Log clearly and return appropriate responses (e.g.,
503 Service Unavailable
if DB is down during/send-sms
, but still200 OK
during webhook processing).
- Handle database connection errors on startup (as shown with
(Retry mechanisms are beyond the scope of this basic guide but would involve queueing failed webhook processing tasks or implementing idempotent operations.)