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.
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)
Configure .gitignore:
Prevent committing sensitive information and unnecessary files to version control. Add the following lines to your .gitignore file:
Prepare .env File:
This file stores your sensitive credentials. Open .env and add the following placeholders. Fill these in the next section.
.env
# 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.
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_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.
In the Dashboard, go to Developers → Settings
Find the Signing Key section
Copy your signing key
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).
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)
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 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):
.env
# 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
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 /webhook path matches the route you'll create in Express
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:
JavaScript
// index.js// 1. Import necessary modulesconst 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 appconst app = express();
const port = process.env.PORT || 3000;
// 4. Initialize MessageBird SDKconst mb = messagebird(process.env.MESSAGEBIRD_API_KEY);
// 5. Webhook signature verification middleware// This verifies that requests to /webhook actually come from MessageBirdconst verifyWebhook = newExpressMiddlewareVerify(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 itconst bodyString = req.body.toString('utf-8');
const params = newURLSearchParams(bodyString);
// Extract key fields from webhook payloadconst originator = params.get('originator'); // Sender's phone numberconst recipient = params.get('recipient'); // Your MessageBird numberconst payload = params.get('payload'); // Message contentconst messageId = params.get('id'); // MessageBird message IDconst createdDatetime = params.get('createdDatetime');
console.log(`Inbound SMS from ${originator}: "${payload}"`);
// Validate required fieldsif (!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 validationif (!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 {
awaitsendSMS(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-repliesfunctionprocessInboundMessage(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 processingconst lowerPayload = payload.toLowerCase().trim();
let replyMessage = '';
// Example conversation flowsif (lowerPayload.includes('hello') || lowerPayload.includes('hi')) {
replyMessage = 'Hello! Thanks for contacting us. How can we help you today?';
} elseif (lowerPayload.includes('help')) {
replyMessage = 'We're here to help! TextINFOfor information, SUPPORTfor customer support, or STOP to unsubscribe.';
} else if (lowerPayload.includes('info')) {
replyMessage = 'Visit our website at example.com or call us at 1-800-EXAMPLEfor 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);
} elseif (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 replysendSMS(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 APIfunctionsendSMS(recipient, message) {
returnnewPromise((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);
returnreject(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 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.
Raw Body Middleware (line 22): Webhook verification requires access to the raw request body. Use express.raw() specifically for the /webhook route before verification.
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.
Validate all user inputs to prevent injection attacks and ensure data integrity:
Bash
npm install joi
JavaScript
constJoi = require('joi');
// Phone number validation schemaconst 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 bodyconst { error, value } = smsSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
error: error.details[0].message
});
}
// Proceed with validated dataconst { 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 endpointconst smsLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 50, // Limit each IP to 50 requests per windowmessage: '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 minutemax: 100, // Allow up to 100 webhooks per minuteskipSuccessfulRequests: 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:
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
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: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.
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 synchronizationconsole.log('Server time:', newDate().toISOString());
// Use correct signing key (live for production)const verifyWebhook = newExpressMiddlewareVerify(
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 = newSet(); // In production, use Redis/database
app.post('/webhook', verifyWebhook, (req, res) => {
const params = newURLSearchParams(req.body.toString('utf-8'));
const messageId = params.get('id');
// Check if already processedif (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:
Whitelist MessageBird IP ranges (contact MessageBird support for current ranges):
JavaScript
constMESSAGEBIRD_IP_RANGES = [
'52.210.180.0/24', // Example – verify with MessageBird'34.240.0.0/16'// Example – verify with MessageBird
];
functionisMessageBirdIP(ip) {
// Implement IP range checking using ipaddr.js or similarreturnMESSAGEBIRD_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
This repository includes additional examples for customer support ticketing systems, conversation history storage, and advanced auto-reply logic.
Frequently asked questions
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.
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.
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.
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.
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'.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
mkdir messagebird-two-way-sms cd messagebird-two-way-smsInitialize Node.js Project: Initialize the project using npm. The
-yflag accepts default settings.This creates a
package.jsonfile.Install Dependencies: Install Express, the MessageBird SDK, and
dotenvfor environment variable management.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)Create Project Files: Create the main application file and files for environment variables and Git ignore rules.
touch index.js .env .gitignoreYour initial project structure should look like this:
Configure
.gitignore: Prevent committing sensitive information and unnecessary files to version control. Add the following lines to your.gitignorefile:Prepare
.envFile: This file stores your sensitive credentials. Open.envand add the following placeholders. Fill these in the next section.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
live_)MESSAGEBIRD_API_KEYin your.envfileSecurity 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.
MESSAGEBIRD_SIGNING_KEYin your.envfile4. Purchase a Virtual Mobile Number
To receive SMS messages, you need a dedicated virtual mobile number (VMN).
Regional considerations:
+31612345678) and paste it asMESSAGEBIRD_NUMBERin your.envfileE.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
.envYour
.envfile should now look something like this (with your actual credentials):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:
Install Ngrok
Option 1 – Download binary:
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:
Output example:
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:
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:
Configure the SMS Trigger:
Configure the HTTP Endpoint (Webhook):
/webhookhttps://a1b2c3d4.ngrok.app/webhook/webhookpath matches the route you'll create in ExpressPublish Your Flow:
Name Your Flow (Optional but Recommended):
Important webhook URL requirements for production:
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.jsand 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):
originator(sender's phone) andpayload(message text)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)datacoding,mclass,validity,typeDetails, etc.Auto-Reply Logic (lines 69-107): Simple keyword-based responses. In production, replace with:
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
MESSAGEBIRD_API_KEYin.envoriginator,recipients,bodySource: MessageBird API Error Codes
Enhanced Error Handling Example
Add this improved error handling to the
sendSMSfunction: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.logwith a structured logging library like Winston: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:
messageIdSecurity 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-Signature-JWTheader with each webhookManual 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:
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: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-smsendpoint, 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:
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.
6. Environment Variable Security
Never commit
.envto version control. In production:OWASP Top 10 Considerations for SMS Applications
npm audit)Reference: OWASP Top 10
How to Test Your SMS Webhook Integration
1. Start the Application
Ensure your
.envfile is correctly populated with MessageBird credentials.Expected output:
2. Start Ngrok Tunnel
In a separate terminal window, start ngrok:
Copy the HTTPS forwarding URL (e.g.,
https://a1b2c3d4.ngrok.app).3. Update MessageBird Flow Builder
/webhookhttps://a1b2c3d4.ngrok.app/webhook4. Test Inbound SMS (Two-Way Messaging)
Send a test message:
Expected behavior:
Inbound SMS from +1234567890: "hello"http://127.0.0.1:4040to see webhook requestsTroubleshooting inbound messages:
MESSAGEBIRD_SIGNING_KEYin.envsendSMS()function5. Test Outbound SMS via API
Use
curlor 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:
Example 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:4040to inspect all webhook requests in real-time, including headers, body, and response.Webhook.site: For testing without running your server:
Common Issues and Troubleshooting
Common Issues and Solutions
/webhook, check ngrok is runningMESSAGEBIRD_SIGNING_KEYin.envmatches Dashboard valueexpress.json()before verificationexpress.raw()for webhook route (see code example)MESSAGEBIRD_API_KEYin.env, ensure usinglive_key+[country][number])sendSMS()functionMESSAGEBIRD_NUMBERin.envMessageBird Dashboard Logs
For detailed debugging:
Webhook-Specific Troubleshooting
Issue: Webhook signature verification fails intermittently
Causes:
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
.envfiles. 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):
Vercel/Netlify:
Docker:
Webhook URL Requirements in Production
Example production webhook URL:
https://api.yourdomain.com/webhookhttps://sms.example.com/messagebird/inboundhttp://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:
Or 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:
Platform-Specific Deployment Examples
Heroku:
Procfile:heroku create your-app-name git push heroku main heroku config:set MESSAGEBIRD_API_KEY=live_abc123 heroku openAWS 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:
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: 3000CI/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:
Option 3 – AWS Certificate Manager (free for AWS resources):
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:
Improve reliability:
Expand integrations:
Additional Resources
Related Tutorials
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.