code examples

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

How to Receive SMS Messages with MessageBird Node.js Express: Two-Way SMS Tutorial

Build a two-way SMS messaging system using MessageBird API, Node.js, and Express. Complete guide covering webhook setup, inbound message handling, auto-replies, and production deployment.

Two-Way SMS Messaging with Node.js, Express, and the MessageBird API

Build a complete two-way SMS messaging system using the MessageBird API with Node.js and Express. This comprehensive tutorial shows you how to receive SMS messages, set up webhooks, implement auto-replies, and handle inbound messages securely – everything you need to create interactive SMS conversations in your applications.

You'll build a functional system that receives incoming SMS messages via webhooks and sends automated or custom replies, perfect for customer support, notifications, surveys, verification codes, or any interactive messaging scenario. The guide includes working code examples, security best practices, and production deployment considerations.

Prerequisites

Before starting, ensure you have:

  • Node.js (version 14.x or higher) and npm installed
  • Basic understanding of JavaScript, Node.js, and REST APIs
  • Familiarity with asynchronous programming (async/await or callbacks)
  • A MessageBird account (free trial available)
  • Command line/terminal access

How to Set Up Your Node.js MessageBird SMS Project

Initialize your Node.js project and install the necessary dependencies for receiving SMS messages.

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

    bash
    mkdir messagebird-two-way-sms
    cd messagebird-two-way-sms
  2. Initialize Node.js Project: Initialize the project using npm. The -y flag accepts default settings.

    bash
    npm init -y

    This creates a package.json file.

  3. Install Dependencies: Install Express, the MessageBird SDK, and dotenv for environment variable management.

    bash
    npm install express messagebird dotenv

    Package versions (as of January 2025):

    • express: ^4.18.x (stable, production-ready)
    • messagebird: ^3.8.x (official MessageBird SDK, Node.js >= 0.10 required)
    • dotenv: ^16.x.x (environment variable management)
  4. Create Project Files: Create the main application file and files for environment variables and Git ignore rules.

    bash
    touch index.js .env .gitignore

    Your initial project structure should look like this:

    text
    messagebird-two-way-sms/
    ├── node_modules/
    ├── .env
    ├── .gitignore
    ├── index.js
    ├── package-lock.json
    └── package.json
  5. Configure .gitignore: Prevent committing sensitive information and unnecessary files to version control. Add the following lines to your .gitignore file:

    Code
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Log files
    *.log
    
    # Operating system files
    .DS_Store
    Thumbs.db
  6. Prepare .env File: This file stores your sensitive credentials. Open .env and add the following placeholders. Fill these in the next section.

    Code
    # MessageBird Credentials
    MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_API_KEY
    MESSAGEBIRD_SIGNING_KEY=YOUR_MESSAGEBIRD_SIGNING_KEY
    MESSAGEBIRD_NUMBER=YOUR_MESSAGEBIRD_VIRTUAL_NUMBER
    
    # Server Configuration
    PORT=3000

    Why .env? Storing configuration and secrets in environment variables follows the best practice recommended by The Twelve-Factor App. It keeps sensitive data separate from your codebase, making your application more secure and portable across different environments (development, staging, production).

Integrating with MessageBird

Configure your MessageBird account and obtain the necessary credentials for receiving SMS messages.

1. Sign Up and Access Your Dashboard

If you haven't already, sign up or log in to the MessageBird Dashboard.

Cost considerations: MessageBird offers a free trial with test credits (typically €10-20) for new accounts. After trial credits are exhausted, add payment details. Pricing varies by country; check MessageBird pricing for specific rates.

2. Obtain Your API Key

  1. In the MessageBird Dashboard, navigate to DevelopersAPI access (or look for the API key in the top-right corner)
  2. Copy your Live API key (starts with live_)
  3. Paste it as the value for MESSAGEBIRD_API_KEY in your .env file

Security note: Never use your live API key in client-side code or commit it to version control. For testing, use the test API key, but it won't send real SMS messages.

3. Obtain Your Signing Key (for Webhook Verification)

MessageBird signs all webhook requests with HMAC-SHA256 to ensure authenticity. You need the signing key to verify these signatures.

  1. In the Dashboard, go to DevelopersSettings
  2. Find the Signing Key section
  3. Copy your signing key
  4. Paste it as the value for MESSAGEBIRD_SIGNING_KEY in your .env file

4. Purchase a Virtual Mobile Number

To receive SMS messages, you need a dedicated virtual mobile number (VMN).

  1. Navigate to NumbersBuy a number in the Dashboard
  2. Select the country where you and your customers are located
  3. Important: Ensure the SMS capability is enabled for the number
  4. Choose a number from the available selection
  5. Complete the purchase

Regional considerations:

  • Some countries have restrictions on virtual numbers for SMS (e.g., requiring business registration)
  • Two-way messaging capability varies by country; verify international 2-way messaging support
  • Number capabilities differ: local vs. toll-free, SMS vs. voice vs. both
  • Consider your target audience's location for optimal delivery rates
  1. Copy your purchased number (in E.164 format, e.g., +31612345678) and paste it as MESSAGEBIRD_NUMBER in your .env file

E.164 format: International phone number standard (e.g., +[country code][subscriber number]). Required by MessageBird API for reliable message routing. Examples: +14155551234 (US), +447700900123 (UK), +31612345678 (Netherlands).

5. Recap of .env

Your .env file should now look something like this (with your actual credentials):

Code
# MessageBird Credentials
MESSAGEBIRD_API_KEY=live_abcdef1234567890
MESSAGEBIRD_SIGNING_KEY=1a2b3c4d5e6f7g8h9i0j
MESSAGEBIRD_NUMBER=+31612345678

# Server Configuration
PORT=3000

Setting Up Webhook Infrastructure with Ngrok

Webhooks are the core mechanism for receiving inbound SMS. When someone sends a message to your MessageBird number, MessageBird makes an HTTP POST request to your webhook URL. During development, your local server isn't publicly accessible, so use ngrok to create a secure tunnel.

Why Ngrok?

Ngrok is a tunneling service that exposes your local server to the internet with a public URL. It's essential for:

  • Testing webhooks locally before deploying
  • Debugging inbound message flows in real-time
  • Avoiding complex firewall or router configurations

Install Ngrok

Option 1 – Download binary:

  1. Visit ngrok.com/download
  2. Download the appropriate version for your OS
  3. Unzip and move to your PATH (optional)

Option 2 – Using package managers:

bash
# macOS (Homebrew)
brew install ngrok

# Windows (Chocolatey)
choco install ngrok

# Linux (Snap)
snap install ngrok

Start Ngrok Tunnel

Since your Express app runs on port 3000:

bash
ngrok http 3000

Output example:

text
Session Status                online
Account                       your@email.com
Version                       3.x.x
Region                        United States (us)
Forwarding                    https://a1b2c3d4.ngrok.app -> http://localhost:3000

Important: Copy the https:// forwarding URL (e.g., https://a1b2c3d4.ngrok.app). You'll need this for MessageBird Flow Builder configuration.

Ngrok free tier limitations:

  • URL changes every time you restart ngrok
  • Sessions expire after 2 hours (on free plan)
  • Limited concurrent connections

Pro tip: For a persistent URL across sessions, consider ngrok paid plans or alternatives like localtunnel or Cloudflare Tunnel.

How to Configure MessageBird Flow Builder for Inbound SMS

MessageBird uses Flow Builder to route incoming messages to your webhook. Flows are visual workflows that connect numbers to actions.

Create Your Inbound SMS Flow

  1. Navigate to Flow Builder:

    • In the MessageBird Dashboard, go to Flow Builder in the left menu
    • Click Create new flow or select the template "Call HTTP endpoint with SMS"
    • If using the template, click "Try this flow"
  2. Configure the SMS Trigger:

    • Click on the first step labeled "SMS"
    • Select the virtual number you purchased earlier
    • This tells MessageBird which number(s) should trigger this flow
    • Click Save
  3. Configure the HTTP Endpoint (Webhook):

    • Click on the second step labeled "Forward to URL" or "HTTP Request"
    • Method: Select POST (recommended for webhook data)
    • URL: Enter your ngrok URL followed by /webhook
      • Example: https://a1b2c3d4.ngrok.app/webhook
      • The /webhook path matches the route you'll create in Express
    • Headers: (Optional) Add custom headers if needed
    • Click Save
  4. Publish Your Flow:

    • Click Publish in the top-right corner to activate the flow
    • Once published, any SMS sent to your number triggers a POST request to your webhook
  5. Name Your Flow (Optional but Recommended):

    • Click the flow name (likely "Untitled flow") to rename it
    • Use a descriptive name like "Two-Way SMS Webhook Handler"

Important webhook URL requirements for production:

  • Must be publicly accessible (no localhost)
  • Must use HTTPS (MessageBird rejects HTTP endpoints in production)
  • Should return a 2xx status code (200 or 204) to acknowledge receipt
  • Must respond within 10 seconds to avoid timeout

Implementing the Webhook: How to Receive and Process SMS Messages

Write the Node.js/Express code to handle incoming messages and send replies.

Complete Implementation Code

Open index.js and add the following code:

javascript
// index.js

// 1. Import necessary modules
const express = require('express');
const dotenv = require('dotenv');
const messagebird = require('messagebird');
const { ExpressMiddlewareVerify } = require('messagebird/lib/webhook-signature-jwt');

// 2. Load environment variables
dotenv.config();

// 3. Initialize Express app
const app = express();
const port = process.env.PORT || 3000;

// 4. Initialize MessageBird SDK
const mb = messagebird(process.env.MESSAGEBIRD_API_KEY);

// 5. Webhook signature verification middleware
// This verifies that requests to /webhook actually come from MessageBird
const verifyWebhook = new ExpressMiddlewareVerify(process.env.MESSAGEBIRD_SIGNING_KEY);

// 6. Middleware for parsing request bodies
// IMPORTANT: For webhook verification, raw body must be available
// Use express.raw() BEFORE applying verification middleware
app.use('/webhook', express.raw({ type: '*/*' }));
app.use(express.json()); // For other routes
app.use(express.urlencoded({ extended: true }));

// 7. Health check endpoint
app.get('/health', (req, res) => {
    res.status(200).json({ status: 'UP', service: 'MessageBird Two-Way SMS' });
});

// 8. WEBHOOK ENDPOINT - Receives inbound SMS messages
app.post('/webhook', verifyWebhook, (req, res) => {
    console.log('Received webhook from MessageBird');

    // Parse the form-encoded body (MessageBird sends data as form fields)
    // When using express.raw(), body is a Buffer, so parse it
    const bodyString = req.body.toString('utf-8');
    const params = new URLSearchParams(bodyString);

    // Extract key fields from webhook payload
    const originator = params.get('originator'); // Sender's phone number
    const recipient = params.get('recipient');   // Your MessageBird number
    const payload = params.get('payload');       // Message content
    const messageId = params.get('id');          // MessageBird message ID
    const createdDatetime = params.get('createdDatetime');

    console.log(`Inbound SMS from ${originator}: "${payload}"`);

    // Validate required fields
    if (!originator || !payload) {
        console.error('Missing required fields in webhook payload');
        return res.status(400).send('Invalid webhook payload');
    }

    // Process the message (your business logic goes here)
    processInboundMessage(originator, payload, messageId);

    // ALWAYS return 200 OK to acknowledge receipt
    // MessageBird retries if you return an error status
    res.status(200).send('OK');
});

// 9. SEND SMS ENDPOINT - API endpoint to send outbound messages
app.post('/send-sms', async (req, res) => {
    console.log('Received send request:', req.body);

    const { recipient, message } = req.body;

    // Input validation
    if (!recipient || !message) {
        return res.status(400).json({
            success: false,
            error: 'Missing required fields: recipient and message'
        });
    }

    // Validate E.164 format (basic check)
    if (!recipient.match(/^\+[1-9]\d{1,14}$/)) {
        return res.status(400).json({
            success: false,
            error: 'Recipient must be in E.164 format (e.g., +14155551234)'
        });
    }

    try {
        await sendSMS(recipient, message);
        res.status(200).json({
            success: true,
            message: 'SMS sent successfully'
        });
    } catch (error) {
        console.error('Error sending SMS:', error);
        res.status(500).json({
            success: false,
            error: error.message || 'Failed to send SMS'
        });
    }
});

// 10. HELPER FUNCTION - Process inbound messages and send auto-replies
function processInboundMessage(originator, payload, messageId) {
    // Example: Simple auto-reply logic
    // In production, replace this with your business logic:
    // - Database lookups
    // - Intent classification
    // - Integration with CRM/ticketing systems
    // - Natural language processing

    const lowerPayload = payload.toLowerCase().trim();
    let replyMessage = '';

    // Example conversation flows
    if (lowerPayload.includes('hello') || lowerPayload.includes('hi')) {
        replyMessage = 'Hello! Thanks for contacting us. How can we help you today?';
    } else if (lowerPayload.includes('help')) {
        replyMessage = 'We're here to help! Text INFO for information, SUPPORT for customer support, or STOP to unsubscribe.';
    } else if (lowerPayload.includes('info')) {
        replyMessage = 'Visit our website at example.com or call us at 1-800-EXAMPLE for more information.';
    } else if (lowerPayload.includes('support')) {
        replyMessage = 'We've created a support ticket for you. Our team will respond within 24 hours. Reference ID: ' + messageId.substring(0, 8);
    } else if (lowerPayload.includes('stop') || lowerPayload.includes('unsubscribe')) {
        replyMessage = 'You have been unsubscribed. Reply START to subscribe again.';
        // In production: Update database to mark user as unsubscribed
    } else {
        replyMessage = 'Thank you for your message. A team member will respond shortly. For immediate assistance, text HELP.';
    }

    // Send the reply
    sendSMS(originator, replyMessage)
        .then(() => console.log(`Auto-reply sent to ${originator}`))
        .catch(err => console.error('Failed to send auto-reply:', err));
}

// 11. HELPER FUNCTION - Send SMS via MessageBird API
function sendSMS(recipient, message) {
    return new Promise((resolve, reject) => {
        const params = {
            originator: process.env.MESSAGEBIRD_NUMBER,
            recipients: [recipient],
            body: message
        };

        console.log(`Sending SMS to ${recipient}: "${message.substring(0, 50)}..."`);

        mb.messages.create(params, (err, response) => {
            if (err) {
                console.error('MessageBird API error:', err);
                return reject(err);
            }
            console.log('SMS sent successfully. Message ID:', response.id);
            resolve(response);
        });
    });
}

// 12. Start the server
app.listen(port, () => {
    console.log(`✓ MessageBird Two-Way SMS server running on http://localhost:${port}`);
    console.log(`✓ Webhook endpoint: POST /webhook`);
    console.log(`✓ Send SMS endpoint: POST /send-sms`);
    console.log(`✓ Health check: GET /health`);
    console.log('\nWaiting for inbound messages...');
});

// 13. Graceful shutdown
process.on('SIGINT', () => {
    console.log('\nShutting down gracefully...');
    process.exit(0);
});

Code Explanation

Key Components:

  1. Webhook Signature Verification (lines 18-19): The ExpressMiddlewareVerify middleware from the MessageBird SDK validates that webhook requests are authentic and haven't been tampered with. This prevents malicious actors from sending fake webhook requests.

  2. Raw Body Middleware (line 22): Webhook verification requires access to the raw request body. Use express.raw() specifically for the /webhook route before verification.

  3. Webhook Handler (lines 29-58):

    • Receives POST requests from MessageBird when SMS arrives
    • Extracts originator (sender's phone) and payload (message text)
    • Processes the message and triggers auto-reply logic
    • Returns 200 OK to acknowledge receipt

    MessageBird webhook payload fields:

    • originator: Sender's phone number (E.164 format)
    • recipient: Your MessageBird number
    • payload: Message content (text)
    • id: Unique message identifier
    • createdDatetime: Timestamp (ISO 8601 format)
    • Additional fields: datacoding, mclass, validity, typeDetails, etc.
  4. Auto-Reply Logic (lines 69-107): Simple keyword-based responses. In production, replace with:

    • Database-driven conversation flows
    • AI/NLP intent recognition
    • CRM integration for ticket creation
    • User authentication and session management
  5. Send SMS Helper (lines 110-131): Wraps the MessageBird SDK's callback-based API in a Promise for easier async/await usage.

Error Handling & Logging

Built-in Error Handling

The code includes basic error handling, but production applications need more robust logging and error management.

Common MessageBird API Errors

Error CodeDescriptionHTTP StatusResolution
2Request not allowed (incorrect access_key)401Verify MESSAGEBIRD_API_KEY in .env
9Missing required parameter400Check request payload includes originator, recipients, body
20Insufficient balance402Add credits to your MessageBird account
21Invalid phone number format400Ensure recipient is in E.164 format
25Number not owned by account403Use a number you've purchased in MessageBird Dashboard
101Rate limit exceeded429Implement exponential backoff and reduce request frequency

Source: MessageBird API Error Codes

Enhanced Error Handling Example

Add this improved error handling to the sendSMS function:

javascript
function sendSMS(recipient, message) {
    return new Promise((resolve, reject) => {
        const params = {
            originator: process.env.MESSAGEBIRD_NUMBER,
            recipients: [recipient],
            body: message
        };

        mb.messages.create(params, (err, response) => {
            if (err) {
                // Parse MessageBird error structure
                const mbError = err.errors?.[0];
                const errorCode = mbError?.code;
                const errorDesc = mbError?.description;

                console.error('MessageBird API error:', {
                    code: errorCode,
                    description: errorDesc,
                    parameter: mbError?.parameter
                });

                // Specific error handling
                if (errorCode === 2) {
                    return reject(new Error('Authentication failed. Check your API key.'));
                } else if (errorCode === 20) {
                    return reject(new Error('Insufficient balance. Add credits to your account.'));
                } else if (errorCode === 21) {
                    return reject(new Error(`Invalid phone number: ${recipient}`));
                } else if (errorCode === 101) {
                    return reject(new Error('Rate limit exceeded. Retry later.'));
                }

                return reject(new Error(errorDesc || 'Unknown MessageBird error'));
            }
            resolve(response);
        });
    });
}

Production Logging with Winston

For production applications, replace console.log with a structured logging library like Winston:

bash
npm install winston
javascript
const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' }),
        new winston.transports.Console({
            format: winston.format.simple()
        })
    ]
});

// Usage
logger.info('Inbound SMS received', { originator, payload });
logger.error('Failed to send SMS', { error: err.message, recipient });

Webhook Retry Logic

MessageBird automatically retries failed webhook deliveries:

  • Retries up to 3 times with exponential backoff
  • Retries occur if your endpoint returns 5xx status or times out
  • Do not retry on 4xx errors (client errors)
  • Ensure idempotency: handle duplicate webhook deliveries gracefully by tracking messageId

Security Features for SMS Webhooks

1. Webhook Signature Verification

Critical security measure: Always verify webhook signatures to prevent spoofing attacks.

The code already implements this using MessageBird's ExpressMiddlewareVerify:

javascript
const { ExpressMiddlewareVerify } = require('messagebird/lib/webhook-signature-jwt');
const verifyWebhook = new ExpressMiddlewareVerify(process.env.MESSAGEBIRD_SIGNING_KEY);

app.post('/webhook', verifyWebhook, (req, res) => {
    // Only executes if signature is valid
});

How it works:

  • MessageBird sends a MessageBird-Signature-JWT header with each webhook
  • The JWT is signed with HMAC-SHA256 using your signing key
  • The SDK middleware validates the signature and timestamp claims
  • Invalid signatures result in 401 Unauthorized response

Manual verification (advanced):

javascript
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

function verifyWebhookManually(req, signingKey) {
    const signature = req.headers['messagebird-signature-jwt'];

    if (!signature) {
        throw new Error('Missing signature header');
    }

    try {
        // Decode JWT without verification first to inspect claims
        const decoded = jwt.decode(signature, { complete: true });

        // Verify signature
        const verified = jwt.verify(signature, signingKey, {
            algorithms: ['HS256']
        });

        // Check timestamp to prevent replay attacks (within 5 minutes)
        const now = Math.floor(Date.now() / 1000);
        if (verified.nbf && verified.nbf > now + 300) {
            throw new Error('Token not yet valid');
        }

        // Verify request body hash
        const bodyHash = crypto.createHash('sha256')
            .update(req.body)
            .digest('hex');

        if (verified.body_hash && verified.body_hash !== bodyHash) {
            throw new Error('Body hash mismatch');
        }

        return true;
    } catch (err) {
        console.error('Webhook verification failed:', err.message);
        throw err;
    }
}

Source: MessageBird Webhook Signature Verification

2. Input Validation and Sanitization

Validate all user inputs to prevent injection attacks and ensure data integrity:

bash
npm install joi
javascript
const Joi = require('joi');

// Phone number validation schema
const smsSchema = Joi.object({
    recipient: Joi.string()
        .pattern(/^\+[1-9]\d{1,14}$/)
        .required()
        .messages({
            'string.pattern.base': 'Recipient must be in E.164 format (e.g., +14155551234)'
        }),
    message: Joi.string()
        .min(1)
        .max(1600) // SMS concatenation limit
        .required()
        .messages({
            'string.max': 'Message exceeds maximum length of 1600 characters'
        })
});

app.post('/send-sms', async (req, res) => {
    // Validate request body
    const { error, value } = smsSchema.validate(req.body);

    if (error) {
        return res.status(400).json({
            success: false,
            error: error.details[0].message
        });
    }

    // Proceed with validated data
    const { recipient, message } = value;
    // ... send SMS
});

3. Rate Limiting

Protect your API from abuse and DDoS attacks using express-rate-limit:

bash
npm install express-rate-limit
javascript
const rateLimit = require('express-rate-limit');

// Rate limiter for outbound SMS endpoint
const smsLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 50, // Limit each IP to 50 requests per window
    message: 'Too many SMS requests from this IP, try again later',
    standardHeaders: true,
    legacyHeaders: false,
});

// Apply to specific routes
app.use('/send-sms', smsLimiter);

// Separate limiter for webhooks (more permissive)
const webhookLimiter = rateLimit({
    windowMs: 60 * 1000, // 1 minute
    max: 100, // Allow up to 100 webhooks per minute
    skipSuccessfulRequests: true, // Don't count successful requests
});

app.use('/webhook', webhookLimiter);

4. API Authentication

The webhook endpoint should only accept requests from MessageBird (handled by signature verification). For your /send-sms endpoint, implement authentication:

Option A – API Key Authentication:

javascript
function requireApiKey(req, res, next) {
    const apiKey = req.headers['x-api-key'];

    if (!apiKey || apiKey !== process.env.APP_API_KEY) {
        return res.status(401).json({
            success: false,
            error: 'Invalid or missing API key'
        });
    }

    next();
}

app.post('/send-sms', requireApiKey, async (req, res) => {
    // ... handle request
});

Option B – JWT Authentication:

bash
npm install jsonwebtoken
javascript
const jwt = require('jsonwebtoken');

function requireJWT(req, res, next) {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Missing or invalid authorization header' });
    }

    const token = authHeader.substring(7);

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded; // Attach user info to request
        next();
    } catch (err) {
        return res.status(403).json({ error: 'Invalid or expired token' });
    }
}

app.post('/send-sms', requireJWT, async (req, res) => {
    // Access authenticated user: req.user
});

5. HTTPS in Production

Critical: Always use HTTPS in production. MessageBird requires HTTPS for webhook URLs.

  • Use Let's Encrypt for free SSL certificates
  • Most cloud platforms (Heroku, AWS, Vercel) provide HTTPS automatically
  • For custom deployments, use Nginx or Caddy as a reverse proxy

6. Environment Variable Security

Never commit .env to version control. In production:

  • Use platform-specific secret management (AWS Secrets Manager, Heroku Config Vars, Azure Key Vault)
  • Rotate API keys and signing keys periodically
  • Use separate keys for development, staging, and production

OWASP Top 10 Considerations for SMS Applications

  1. Injection Attacks: Validate and sanitize all inputs (phone numbers, message content)
  2. Broken Authentication: Implement strong API authentication (JWT, API keys)
  3. Sensitive Data Exposure: Never log full message content or phone numbers in production
  4. XML External Entities (XXE): Not applicable (using JSON)
  5. Broken Access Control: Verify user permissions before sending SMS on their behalf
  6. Security Misconfiguration: Use environment variables, disable debug mode in production
  7. Cross-Site Scripting (XSS): Sanitize user inputs if displaying in web UI
  8. Insecure Deserialization: Use trusted libraries, validate all parsed data
  9. Using Components with Known Vulnerabilities: Keep dependencies updated (npm audit)
  10. Insufficient Logging & Monitoring: Implement structured logging, monitor for suspicious activity

Reference: OWASP Top 10

How to Test Your SMS Webhook Integration

1. Start the Application

Ensure your .env file is correctly populated with MessageBird credentials.

bash
node index.js

Expected output:

text
✓ MessageBird Two-Way SMS server running on http://localhost:3000
✓ Webhook endpoint: POST /webhook
✓ Send SMS endpoint: POST /send-sms
✓ Health check: GET /health

Waiting for inbound messages...

2. Start Ngrok Tunnel

In a separate terminal window, start ngrok:

bash
ngrok http 3000

Copy the HTTPS forwarding URL (e.g., https://a1b2c3d4.ngrok.app).

3. Update MessageBird Flow Builder

  1. Go to your Flow Builder flow in the MessageBird Dashboard
  2. Edit the "Forward to URL" step
  3. Update the URL to your new ngrok URL + /webhook
  4. Example: https://a1b2c3d4.ngrok.app/webhook
  5. Click Save and Publish

4. Test Inbound SMS (Two-Way Messaging)

Send a test message:

  1. Use your mobile phone to send an SMS to your MessageBird virtual number
  2. Try different keywords: "hello", "help", "info", "support"

Expected behavior:

  • Your server logs show: Inbound SMS from +1234567890: "hello"
  • You receive an auto-reply SMS on your phone within 5-10 seconds
  • Check ngrok's web inspector at http://127.0.0.1:4040 to see webhook requests

Troubleshooting inbound messages:

  • No webhook received: Check Flow Builder configuration, ensure flow is published
  • 401 Unauthorized: Signing key mismatch; verify MESSAGEBIRD_SIGNING_KEY in .env
  • Delayed delivery: Normal latency is 2-10 seconds; longer delays may indicate network issues
  • No auto-reply: Check server logs for errors in sendSMS() function

5. Test Outbound SMS via API

Use curl or an API client (Postman, Insomnia) to send a message:

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -d '{
           "recipient": "+1234567890",
           "message": "Hello from MessageBird two-way SMS!"
         }'

Expected response:

json
{
  "success": true,
  "message": "SMS sent successfully"
}

Check your phone: You should receive the SMS within 5-30 seconds.

6. Test Webhook Signature Verification

Simulate invalid webhook (should be rejected):

bash
curl -X POST https://a1b2c3d4.ngrok.app/webhook \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "originator=%2B1234567890&payload=test&recipient=%2B31612345678"

Expected: 401 Unauthorized (missing or invalid signature)

7. Test Error Scenarios

Invalid phone number format:

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -d '{"recipient": "1234567890", "message": "test"}'

Expected: 400 Bad Request

Missing required field:

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -d '{"recipient": "+1234567890"}'

Expected: 400 Bad Request

8. Automated Testing with Jest

For production applications, implement automated tests:

bash
npm install --save-dev jest supertest

Example test suite (index.test.js):

javascript
const request = require('supertest');
const app = require('./index'); // Export app from index.js

describe('Two-Way SMS API', () => {
    describe('GET /health', () => {
        it('should return 200 OK', async () => {
            const res = await request(app).get('/health');
            expect(res.statusCode).toBe(200);
            expect(res.body).toHaveProperty('status', 'UP');
        });
    });

    describe('POST /send-sms', () => {
        it('should reject invalid phone number', async () => {
            const res = await request(app)
                .post('/send-sms')
                .send({ recipient: '1234567890', message: 'test' });

            expect(res.statusCode).toBe(400);
            expect(res.body).toHaveProperty('error');
        });

        it('should reject missing message', async () => {
            const res = await request(app)
                .post('/send-sms')
                .send({ recipient: '+1234567890' });

            expect(res.statusCode).toBe(400);
        });
    });
});

Run tests:

bash
npm test

9. Webhook Testing Tools

Ngrok Inspector: Visit http://127.0.0.1:4040 to inspect all webhook requests in real-time, including headers, body, and response.

Webhook.site: For testing without running your server:

  1. Visit webhook.site
  2. Copy the unique URL
  3. Configure it in Flow Builder temporarily
  4. Send a test SMS
  5. View the webhook payload structure

Common Issues and Troubleshooting

Common Issues and Solutions

IssueCauseSolution
Webhook not receiving messagesFlow not published or incorrect URLVerify Flow Builder configuration, ensure URL ends with /webhook, check ngrok is running
401 Unauthorized on webhookInvalid or missing signing keyVerify MESSAGEBIRD_SIGNING_KEY in .env matches Dashboard value
Signature verification failsUsing express.json() before verificationUse express.raw() for webhook route (see code example)
MessageBird API error code 2Invalid API keyCheck MESSAGEBIRD_API_KEY in .env, ensure using live_ key
MessageBird API error code 20Insufficient account balanceAdd credits to your MessageBird account
MessageBird API error code 21Invalid phone number formatEnsure recipient is in E.164 format (+[country][number])
MessageBird API error code 25Originator number not ownedUse a number purchased in your MessageBird account
No auto-reply receivedError in sendSMS() functionCheck server logs for errors, verify MESSAGEBIRD_NUMBER in .env
Ngrok URL expiredFree ngrok sessions expire after 2 hoursRestart ngrok, update Flow Builder URL
Message delayed or not deliveredNetwork latency or carrier issuesNormal delay: 2-10 seconds. Check MessageBird Dashboard logs
Cannot install dependenciesNode.js version too oldUpgrade to Node.js 14.x or higher

MessageBird Dashboard Logs

For detailed debugging:

  1. Go to LogsSMS Logs in MessageBird Dashboard
  2. View delivery status, timestamps, and error messages
  3. Filter by number, date range, or status (sent, delivered, failed)

Webhook-Specific Troubleshooting

Issue: Webhook signature verification fails intermittently

Causes:

  • Request body modified by middleware before verification
  • Clock skew between your server and MessageBird (>5 minutes)
  • Using wrong signing key (test vs. live)

Solutions:

javascript
// Ensure raw body is preserved for verification
app.use('/webhook', express.raw({ type: '*/*' }));

// Check server time synchronization
console.log('Server time:', new Date().toISOString());

// Use correct signing key (live for production)
const verifyWebhook = new ExpressMiddlewareVerify(
    process.env.MESSAGEBIRD_SIGNING_KEY
);

Issue: Duplicate webhook deliveries

Cause: MessageBird retries if your server doesn't respond with 200 OK quickly enough (<10 seconds)

Solution: Implement idempotency by tracking processed message IDs:

javascript
const processedMessages = new Set(); // In production, use Redis/database

app.post('/webhook', verifyWebhook, (req, res) => {
    const params = new URLSearchParams(req.body.toString('utf-8'));
    const messageId = params.get('id');

    // Check if already processed
    if (processedMessages.has(messageId)) {
        console.log(`Duplicate webhook for message ${messageId}, ignoring`);
        return res.status(200).send('OK');
    }

    // Mark as processed
    processedMessages.add(messageId);

    // Process message...
    // Return 200 OK immediately
    res.status(200).send('OK');
});

Production Deployment Guide

Environment Variables in Production

Never commit .env files. Use platform-specific secret management:

Heroku:

bash
heroku config:set MESSAGEBIRD_API_KEY=live_abc123
heroku config:set MESSAGEBIRD_SIGNING_KEY=xyz789
heroku config:set MESSAGEBIRD_NUMBER=+31612345678

AWS (Elastic Beanstalk):

  • Use AWS Systems Manager Parameter Store or Secrets Manager
  • Configure in EB console under "Software" → "Environment properties"

Vercel/Netlify:

  • Add environment variables in project settings dashboard
  • Mark sensitive variables as "secret"

Docker:

bash
docker run -e MESSAGEBIRD_API_KEY=live_abc123 \
           -e MESSAGEBIRD_SIGNING_KEY=xyz789 \
           -e MESSAGEBIRD_NUMBER=+31612345678 \
           your-image

Webhook URL Requirements in Production

  1. Must use HTTPS: MessageBird rejects HTTP webhooks in production
  2. Must be publicly accessible: No localhost, private IPs, or firewalled endpoints
  3. Must respond within 10 seconds: Use async processing for long-running tasks
  4. Should be reliable: Use load balancers, health checks, auto-scaling

Example production webhook URL:

  • https://api.yourdomain.com/webhook
  • https://sms.example.com/messagebird/inbound
  • http://api.yourdomain.com/webhook (not HTTPS)
  • http://localhost:3000/webhook (not public)
  • https://192.168.1.100/webhook (private IP)

Production Dependencies

Install only production dependencies in deployment:

bash
npm ci --omit=dev

Or specify in package.json:

json
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "messagebird": "^3.8.0",
    "dotenv": "^16.0.3"
  },
  "devDependencies": {
    "nodemon": "^2.0.22",
    "jest": "^29.5.0",
    "supertest": "^6.3.3"
  }
}

Process Management with PM2

Use PM2 to keep your application running and restart on crashes:

bash
npm install -g pm2

# Start application
pm2 start index.js --name "messagebird-sms"

# View logs
pm2 logs messagebird-sms

# Restart on file changes (development)
pm2 start index.js --name "messagebird-sms" --watch

# Save process list
pm2 save

# Auto-start on system boot
pm2 startup

PM2 ecosystem config (ecosystem.config.js):

javascript
module.exports = {
    apps: [{
        name: 'messagebird-sms',
        script: './index.js',
        instances: 2, // Use 2 CPU cores
        exec_mode: 'cluster',
        env: {
            NODE_ENV: 'production',
            PORT: 3000
        },
        error_file: './logs/err.log',
        out_file: './logs/out.log',
        log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
    }]
};

Start with:

bash
pm2 start ecosystem.config.js

Platform-Specific Deployment Examples

Heroku:

  1. Create Procfile:
web: node index.js
  1. Deploy:
bash
heroku create your-app-name
git push heroku main
heroku config:set MESSAGEBIRD_API_KEY=live_abc123
heroku open
  1. Update Flow Builder webhook URL to Heroku app URL

AWS Lambda (Serverless):

Requires adaptation for serverless architecture. Use AWS API Gateway + Lambda:

javascript
// lambda-handler.js
const serverless = require('serverless-http');
const app = require('./index'); // Export app from index.js

module.exports.handler = serverless(app);

DigitalOcean App Platform:

  1. Create app.yaml:
yaml
name: messagebird-sms
services:
  - name: web
    github:
      repo: your-username/your-repo
      branch: main
    run_command: node index.js
    envs:
      - key: MESSAGEBIRD_API_KEY
        scope: RUN_TIME
        type: SECRET
    http_port: 3000
  1. Deploy via DigitalOcean dashboard or CLI

CI/CD Pipeline Example (GitHub Actions)

Create .github/workflows/deploy.yml:

yaml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to Heroku
        uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "your-app-name"
          heroku_email: "your@email.com"

HTTPS Configuration

Option 1 – Let's Encrypt (free) with Certbot:

bash
# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d yourdomain.com

# Auto-renewal (runs twice daily)
sudo systemctl enable certbot.timer

Option 2 – Cloudflare (free) as reverse proxy:

  1. Point your domain to Cloudflare nameservers
  2. Enable "Full (strict)" SSL/TLS mode
  3. Configure origin server certificate
  4. Cloudflare handles SSL termination

Option 3 – AWS Certificate Manager (free for AWS resources):

  • Use with Application Load Balancer (ALB)
  • Automatic certificate renewal
  • Integrates with Route 53 for DNS validation

Firewall and Network Security

Open required ports:

bash
# Allow HTTPS (443)
sudo ufw allow 443/tcp

# Allow HTTP (80) for Let's Encrypt renewal only
sudo ufw allow 80/tcp

Restrict webhook endpoint access (optional):

Whitelist MessageBird IP ranges (contact MessageBird support for current ranges):

javascript
const MESSAGEBIRD_IP_RANGES = [
    '52.210.180.0/24', // Example – verify with MessageBird
    '34.240.0.0/16'    // Example – verify with MessageBird
];

function isMessageBirdIP(ip) {
    // Implement IP range checking using ipaddr.js or similar
    return MESSAGEBIRD_IP_RANGES.some(range =>
        ipInRange(ip, range)
    );
}

app.post('/webhook', (req, res, next) => {
    const clientIP = req.ip || req.connection.remoteAddress;
    if (!isMessageBirdIP(clientIP)) {
        return res.status(403).send('Forbidden');
    }
    next();
}, verifyWebhook, (req, res) => {
    // Handle webhook
});

Conclusion

You have successfully built a complete two-way SMS messaging system using Node.js, Express, and the MessageBird API. This guide covered:

Project setup with proper dependency management ✅ MessageBird integration with API keys and virtual numbers ✅ Webhook infrastructure using ngrok for local development ✅ Inbound message handling with signature verification ✅ Outbound SMS sending with error handling ✅ Auto-reply logic for interactive conversations ✅ Security best practices including authentication and rate limiting ✅ Production deployment strategies across multiple platforms

This foundation enables you to build sophisticated SMS applications including customer support systems, appointment reminders, marketing campaigns, two-factor authentication, surveys, and more.

Next Steps

Enhance functionality:

  • Add database integration (MongoDB, PostgreSQL) to store conversation history
  • Implement user session management for multi-step conversations
  • Integrate AI/NLP (Dialogflow, OpenAI) for intelligent auto-replies
  • Build a web dashboard to view and manage conversations
  • Add media support (MMS) for sending images and files
  • Implement conversation analytics and reporting

Improve reliability:

  • Set up monitoring and alerting (Datadog, New Relic, Sentry)
  • Implement message queuing (Redis, RabbitMQ) for high-volume scenarios
  • Add database-backed idempotency tracking
  • Configure automatic failover and disaster recovery

Expand integrations:

  • Connect to CRM systems (Salesforce, HubSpot)
  • Integrate with ticketing systems (Zendesk, Freshdesk)
  • Add payment processing for transactional SMS
  • Implement multi-channel support (WhatsApp, Telegram) via MessageBird Conversations API

Additional Resources

Explore these additional MessageBird Node.js tutorials:

GitHub Repository

A complete working example of the code developed in this guide can be found on GitHub:

https://github.com/messagebird/sms-customer-support-guide

This repository includes additional examples for customer support ticketing systems, conversation history storage, and advanced auto-reply logic.

Frequently Asked Questions

How to send SMS with Node.js and Express

Use the Vonage Messages API and the Vonage Node.js Server SDK. Create an Express.js endpoint that takes the recipient's number and message, then uses the SDK to send the SMS via Vonage.

What is the Vonage Messages API?

The Vonage Messages API is a unified platform for sending messages across multiple channels, such as SMS, MMS, WhatsApp, and more. It simplifies the integration of messaging into applications.

Why use dotenv in a Node.js project?

Dotenv loads environment variables from a .env file into process.env. This best practice helps keep sensitive credentials like API keys out of your source code, enhancing security.

When should I use ngrok with Vonage?

Ngrok is helpful when developing locally and needing to expose your server to the internet. It is particularly useful for handling incoming messages (webhooks) during development, not for sending SMS messages.

How to set up Vonage Messages API

Sign up for a Vonage API account. Create a Vonage application, generate keys, enable the Messages capability, and link a Vonage virtual number to your application. Ensure default SMS API is set to 'Messages API'.

How to initialize Vonage Node.js SDK

Import the Vonage library (`@vonage/server-sdk`). Create a new Vonage instance, passing in your API key, API secret, Application ID, and private key. Ensure you read your private key from the `private.key` file.

How to handle Vonage API errors

Implement a try-catch block around the vonage.messages.send() call. Inspect the error?.response?.data object for specific error codes and messages from Vonage. Return appropriate HTTP status codes and informative messages based on the error.

How to fix 'Non-Whitelisted Destination' error

This error usually occurs with trial Vonage accounts. Add the recipient's phone number to your Test Numbers whitelist in the Vonage dashboard. Upgrading to a paid account removes this limitation.

What is the purpose of a Vonage application?

A Vonage Application acts as a container for your project's configuration within Vonage. It holds settings like linked numbers, capabilities (e.g., Messages, Voice), and webhooks for incoming messages or events.

How to secure Vonage API credentials

Use environment variables (process.env) to store API keys and secrets. Do not commit .env files to version control. Utilize your deployment platform's secure mechanisms for handling sensitive data in production.

What is E.164 number format for Vonage

E.164 is an international telephone number format that includes a '+' and the country code, followed by the subscriber number, like +14155550101. Use this format for recipient numbers when sending SMS with Vonage.

How to structure a Node.js Express SMS app

Use Express.js to create a server and define routes. Use the Vonage Node.js SDK to send SMS messages via API requests to the Vonage platform.

Why is my private key not working with Vonage

Double-check the VONAGE_PRIVATE_KEY_PATH in your .env file. It should point to the correct path within your project where the private.key file was placed (usually project root). Also, ensure the file content hasn't been modified.

What causes Vonage authentication errors

Incorrect API key, secret, application ID, or an invalid private key. Check your .env file against your Vonage dashboard values. Also, ensure your account has 'Messages API' selected as default for SMS.

What are common troubleshooting steps for Vonage SMS

Verify correct API credentials in .env, ensure the recipient number is in E.164 format and whitelisted (if applicable), check your Vonage application settings, and consult Vonage logs for more details.