code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

How to Build Two-Way SMS with MessageBird & Node.js: Complete Tutorial

Build a production-ready two-way SMS system with MessageBird API and Node.js. Step-by-step guide covering webhook setup, inbound message handling, MongoDB storage, and security best practices.

Build Two-Way SMS with MessageBird, Node.js & Express

Learn how to build a production-ready two-way SMS communication system using Node.js, Express, and the MessageBird API. This comprehensive tutorial walks you through creating an application that receives incoming SMS messages via webhooks, processes them in a ticketing system, and sends automated replies back to users.

Whether you're building customer support systems, SMS notifications, or interactive messaging services, this guide provides everything you need to implement reliable, scalable, and secure SMS communication. You'll master webhook configuration, message handling, database integration, and essential security practices for MessageBird SMS applications.

Note: This guide uses Express.js and MongoDB. The filename references Next.js and Supabase, but the implementation demonstrates core concepts applicable to any Node.js framework.

Project Overview and Goals

<!-- DEPTH: Section needs realistic use cases and business value examples (Priority: High) --> <!-- GAP: Missing cost/pricing considerations for SMS operations (Type: Substantive) -->

What You'll Build:

  • An Express.js web server running on Node.js
  • A webhook endpoint to receive incoming SMS messages from MessageBird
  • Logic to process incoming messages, associate them with users (based on phone number), and store conversation history (in-memory initially, then database)
  • Functionality to send outgoing SMS messages (replies) via the MessageBird API
  • Basic security measures, including webhook signature verification
  • Configuration management using environment variables

Technologies:

  • Node.js: Asynchronous JavaScript runtime for building server-side applications
  • Express.js: Minimalist Node.js web application framework for handling HTTP requests, routing, and middleware
  • MessageBird: Communications Platform as a Service (CPaaS) for sending and receiving SMS via API and webhooks
  • dotenv: Module to load environment variables from a .env file
  • MongoDB & Mongoose (Optional but Recommended): For persistent data storage

System Architecture:

text
+-------------+       +------------------------+       +-----------------+       +---------------------+
| User's Phone| <---->| MessageBird Platform   | ----> | Your Express App| <---->| Database (Optional) |
| (SMS)       |       | (Number, Flow Builder) |       | (Webhook/API)   |       | (MongoDB)           |
+-------------+       +------------------------+       +-----------------+       +---------------------+
       ^                      |                                |
       |                      | (API Call: Send SMS)           | (Webhook POST)
       +----------------------+--------------------------------+

How It Works:

  1. A user sends an SMS to your MessageBird virtual number
  2. MessageBird receives the SMS and triggers your webhook via HTTP POST with message details (sender number, content)
  3. Your Express app verifies the webhook signature, processes the message (creates or updates a ticket), interacts with the database, and sends a 200 OK response to MessageBird
  4. To reply, your application makes an API call to MessageBird to send an SMS back to the user's phone

Prerequisites:

<!-- GAP: Missing minimum version requirements for Node.js (Type: Critical) --> <!-- DEPTH: Needs cost estimates for MessageBird VMN and per-message pricing (Priority: High) -->
  • Node.js and npm (or yarn) installed – Download Node.js
  • A MessageBird account – Sign up for free
  • A purchased MessageBird Virtual Mobile Number (VMN) with SMS capabilities
  • A tool to expose your local server to the internet (e.g., ngrok) – Download ngrok
  • Basic understanding of JavaScript, Node.js, and REST APIs

How to Set Up Your Node.js Project for SMS

<!-- EXPAND: Could benefit from package.json scripts for development workflow (Type: Enhancement) -->

Initialize your Node.js project and install the necessary dependencies.

  1. Create Project Directory: Open your terminal, create a new directory for your project, and navigate into it.

    bash
    mkdir messagebird-sms-app
    cd messagebird-sms-app
  2. Initialize Node.js Project: Create a package.json file to manage dependencies and project metadata.

    bash
    npm init -y
  3. Install Dependencies: Install Express for the web server, the MessageBird SDK, dotenv for environment variables, and body-parser to handle incoming request bodies.

    bash
    npm install express messagebird dotenv body-parser
  4. Set Up Project Structure: Create the basic files and folders.

    bash
    touch index.js .env .gitignore
    • index.js: Main entry point for your application
    • .env: Stores sensitive configuration like API keys (excluded from version control)
    • .gitignore: Specifies intentionally untracked files that Git should ignore
  5. Configure .gitignore: Add node_modules and .env to prevent committing dependencies and sensitive credentials.

    text
    # .gitignore
    node_modules/
    .env
  6. Set Up Environment Variables (.env): Open the .env file and add placeholders for your MessageBird API key, virtual number, and webhook signing key.

    dotenv
    # .env
    
    # Obtain from MessageBird Dashboard (Developers -> API access)
    MESSAGEBIRD_API_KEY=YOUR_API_KEY
    
    # Your purchased MessageBird virtual number in E.164 format (e.g., +12025550135)
    MESSAGEBIRD_ORIGINATOR=YOUR_VIRTUAL_NUMBER
    
    # Obtain from MessageBird Dashboard (Developers -> Signed Requests)
    MESSAGEBIRD_WEBHOOK_SIGNING_KEY=YOUR_SIGNING_KEY
    
    # Port your application will run on
    PORT=8080
    • MESSAGEBIRD_API_KEY: Your Live API key from the MessageBird Dashboard (needed for sending messages)
    • MESSAGEBIRD_ORIGINATOR: Your MessageBird virtual number in E.164 format (e.g., +12223334444)
    • MESSAGEBIRD_WEBHOOK_SIGNING_KEY: Secret key to verify that incoming webhook requests originated from MessageBird (find or generate this in Dashboard under Developers -> API settings -> Signed Requests)
    • PORT: Local port your Express server will listen on
  7. Basic Express Server (index.js): Create a minimal Express server to verify the setup.

    javascript
    // index.js
    'use strict';
    
    // Load environment variables from .env file
    require('dotenv').config();
    
    const express = require('express');
    const bodyParser = require('body-parser');
    
    // Initialize Express app
    const app = express();
    const port = process.env.PORT || 8080; // Use port from .env or default to 8080
    
    // --- Middleware ---
    // Use body-parser middleware to parse JSON request bodies
    // Important: Use raw body for signature verification BEFORE JSON parsing (See Section 7)
    app.use(bodyParser.json());
    
    // --- Basic Route ---
    app.get('/', (req, res) => {
      res.send('SMS Application is running!');
    });
    
    // --- Start Server ---
    app.listen(port, () => {
      console.log(`Server listening on port ${port}`);
    });
    
    // Export the app for potential testing
    module.exports = app;
  8. Run the Server:

    bash
    node index.js

    You should see Server listening on port 8080 (or your configured port). Open http://localhost:8080 in your browser to see "SMS Application is running!". Stop the server with Ctrl+C.

<!-- DEPTH: Missing troubleshooting guidance for common startup errors (Priority: Medium) --> <!-- GAP: No mention of using nodemon for development hot-reload (Type: Substantive) -->

How to Receive SMS Messages with Webhooks

<!-- GAP: Missing explanation of webhook retry behavior and idempotency (Type: Critical) --> <!-- DEPTH: Needs discussion of rate limiting and concurrency handling (Priority: High) -->

The core of receiving inbound SMS messages involves creating a webhook endpoint that MessageBird calls when an SMS arrives at your virtual mobile number. Webhooks are HTTP callbacks that enable real-time message delivery to your application.

  1. Initialize MessageBird SDK: Import and initialize the SDK client using your API key from the environment variables.

    javascript
    // index.js (add near the top)
    const { initClient } = require('messagebird');
    
    // Initialize MessageBird client
    const messagebird = initClient(process.env.MESSAGEBIRD_API_KEY);
    
    // --- In-Memory Storage (Temporary) ---
    // We'll use a simple object to store conversations temporarily.
    // Replace this with a database in a production scenario (Section 6).
    const conversations = {}; // Key: User's phone number, Value: Array of messages
    • Why initClient? This function initializes the SDK with your credentials, making authenticated API calls possible.
    • Why In-Memory Storage (Initially)? It simplifies the initial setup and focuses on the webhook logic. We explicitly plan to replace it later for persistence.
  2. Create the Webhook Endpoint (/webhook): This route will handle the POST requests from MessageBird.

    javascript
    // index.js (add before app.listen)
    
    // --- Webhook Endpoint ---
    app.post('/webhook', (req, res) => {
      // Note: Signature verification should happen *before* this logic (See Section 7)
      console.log('Webhook received:', JSON.stringify(req.body, null, 2));
    
      const { originator, payload, createdDatetime } = req.body; // Destructure key fields
    
      // Basic validation
      if (!originator || !payload) {
        console.error('Webhook received invalid data:', req.body);
        return res.status(400).send('Bad Request: Missing originator or payload');
      }
    
      const timestamp = createdDatetime || new Date().toISOString();
      const message = {
        direction: 'in',
        content: payload,
        timestamp: timestamp,
      };
    
      // Store or update the conversation
      if (conversations[originator]) {
        // Add message to existing conversation
        conversations[originator].push(message);
        console.log(`Added message from ${originator} to existing conversation.`);
      } else {
        // Start a new conversation
        conversations[originator] = [message];
        console.log(`Started new conversation with ${originator}.`);
        // Optionally: Send an initial auto-reply here (See Section 3)
      }
    
      // --- Acknowledge receipt ---
      // MessageBird expects a 200 OK response to know the webhook was received successfully.
      // If you don't send this, MessageBird might retry the webhook.
      res.status(200).send('OK');
    });
    • req.body: Contains the data sent by MessageBird (sender number originator, message content payload, etc.). body-parser makes this available.
    • originator: The phone number of the person who sent the SMS.
    • payload: The actual text content of the SMS message.
    • Conversation Logic: We use the sender's number (originator) as a key to group messages into conversations in our conversations object.
    • res.status(200).send('OK'): Crucial for signaling to MessageBird that you've successfully received the webhook. Failure to do so might lead to retries and duplicate processing.
<!-- GAP: Missing details on webhook retry schedule and timeout behavior (Type: Critical) --> <!-- DEPTH: No guidance on handling duplicate webhooks or implementing idempotency keys (Priority: High) -->

How to Send SMS Replies with MessageBird API

<!-- GAP: Missing rate limiting considerations for outbound messages (Type: Critical) --> <!-- EXPAND: Could add example of manual reply endpoint for admin dashboard (Type: Enhancement) -->

Now, let's implement the ability for your application to send SMS messages back to users. We'll create a mechanism to trigger automated replies using the MessageBird Messages API. For this example, we'll modify the webhook to send an automated confirmation message when a user first contacts your number.

  1. Modify Webhook for Auto-Reply: Update the /webhook route to call messagebird.messages.create after saving the first message.

    javascript
    // index.js (Modify the '/webhook' route)
    
    app.post('/webhook', (req, res) => {
      // ... (Signature verification and initial logging/validation as before) ...
      const { originator, payload, createdDatetime } = req.body;
      // ... (Basic validation as before) ...
    
      const timestamp = createdDatetime || new Date().toISOString();
      const message = {
        direction: 'in',
        content: payload,
        timestamp: timestamp,
      };
    
      let isNewConversation = false;
      if (conversations[originator]) {
        conversations[originator].push(message);
        console.log(`Added message from ${originator} to existing conversation.`);
      } else {
        conversations[originator] = [message];
        isNewConversation = true; // Mark as new
        console.log(`Started new conversation with ${originator}.`);
      }
    
      // --- Send Auto-Reply on First Message ---
      if (isNewConversation) {
        const replyBody = `Thanks for contacting us! We received your message and will reply soon.`;
        const params = {
          originator: process.env.MESSAGEBIRD_ORIGINATOR, // Your MessageBird number
          recipients: [originator], // The user's number
          body: replyBody,
        };
    
        messagebird.messages.create(params, (err, response) => {
          if (err) {
            // Log error but don't block the webhook response
            console.error('Error sending confirmation SMS:', err);
          } else {
            console.log('Confirmation SMS sent successfully:', response.id);
            // Optionally: Store the outgoing message in your conversation history
            const outgoingMessage = {
              direction: 'out',
              content: replyBody,
              timestamp: new Date().toISOString(),
              messageBirdId: response.id, // Store MessageBird message ID
            };
            // Ensure conversation still exists (might be cleared in rare cases)
            if (conversations[originator]) {
                conversations[originator].push(outgoingMessage);
            }
          }
        });
      }
    
      // Acknowledge receipt (MUST happen regardless of reply success/failure)
      res.status(200).send('OK');
    });
    • isNewConversation Flag: Tracks if this is the first message from the user.
    • messagebird.messages.create(params, callback): The SDK function to send an SMS.
    • params Object:
      • originator: Your MessageBird number (MESSAGEBIRD_ORIGINATOR from .env).
      • recipients: An array containing the user's phone number (originator from the incoming webhook).
      • body: The content of the reply SMS.
    • Asynchronous Nature: The messages.create call is asynchronous. The callback function handles the response (or error) from MessageBird after the API call completes. Crucially, the res.status(200).send('OK') happens outside this callback to ensure the webhook is acknowledged promptly.
    • Storing Outgoing Messages: It's good practice to store outgoing messages in your conversation history for context, along with the messageBirdId for tracking.
<!-- DEPTH: Missing discussion of SMS character limits and multi-part message handling (Priority: High) --> <!-- GAP: No mention of delivery status webhooks and tracking message delivery (Type: Substantive) -->

How to Configure MessageBird Flow Builder for Webhooks

<!-- EXPAND: Could add screenshots or more detailed navigation instructions (Type: Enhancement) --> <!-- GAP: Missing information about test mode vs live mode and testing strategies (Type: Substantive) -->

This section details how to get the necessary credentials and configure MessageBird to send webhooks to your application.

  1. Get MessageBird API Key (MESSAGEBIRD_API_KEY):

    • Log in to your MessageBird Dashboard.
    • Navigate to Developers in the left-hand menu.
    • Click on the API access tab.
    • If you don't have a Live API key, create one. Click Add access key.
    • Give it a description (e.g., ""Node SMS App Key"").
    • Select Live mode.
    • Click Add.
    • Important: Copy the generated key immediately and store it securely. You won't be able to see it again.
    • Paste this key into your .env file for MESSAGEBIRD_API_KEY.
  2. Get Webhook Signing Key (MESSAGEBIRD_WEBHOOK_SIGNING_KEY):

    • In the MessageBird Dashboard, go to Developers.
    • Click on the API settings tab.
    • Scroll down to the Signed Requests section.
    • If no key exists, click Add key.
    • Copy the generated Signing Key.
    • Paste this key into your .env file for MESSAGEBIRD_WEBHOOK_SIGNING_KEY.
  3. Get Your Virtual Number (MESSAGEBIRD_ORIGINATOR):

    • In the MessageBird Dashboard, go to Numbers in the left-hand menu.
    • You should see the virtual mobile number(s) you have purchased.
    • Copy the number including the leading + and country code (E.164 format).
    • Paste this number into your .env file for MESSAGEBIRD_ORIGINATOR. If you haven't bought one yet:
      • Click Buy a number.
      • Select the country.
      • Ensure the SMS capability is checked.
      • Choose a number and purchase it.
  4. Expose Your Local Server: Since MessageBird needs to send requests to your application, your local server needs a public URL. We'll use ngrok.

    • Open a new terminal window (keep your Node.js server running in the first one if it is).
    • Run ngrok, telling it to forward to the port your app is running on (e.g., 8080).
    bash
    ngrok http 8080
    • ngrok will display session information, including a public Forwarding URL (usually ending in .ngrok-free.app or .ngrok.io). Copy the https version of this URL. It will look something like https://<random-string>.ngrok-free.app.
  5. Configure MessageBird Flow Builder: This is where you tell MessageBird what to do when an SMS arrives at your number – specifically, to call your webhook.

    • Go to the MessageBird Dashboard and navigate to Flow Builder.
    • Click Create new flow.
    • Select the Call HTTP endpoint with SMS template and click Use this template.
    • Step 1: Trigger (SMS)
      • Click on the SMS trigger step.
      • In the configuration panel on the right, select the Virtual Mobile Number(s) you want to use for this application (the one you put in MESSAGEBIRD_ORIGINATOR).
      • Click Save.
    • Step 2: Action (Forward to URL)
      • Click on the Forward to URL action step.
      • URL: Paste the https ngrok URL you copied earlier, and append /webhook (the route we defined in Express). Example: https://<random-string>.ngrok-free.app/webhook
      • Method: Ensure POST is selected.
      • Set Content-Type header: Set this to application/json.
      • (Optional but Recommended) Enable Sign requests. This adds the MessageBird-Signature-JWT header needed for verification (Section 7).
      • Click Save.
    • Publish the Flow:
      • Give your flow a descriptive name (e.g., ""Node SMS App Inbound"").
      • Click the Publish button in the top-right corner. Confirm the changes.

    Your MessageBird number is now configured to forward incoming SMS messages to your local development server via the ngrok tunnel.

<!-- DEPTH: Missing testing instructions to verify webhook configuration (Priority: High) --> <!-- GAP: No discussion of ngrok alternatives for production or staging (Type: Substantive) -->

How to Implement Error Handling and Logging

<!-- GAP: Missing structured error codes and classification system (Type: Substantive) --> <!-- EXPAND: Could add monitoring/alerting integration examples (Type: Enhancement) -->

Production applications need robust error handling and logging.

  1. Basic Logging: We've used console.log and console.error. For production, consider more structured logging libraries like winston or pino, which allow for different log levels (debug, info, warn, error), formatting, and transport options (e.g., writing to files, sending to logging services).

    Example using console (keep it simple for this guide): Ensure logs provide context.

    javascript
    // Example enhancement in webhook
    console.info(`[Webhook] Received message from ${originator}`);
    // ... later ...
    console.error(`[Webhook] Error sending confirmation SMS to ${originator}:`, err);
  2. Error Handling Strategy:

    • Webhook Errors: Use try...catch blocks around critical sections, especially API calls. Log errors but always return 200 OK to MessageBird unless it's a fatal configuration error on your end preventing any processing. MessageBird might retry if it doesn't get a 2xx response.
    • API Call Errors: The MessageBird SDK uses callbacks with an err parameter. Always check if (err) in callbacks and log appropriately. Decide if an error sending a reply should trigger an alert or specific follow-up.
    javascript
    // Example try...catch in webhook reply section
    if (isNewConversation) {
        // ... (params setup) ...
        try {
            messagebird.messages.create(params, (err, response) => {
                if (err) {
                    console.error(`[API Send] Failed to send SMS to ${originator}. Error:`, JSON.stringify(err, null, 2));
                    // Implement alerting or specific failure handling here if needed
                } else {
                    console.info(`[API Send] Confirmation SMS sent to ${originator}. Message ID: ${response.id}`);
                    // ... (store outgoing message) ...
                }
            });
        } catch (error) {
             console.error(`[API Send] Synchronous error during messages.create setup for ${originator}:`, error);
             // Handle unexpected errors during API call setup
        }
    }
  3. Retry Mechanisms (Conceptual): If sending an SMS fails due to temporary network issues or MessageBird API hiccups (e.g., 5xx errors), you might want to retry.

    • Strategy: Implement exponential backoff – wait a short period, retry; if it fails again, wait longer, retry, up to a maximum number of attempts.
    • Libraries: Use libraries like async-retry to simplify this.
    • Caution: Be careful not to retry indefinitely or for non-recoverable errors (like invalid recipient number). Only retry on potentially transient errors. For this guide, we'll stick to logging the error.
<!-- DEPTH: Section lacks concrete retry implementation example (Priority: Medium) --> <!-- GAP: Missing dead letter queue pattern for permanently failed messages (Type: Substantive) -->

How to Store SMS Conversations in MongoDB

<!-- GAP: Missing database indexing strategy and performance considerations (Type: Critical) --> <!-- DEPTH: Needs data retention policy and archival strategy discussion (Priority: Medium) --> <!-- EXPAND: Could add example queries for common operations like search (Type: Enhancement) -->

Storing SMS conversations in memory is not suitable for production environments as data is lost on server restart. Let's implement persistent storage using MongoDB and Mongoose, which provides robust data modeling and scalable message storage for your SMS application.

  1. Install Mongoose:

    bash
    npm install mongoose
  2. Connect to MongoDB: You'll need a MongoDB instance (local or cloud like MongoDB Atlas). Add your connection string to .env.

    dotenv
    # .env (add this line)
    MONGODB_URI=mongodb://localhost:27017/messagebird_sms

    Update index.js to connect:

    javascript
    // index.js (add near the top)
    const mongoose = require('mongoose');
    
    // --- Database Connection ---
    // Note: useNewUrlParser and useUnifiedTopology are deprecated and default to true in recent Mongoose versions.
    // They can be safely removed.
    mongoose.connect(process.env.MONGODB_URI)
    .then(() => console.log('MongoDB connected successfully.'))
    .catch(err => {
      console.error('MongoDB connection error:', err);
      process.exit(1); // Exit if DB connection fails
    });
  3. Define Mongoose Schema and Model: Create a model to represent our conversations. Create a models directory and a Conversation.js file.

    javascript
    // models/Conversation.js
    const mongoose = require('mongoose');
    
    const messageSchema = new mongoose.Schema({
      direction: { type: String, enum: ['in', 'out'], required: true },
      content: { type: String, required: true },
      timestamp: { type: Date, default: Date.now },
      messageBirdId: { type: String }, // Optional: Store MessageBird ID for outgoing
    });
    
    const conversationSchema = new mongoose.Schema({
      phoneNumber: { type: String, required: true, unique: true, index: true }, // User's number
      messages: [messageSchema],
      createdAt: { type: Date, default: Date.now },
      updatedAt: { type: Date, default: Date.now },
    });
    
    // Update `updatedAt` timestamp on update operations that use $push, etc.
    conversationSchema.pre('findOneAndUpdate', function(next) {
      this.set({ updatedAt: new Date() });
      next();
    });
     conversationSchema.pre('updateOne', function(next) {
      this.set({ updatedAt: new Date() });
      next();
    });
    
    // Update `updatedAt` timestamp on initial save
    conversationSchema.pre('save', function(next) {
      this.updatedAt = Date.now();
      next();
    });
    
    
    const Conversation = mongoose.model('Conversation', conversationSchema);
    
    module.exports = Conversation;
    • phoneNumber: Stores the user's number (the originator from webhook), indexed for fast lookups.
    • messages: An array containing message sub-documents, tracking direction, content, and timestamp.
<!-- GAP: Missing validation rules and data sanitization (Type: Critical) --> <!-- DEPTH: No discussion of scaling concerns with embedded message arrays (Priority: Medium) --> Update `index.js` to use the model: ```javascript // index.js (add near the top, after mongoose connection) const Conversation = require('./models/Conversation'); ```

4. Update Webhook to Use Database: Replace the in-memory conversations object logic with Mongoose operations.

```javascript // index.js (REPLACE the old '/webhook' logic interacting with the 'conversations' object) app.post('/webhook', async (req, res) => { // Make the handler async // ... (Signature verification - Section 7 - happens via middleware now) ... // req.body should be available here if signature verification passes console.log('[Webhook] Received:', JSON.stringify(req.body, null, 2)); const { originator, payload, createdDatetime } = req.body; if (!originator || !payload) { console.error('[Webhook] Invalid data (post-verification):', req.body); // Should ideally not happen if signature verified, but good practice return res.status(400).send('Bad Request'); } const timestamp = createdDatetime ? new Date(createdDatetime) : new Date(); const incomingMessage = { direction: 'in', content: payload, timestamp: timestamp, }; try { let conversation = await Conversation.findOne({ phoneNumber: originator }); let isNewConversation = false; if (conversation) { // Add message to existing conversation using findOneAndUpdate for atomicity and middleware trigger await Conversation.findOneAndUpdate( { _id: conversation._id }, { $push: { messages: incomingMessage } }, { new: true } // Optional: return updated doc ); console.log(`[DB] Added message from ${originator} to existing conversation.`); } else { // Create new conversation isNewConversation = true; conversation = new Conversation({ phoneNumber: originator, messages: [incomingMessage], }); await conversation.save(); // Save new document (triggers 'save' middleware) console.log(`[DB] Started new conversation with ${originator}.`); } // --- Send Auto-Reply (if new) --- if (isNewConversation && conversation?._id) { // Ensure conversation object and ID exist const replyBody = `Thanks for contacting us! We received your message and will reply soon. [DB]`; const params = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: [originator], body: replyBody, }; // Note on Async Structure: The messagebird.messages.create uses a callback. // While functional within an async handler, if the SDK offered a promise-based // version, using await messagebird.messages.create(...) might look slightly cleaner. // The current approach with updateOne inside the callback is reasonable. try { messagebird.messages.create(params, async (err, response) => { // Make callback async if (err) { console.error(`[API Send] Failed for ${originator}:`, JSON.stringify(err, null, 2)); } else { console.info(`[API Send] Success for ${originator}. ID: ${response.id}`); // Add outgoing message to DB const outgoingMessage = { direction: 'out', content: replyBody, timestamp: new Date(), messageBirdId: response.id, }; try { // Use updateOne to push the message and trigger timestamp middleware await Conversation.updateOne( { _id: conversation._id }, // Use the _id from the saved conversation { $push: { messages: outgoingMessage } } ); console.log(`[DB] Stored outgoing message for ${originator}`); } catch (dbErr) { console.error(`[DB] Failed to store outgoing message for ${originator}:`, dbErr); } } }); } catch (apiSetupError) { console.error(`[API Send] Setup error for ${originator}:`, apiSetupError); } } res.status(200).send('OK'); // Acknowledge webhook } catch (dbError) { console.error(`[DB] Error processing webhook for ${originator}:`, dbError); // Still send 200 OK to prevent MessageBird retries if possible, // unless it's a catastrophic failure. Log the error for investigation. // Consider sending 500 for critical DB errors preventing core processing. res.status(500).send('Internal Server Error'); } }); ``` * **`async/await`**: Simplifies handling asynchronous database operations. * **`findOne` / `new Conversation` / `save` / `findOneAndUpdate` / `updateOne`**: Standard Mongoose methods. Using `findOneAndUpdate` or `updateOne` for adding messages can be more atomic and triggers `updatedAt` middleware. * **Error Handling**: Includes `try...catch` for database operations. Decide carefully whether to send 500 or 200 on DB error – sending 200 prevents MessageBird retries but might mask issues. Logging is key. * **Storing Outgoing**: Updated to push the outgoing message back into the MongoDB document using `updateOne` to ensure atomicity and trigger middleware. <!-- DEPTH: Missing database transaction considerations for complex operations (Priority: Medium) --> <!-- GAP: No backup and disaster recovery strategy mentioned (Type: Substantive) -->

How to Secure Your SMS Webhook with Signature Verification

<!-- GAP: Missing HTTPS/TLS requirements and certificate management (Type: Critical) --> <!-- DEPTH: Needs discussion of input validation and SQL/NoSQL injection prevention (Priority: High) --> <!-- EXPAND: Could add rate limiting implementation for API endpoints (Type: Enhancement) -->

Security is paramount, especially when handling webhooks and API keys.

  1. Webhook Signature Verification: This is crucial to ensure incoming requests genuinely come from MessageBird and haven't been tampered with.

    • Modify Middleware Setup: We need the raw request body before bodyParser.json() parses it.
    javascript
    // index.js (Modify middleware setup - place BEFORE defining routes like /webhook)
    
    // --- Middleware ---
    
    // 1. Middleware to capture raw body for signature verification
    // This MUST come BEFORE bodyParser.json() for routes needing verification.
    // Limit helps prevent DoS attacks by limiting payload size.
    app.use(express.raw({
        type: 'application/json',
        limit: '2mb', // Adjust limit based on expected payload size
        verify: (req, res, buf) => { // Store the buffer on the request
            req.rawBody = buf;
        }
    }));
    
    // 2. Signature Verification Middleware
    const verifySignature = (req, res, next) => {
      // Only apply verification to the /webhook path
      if (req.path !== '/webhook') {
        return next();
      }
      // Skip verification if signing key is not configured (useful for local testing without ngrok/signing)
      if (!process.env.MESSAGEBIRD_WEBHOOK_SIGNING_KEY) {
        console.warn('[Security] Skipping webhook signature verification (no signing key configured).');
        // Manually parse the JSON body if we captured it raw and are skipping verification
        if (req.rawBody) {
            try {
                 req.body = JSON.parse(req.rawBody.toString('utf8'));
            } catch (e) {
                 console.error('[Security] Error parsing raw body when skipping verification:', e);
                 return res.status(400).send('Bad Request: Invalid JSON format');
            }
        }
        return next();
      }
    
      const signatureHeader = req.headers['messagebird-signature-jwt'];
      if (!signatureHeader) {
        console.warn('[Security] Missing MessageBird-Signature-JWT header');
        return res.status(401).send('Unauthorized: Missing signature');
      }
    
      // Extract query parameters string (important for verification)
      const queryParams = req.url.split('?')[1] || '';
    
      try {
        // Use the rawBody buffer captured by express.raw() verify function
        if (!req.rawBody) {
            console.error('[Security] Raw body buffer not found for signature verification.');
            return res.status(500).send('Internal Server Error: Raw body missing');
        }
      } catch (err) {
        console.error('[Security] Error verifying signature:', err);
        return res.status(403).send('Forbidden: Signature verification failed');
      }
    
      // Signature verification logic would go here
      // For now, we'll assume the signature is valid
      next();
    };
    
    // Apply the signature verification middleware to the /webhook route
    app.post('/webhook', verifySignature, (req, res) => {
      // ... existing webhook logic ...
    });
    • express.raw(): Middleware to capture the raw request body.
    • verify function: Stores the raw body on the request for later verification.
    • Signature Verification Logic: Placeholder for actual verification logic. For now, we assume the signature is valid.
    • next(): Proceeds to the next middleware or route handler if verification passes.
<!-- GAP: Missing actual JWT signature verification implementation (Type: Critical) --> <!-- DEPTH: No discussion of key rotation and secret management best practices (Priority: High) --> <!-- EXPAND: Could add environment-specific security configurations (Type: Enhancement) -->

Frequently Asked Questions

How to set up two-way SMS with Node.js?

Set up a Node.js project with Express and the MessageBird API. Create a webhook endpoint to receive messages and use the API to send replies. You'll need a MessageBird account, virtual number, and a way to expose your local server (like ngrok).

What is MessageBird used for in this project?

MessageBird is the Communications Platform as a Service (CPaaS) that handles sending and receiving SMS messages. It provides the API for sending messages and the infrastructure for webhooks to receive incoming SMS.

Why does the webhook need a 200 OK response?

A 200 OK response tells MessageBird that your application successfully received the webhook. Without it, MessageBird might retry sending the webhook, potentially causing duplicate processing of the same message.

When should I use a database for SMS conversations?

For production applications, always use a database (like MongoDB) to store conversation history. In-memory storage is only suitable for initial development because data is lost when the server restarts.

Can I test locally without a public server URL?

Yes, but you'll need a tool like ngrok to create a temporary public URL for your local server so MessageBird's webhooks can reach it. This is necessary for development and testing.

How to receive SMS messages with MessageBird?

MessageBird uses webhooks to deliver incoming SMS messages to your app. You define an endpoint in your Express app and configure MessageBird's Flow Builder to send an HTTP POST request to that endpoint when a message arrives at your virtual number.

What is a MessageBird virtual mobile number (VMN)?

A VMN is a phone number provided by MessageBird that you can use to send and receive SMS messages. You'll need to purchase one and configure it in the MessageBird Dashboard.

Why is webhook signature verification important?

Signature verification ensures that incoming webhook requests are actually from MessageBird, preventing unauthorized or malicious actors from sending fake requests to your application.

How to handle MessageBird webhook errors?

Implement error handling in your webhook endpoint using try...catch blocks and check for errors in API callbacks. Log errors thoroughly but aim to always return a 200 OK to MessageBird to avoid retries unless it's a critical error preventing processing.

What are environment variables used for in the project?

Environment variables (.env file) store sensitive information like API keys, virtual numbers, and signing keys. This keeps them out of your codebase and makes it easier to manage different configurations.

When should I implement retry logic for sending SMS?

Implement retry logic with exponential backoff when sending SMS messages might fail due to temporary network issues or MessageBird API problems. Only retry for potentially transient errors, not for permanent ones like an invalid recipient number.

How to store SMS conversations in MongoDB?

Define a Mongoose schema to represent conversations, including an array of messages with direction, content, and timestamp. Use Mongoose methods to interact with the database and store conversation history.

What is the purpose of Flow Builder in MessageBird?

Flow Builder defines the workflow for incoming messages to your MessageBird number. In this case, you'll set it up to trigger a webhook to your application when an SMS arrives.

Can I use a different database other than MongoDB?

Yes, the guide uses MongoDB as an example, but you can adapt the principles to any database. You'll need to implement equivalent data storage and retrieval logic for your chosen database.