code examples
code examples
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.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for the project, then navigate into it.
bashmkdir messagebird-two-way-sms cd messagebird-two-way-sms -
Initialize Node.js Project: Initialize the project using npm. The
-yflag accepts default settings.bashnpm init -yThis creates a
package.jsonfile. -
Install Dependencies: Install Express, the MessageBird SDK, and
dotenvfor environment variable management.bashnpm install express messagebird dotenvPackage 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)
-
Create Project Files: Create the main application file and files for environment variables and Git ignore rules.
bashtouch index.js .env .gitignoreYour initial project structure should look like this:
textmessagebird-two-way-sms/ ├── node_modules/ ├── .env ├── .gitignore ├── index.js ├── package-lock.json └── package.json -
Configure
.gitignore: Prevent committing sensitive information and unnecessary files to version control. Add the following lines to your.gitignorefile:Code# Dependencies node_modules/ # Environment variables .env # Log files *.log # Operating system files .DS_Store Thumbs.db -
Prepare
.envFile: This file stores your sensitive credentials. Open.envand 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=3000Why
.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
- In the MessageBird Dashboard, navigate to Developers → API access (or look for the API key in the top-right corner)
- Copy your Live API key (starts with
live_) - Paste it as the value for
MESSAGEBIRD_API_KEYin your.envfile
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.
- In the Dashboard, go to Developers → Settings
- Find the Signing Key section
- Copy your signing key
- Paste it as the value for
MESSAGEBIRD_SIGNING_KEYin your.envfile
4. Purchase a Virtual Mobile Number
To receive SMS messages, you need a dedicated virtual mobile number (VMN).
- Navigate to Numbers → Buy a number in the Dashboard
- Select the country where you and your customers are located
- Important: Ensure the SMS capability is enabled for the number
- Choose a number from the available selection
- 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
- Copy your purchased number (in E.164 format, e.g.,
+31612345678) and paste it asMESSAGEBIRD_NUMBERin your.envfile
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):
# MessageBird Credentials
MESSAGEBIRD_API_KEY=live_abcdef1234567890
MESSAGEBIRD_SIGNING_KEY=1a2b3c4d5e6f7g8h9i0j
MESSAGEBIRD_NUMBER=+31612345678
# Server Configuration
PORT=3000Setting 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:
- Visit ngrok.com/download
- Download the appropriate version for your OS
- Unzip and move to your PATH (optional)
Option 2 – Using package managers:
# macOS (Homebrew)
brew install ngrok
# Windows (Chocolatey)
choco install ngrok
# Linux (Snap)
snap install ngrokStart Ngrok Tunnel
Since your Express app runs on port 3000:
ngrok http 3000Output example:
Session Status online
Account your@email.com
Version 3.x.x
Region United States (us)
Forwarding https://a1b2c3d4.ngrok.app -> http://localhost:3000Important: 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
-
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"
-
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
-
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
/webhookpath matches the route you'll create in Express
- Example:
- Headers: (Optional) Add custom headers if needed
- Click Save
-
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
-
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:
// 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:
-
Webhook Signature Verification (lines 18-19): The
ExpressMiddlewareVerifymiddleware 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. -
Raw Body Middleware (line 22): Webhook verification requires access to the raw request body. Use
express.raw()specifically for the/webhookroute before verification. -
Webhook Handler (lines 29-58):
- Receives POST requests from MessageBird when SMS arrives
- Extracts
originator(sender's phone) andpayload(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 numberpayload: Message content (text)id: Unique message identifiercreatedDatetime: Timestamp (ISO 8601 format)- Additional fields:
datacoding,mclass,validity,typeDetails, etc.
-
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
-
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 Code | Description | HTTP Status | Resolution |
|---|---|---|---|
| 2 | Request not allowed (incorrect access_key) | 401 | Verify MESSAGEBIRD_API_KEY in .env |
| 9 | Missing required parameter | 400 | Check request payload includes originator, recipients, body |
| 20 | Insufficient balance | 402 | Add credits to your MessageBird account |
| 21 | Invalid phone number format | 400 | Ensure recipient is in E.164 format |
| 25 | Number not owned by account | 403 | Use a number you've purchased in MessageBird Dashboard |
| 101 | Rate limit exceeded | 429 | Implement exponential backoff and reduce request frequency |
Source: MessageBird API Error Codes
Enhanced Error Handling Example
Add this improved error handling to the sendSMS function:
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:
npm install winstonconst 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:
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-JWTheader 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):
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:
npm install joiconst 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:
npm install express-rate-limitconst 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:
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:
npm install jsonwebtokenconst 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
- Injection Attacks: Validate and sanitize all inputs (phone numbers, message content)
- Broken Authentication: Implement strong API authentication (JWT, API keys)
- Sensitive Data Exposure: Never log full message content or phone numbers in production
- XML External Entities (XXE): Not applicable (using JSON)
- Broken Access Control: Verify user permissions before sending SMS on their behalf
- Security Misconfiguration: Use environment variables, disable debug mode in production
- Cross-Site Scripting (XSS): Sanitize user inputs if displaying in web UI
- Insecure Deserialization: Use trusted libraries, validate all parsed data
- Using Components with Known Vulnerabilities: Keep dependencies updated (
npm audit) - 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.
node index.jsExpected output:
✓ 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:
ngrok http 3000Copy the HTTPS forwarding URL (e.g., https://a1b2c3d4.ngrok.app).
3. Update MessageBird Flow Builder
- Go to your Flow Builder flow in the MessageBird Dashboard
- Edit the "Forward to URL" step
- Update the URL to your new ngrok URL +
/webhook - Example:
https://a1b2c3d4.ngrok.app/webhook - Click Save and Publish
4. Test Inbound SMS (Two-Way Messaging)
Send a test message:
- Use your mobile phone to send an SMS to your MessageBird virtual number
- 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:4040to 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_KEYin.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:
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:
{
"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):
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:
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:
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:
npm install --save-dev jest supertestExample test suite (index.test.js):
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:
npm test9. 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:
- Visit webhook.site
- Copy the unique URL
- Configure it in Flow Builder temporarily
- Send a test SMS
- View the webhook payload structure
Common Issues and Troubleshooting
Common Issues and Solutions
| Issue | Cause | Solution |
|---|---|---|
| Webhook not receiving messages | Flow not published or incorrect URL | Verify Flow Builder configuration, ensure URL ends with /webhook, check ngrok is running |
| 401 Unauthorized on webhook | Invalid or missing signing key | Verify MESSAGEBIRD_SIGNING_KEY in .env matches Dashboard value |
| Signature verification fails | Using express.json() before verification | Use express.raw() for webhook route (see code example) |
| MessageBird API error code 2 | Invalid API key | Check MESSAGEBIRD_API_KEY in .env, ensure using live_ key |
| MessageBird API error code 20 | Insufficient account balance | Add credits to your MessageBird account |
| MessageBird API error code 21 | Invalid phone number format | Ensure recipient is in E.164 format (+[country][number]) |
| MessageBird API error code 25 | Originator number not owned | Use a number purchased in your MessageBird account |
| No auto-reply received | Error in sendSMS() function | Check server logs for errors, verify MESSAGEBIRD_NUMBER in .env |
| Ngrok URL expired | Free ngrok sessions expire after 2 hours | Restart ngrok, update Flow Builder URL |
| Message delayed or not delivered | Network latency or carrier issues | Normal delay: 2-10 seconds. Check MessageBird Dashboard logs |
| Cannot install dependencies | Node.js version too old | Upgrade to Node.js 14.x or higher |
MessageBird Dashboard Logs
For detailed debugging:
- Go to Logs → SMS Logs in MessageBird Dashboard
- View delivery status, timestamps, and error messages
- 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:
// 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:
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:
heroku config:set MESSAGEBIRD_API_KEY=live_abc123
heroku config:set MESSAGEBIRD_SIGNING_KEY=xyz789
heroku config:set MESSAGEBIRD_NUMBER=+31612345678AWS (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:
docker run -e MESSAGEBIRD_API_KEY=live_abc123 \
-e MESSAGEBIRD_SIGNING_KEY=xyz789 \
-e MESSAGEBIRD_NUMBER=+31612345678 \
your-imageWebhook URL Requirements in Production
- Must use HTTPS: MessageBird rejects HTTP webhooks in production
- Must be publicly accessible: No localhost, private IPs, or firewalled endpoints
- Must respond within 10 seconds: Use async processing for long-running tasks
- 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:
npm ci --omit=devOr specify in package.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:
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 startupPM2 ecosystem config (ecosystem.config.js):
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:
pm2 start ecosystem.config.jsPlatform-Specific Deployment Examples
Heroku:
- Create
Procfile:
web: node index.js
- Deploy:
heroku create your-app-name
git push heroku main
heroku config:set MESSAGEBIRD_API_KEY=live_abc123
heroku open- Update Flow Builder webhook URL to Heroku app URL
AWS Lambda (Serverless):
Requires adaptation for serverless architecture. Use AWS API Gateway + Lambda:
// 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:
- Create
app.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- Deploy via DigitalOcean dashboard or CLI
CI/CD Pipeline Example (GitHub Actions)
Create .github/workflows/deploy.yml:
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:
# 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.timerOption 2 – Cloudflare (free) as reverse proxy:
- Point your domain to Cloudflare nameservers
- Enable "Full (strict)" SSL/TLS mode
- Configure origin server certificate
- 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:
# Allow HTTPS (443)
sudo ufw allow 443/tcp
# Allow HTTP (80) for Let's Encrypt renewal only
sudo ufw allow 80/tcpRestrict webhook endpoint access (optional):
Whitelist MessageBird IP ranges (contact MessageBird support for current ranges):
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
- MessageBird SMS API Documentation
- MessageBird Node.js SDK on GitHub
- MessageBird Flow Builder Tutorial
- Webhook Security Best Practices
- MessageBird Developer Tutorials
- MessageBird Pricing
Related Tutorials
Explore these additional MessageBird Node.js tutorials:
- Send SMS with MessageBird Node.js Express
- MessageBird Bulk SMS with Node.js
- SMS Delivery Status Callbacks with MessageBird
- MessageBird SMS with Next.js
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.