How to Build Two-Way SMS Messaging with Node.js, Express, and Infobip
Learn how to build two-way SMS messaging with Node.js, Express, and Infobip API. Complete tutorial covering webhooks, error handling, security, and production deployment.
How to Build Two-Way SMS Messaging with Node.js, Express, and Infobip
This comprehensive guide walks you through building a production-ready Node.js application using Express to handle two-way SMS messaging powered by the Infobip SMS API. You'll learn how to send and receive SMS messages, configure webhooks for inbound messages, implement error handling, secure your application, and deploy it to production.
We'll build a simple "echo bot" – an SMS automation application that automatically replies to any incoming text message with the same content. This serves as a solid foundation for more complex conversational SMS workflows, chatbots, and automated messaging systems.
Project Goals:
Send outbound SMS messages using the Infobip Node.js SDK.
Receive inbound SMS messages from Infobip via webhooks.
Implement basic two-way SMS logic (echoing the message).
Securely manage API credentials and environment variables.
Set up essential error handling and logging.
Prepare the application for production deployment.
Technologies Used:
Node.js: A JavaScript runtime environment for server-side development (minimum version 14 required by Infobip SDK).
Express: A minimal and flexible Node.js web application framework for building REST APIs.
Infobip API & Node.js SDK (@infobip-api/sdk): Official SDK for interacting with Infobip's SMS services (uses /sms/2/text/advanced endpoint).
dotenv: To manage environment variables securely.
System Architecture:
Code
User's Phone <-----> Mobile Carrier <-----> Infobip Platform <-----> Your Node.js/Express App
| (SMS) | (Webhook POST & API Call)
+-------------------------------------------+
(SMS Delivery)
A user sends an SMS to your provisioned Infobip phone number.
Infobip receives the SMS (Mobile Originated - MO).
Infobip sends an HTTP POST request (webhook) containing the message details to your configured endpoint in the Node.js/Express application.
Your application processes the webhook payload (extracts sender number and message text).
Your application uses the Infobip SDK to send a reply SMS (Mobile Terminated - MT) back to the original sender.
Infobip delivers the reply SMS to the user's phone.
A provisioned phone number within your Infobip account capable of sending and receiving SMS.
Node.js (version 14 or higher) and npm (or yarn) installed on your development machine. (Download Node.js).
Basic understanding of JavaScript, Node.js, REST APIs, and the command line.
A tool for testing API endpoints (like curl or Postman).
A way to expose your local development server to the internet (e.g., ngrok) for testing inbound webhooks.
Final Outcome:
By the end of this guide, you will have a running Node.js/Express application that can receive SMS messages sent to your Infobip number and automatically reply. You will also have a solid understanding of integrating Infobip for two-way SMS communication and best practices for building such applications.
1. Setting up the Project
Let's start by creating our project structure and installing the necessary dependencies.
Create Project Directory:
Open your terminal or command prompt and create a new directory for your project. Navigate into it:
Bash
mkdir infobip-two-way-sms
cd infobip-two-way-sms
Initialize Node.js Project:
Initialize the project using npm. This creates a package.json file to manage dependencies and project metadata. You can accept the defaults or customize them.
Bash
npm init -y
Why -y? This flag automatically accepts the default settings for npm init, speeding up the process. You can omit it to configure settings manually.
Install Dependencies:
We need Express for our web server, the Infobip SDK to interact with their API, and dotenv to manage environment variables.
Bash
npm install express @infobip-api/sdk dotenv
express: The web framework.
@infobip-api/sdk: The official Infobip Node.js SDK simplifies API interactions.
dotenv: Loads environment variables from a .env file into process.env.
Create Project Structure:
Let's organize our code logically.
Set up Basic Express Server (src/server.js):
Open src/server.js and add the following initial setup:
JavaScript
// src/server.jsrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
const webhookHandler = require('./webhookHandler'); // We'll create this nextconst app = express();
constPORT = process.env.PORT || 3000; // Use port from env or default to 3000// Middleware to parse JSON bodies. Crucial for reading webhook payloads.
app.use(express.json());
// Define a simple root route for health checks or basic info
app.get('/', (req, res) => {
res.status(200).send('Infobip Two-Way SMS Server Running!');
});
// Define the route for Infobip inbound SMS webhooks// We use POST as Infobip sends data via POST requests
app.post('/webhook/infobip/sms', webhookHandler.handleIncomingSms);
// Start the server
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
require('dotenv').config(): Must be called early to load variables before they are used.
express.json(): This middleware is essential. Infobip sends webhook data as JSON in the request body. This middleware parses it and makes it available as req.body.
/webhook/infobip/sms: This is the specific path where our application will listen for incoming SMS notifications from Infobip. You'll configure this URL in the Infobip portal later.
Add Start Script to package.json:
Open your package.json file and add a start script within the "scripts" section. This provides a standard way to run your application.
JSON
// package.json (partial view){// ... other configurations"scripts":{"start":"node src/server.js",// Add this line"test":"echo \"Error: no test specified\" && exit 1"},// ... rest of the configurations}
Now you can start your server (though it won't do much yet) by running npm start.
2. Implementing Core Functionality (Sending SMS)
We'll create a dedicated service to encapsulate the logic for sending SMS messages using the Infobip SDK.
Configure Environment Variables (.env):
Open the .env file and add your Infobip API Key and Base URL.
.env
# .env
INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL
INFOBIP_SENDER_ID=YOUR_INFOBIP_NUMBER_OR_SENDER_ID # Your Infobip number or registered Alphanumeric Sender ID
INFOBIP_API_KEY:
Purpose: Authenticates your requests to the Infobip API.
How to obtain: Log in to your Infobip account. Navigate to the "API Keys" section (often found under account settings or developer tools). Generate a new API key if you don't have one. Copy the full key.
Format: A long alphanumeric string (e.g., a1b2c3d4e5f67890a1b2c3d4e5f67890-a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890).
INFOBIP_BASE_URL:
Purpose: Specifies the regional API endpoint you should connect to.
How to obtain: This is provided in your Infobip account dashboard, often on the homepage or API documentation landing page after you log in. It's specific to your account region.
Format: A URL like xxxxx.api.infobip.com. Do not include https:// here; the SDK handles the protocol.
INFOBIP_SENDER_ID:
Purpose: The "from" address for your outbound SMS. This must be a number you've provisioned in Infobip or a pre-registered Alphanumeric Sender ID. Using your provisioned number is crucial for two-way messaging so users can reply.
How to obtain: Find your purchased/provisioned numbers in the "Numbers" section of your Infobip portal.
Format: A phone number in E.164 international format (e.g., 447123456789 – no spaces, country code included) or a registered alphanumeric string (e.g., InfoSMS).
Security: Never commit your .env file to version control. Ensure .env has restrictive file permissions (chmod 600 .env) and is never committed.
Create Infobip Service (src/infobipService.js):
This module will initialize the Infobip client and provide a function to send messages.
JavaScript
// src/infobipService.jsconst { Infobip, AuthType } = require('@infobip-api/sdk');
// Ensure required environment variables are setif (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) {
console.error(""INFOBIP_BASE_URL and INFOBIP_API_KEY must be set in the .env file"");
process.exit(1); // Exit if configuration is missing
}
// Initialize the Infobip clientconst infobipClient = newInfobip({
baseUrl: process.env.INFOBIP_BASE_URL,
apiKey: process.env.INFOBIP_API_KEY,
authType: AuthType.ApiKey, // Specify API Key authentication
});
/**
* Sends an SMS message using the Infobip API.
* @param {string} recipientNumber - The destination phone number in E.164 format (e.g., 447123456789).
* @param {string} messageText - The text content of the SMS.
* @param {string} [senderId=process.env.INFOBIP_SENDER_ID] - The sender ID (your Infobip number or registered alphanumeric). Defaults to env var.
* @returns {Promise<object>} - A promise that resolves with the Infobip API response or rejects with an error.
*/constsendSms = async (recipientNumber, messageText, senderId = process.env.INFOBIP_SENDER_ID) => {
if (!recipientNumber || !messageText) {
thrownewError('Recipient number and message text are required.');
}
if (!senderId) {
console.warn('INFOBIP_SENDER_ID is not set. Using default sender.');
// Infobip might assign a shared short code if no sender is provided,// which might affect reply capabilities and deliverability.// For two-way, always use your provisioned number.
}
const payload = {
messages: [
{
destinations: [{ to: recipientNumber }],
from: senderId, // Use the configured sender ID or numbertext: messageText,
},
],
};
console.log(`Attempting to send SMS to ${recipientNumber} from ${senderId || 'default'}`);
try {
// Use the SDK's send methodconst response = await infobipClient.channels.sms.send(payload);
console.log('Infobip SMS Send API Response:', JSON.stringify(response.data, null, 2));
return response.data; // Return the body of the response
} catch (error) {
console.error('Error sending SMS via Infobip:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
// Re-throw the error or handle it as neededthrow error;
}
};
module.exports = {
sendSms,
};
Client Initialization: We create a single instance of the Infobip client using the credentials from .env. AuthType.ApiKey is specified.
sendSms Function: Takes the recipient, message text, and optionally a sender ID. It constructs the payload according to the Infobip API specification (specifically for the /sms/2/text/advanced endpoint structure, which the SDK uses).
Error Handling: Includes a try...catch block to handle potential API errors during the send request and logs relevant information.
Sender ID: Emphasizes the importance of INFOBIP_SENDER_ID for two-way communication. If not set, Infobip might use a default sender, which likely won't work for replies.
3. Building the API Layer (Handling Inbound SMS)
Now, let's implement the logic to receive and process the incoming SMS messages sent by Infobip to our webhook endpoint.
Understand the Infobip Inbound SMS Payload:
When Infobip receives an SMS directed to your number, it will make a POST request to your configured webhook URL (/webhook/infobip/sms in our case). The request body contains JSON data similar to this structure (refer to Infobip SMS API documentation for the complete, up-to-date schema):
JSON
{"results":[{"messageId":"abc123xyz456","from":"447123456789",// Sender's phone number"to":"447987654321",// Your Infobip number"text":"Hello from user!",// The message content"cleanText":"Hello from user!","keyword":"HELLO",// Often the first word, capitalized"receivedAt":"2025-04-20T10:30:00.123Z","smsCount":1,"price":{"pricePerMessage":0,"currency":"EUR"},"callbackData":null// Data you might have sent with the outbound message}// Potentially more messages if received in quick succession],"messageCount":1,"pendingMessageCount":0}
Key Fields:results[0].from (who sent the message) and results[0].text (what they sent) are crucial for our echo bot.
Implement the Webhook Handler (src/webhookHandler.js):
This module takes the incoming request, extracts the necessary information, and uses the infobipService to send a reply.
JavaScript
// src/webhookHandler.jsconst infobipService = require('./infobipService');
/**
* Handles incoming SMS messages from Infobip webhooks.
* Parses the message, logs it, and triggers a reply.
* @param {import('express').Request} req - The Express request object.
* @param {import('express').Response} res - The Express response object.
*/consthandleIncomingSms = async (req, res) => {
console.log("Received Infobip Webhook:", JSON.stringify(req.body, null, 2));
// Basic validation: Check if the payload looks like an Infobip SMS notificationif (!req.body || !req.body.results || !Array.isArray(req.body.results) || req.body.results.length === 0) {
console.error("Invalid or empty payload received.");
// Respond quickly to Infobip to acknowledge receipt, even if invalidreturn res.status(400).send({ message: "Invalid payload structure" });
}
// Infobip recommends responding quickly to webhooks.// Acknowledge receipt immediately before processing.
res.status(200).send({ message: "Webhook received successfully" });
// Process each message in the payload (usually just one)for (const message of req.body.results) {
const sender = message.from;
const receivedText = message.text;
const messageId = message.messageId; // Useful for logging/trackingif (!sender || !receivedText) {
console.warn(`Skipping message ${messageId}: Missing sender or text.`);
continue; // Skip this message and proceed to the next if any
}
console.log(`Processing message ${messageId} from ${sender}: "${receivedText}"`);
// --- Business Logic: Echo the message back ---const replyText = `You sent: "${receivedText}"`;
try {
// Use the Infobip service to send the reply// The 'sender' of the incoming message is the 'recipient' of our replyawait infobipService.sendSms(sender, replyText);
console.log(`Successfully sent reply to ${sender} for message ${messageId}`);
} catch (error) {
console.error(`Failed to send reply to ${sender} for message ${messageId}:`, error.message);
// Implement retry logic or further error handling here if needed
}
// --- End Business Logic ---
}
};
module.exports = {
handleIncomingSms,
};
Logging: The first step is to log the entire incoming payload. This is invaluable for debugging.
Validation: Basic checks ensure the payload structure is roughly what we expect.
Quick Response: It's crucial to send a 200 OK response back to Infobip before doing potentially long-running processing (like calling the API to send the reply). Webhooks often have short timeouts.
Message Iteration: The code iterates through the results array, although typically it contains only one message per webhook request.
Data Extraction: It pulls out the sender (from) and receivedText (text).
Business Logic: This is where you'd implement your application's specific response logic. Here, we simply formulate an echo reply.
Sending Reply: It calls infobipService.sendSms, passing the original sender's number as the recipient for the reply.
Error Handling: Includes a try...catch around the sendSms call to handle failures specific to sending the reply.
4. Integrating with Infobip (Webhook Configuration)
Your code is ready to send and receive, but you need to tell Infobip where to send the incoming SMS notifications.
Expose Your Local Server:
Infobip's servers need to be able to reach your running application. During development, your machine is usually behind a firewall/NAT. Tools like ngrok create a secure tunnel and provide a public URL.
Run your Node.js app: npm start (it should log ""Server listening on port 3000"").
In a new terminal window, start ngrok, telling it to forward to your application's port (e.g., 3000):
Bash
ngrok http 3000
ngrok will display output similar to this:
Code
Session Status online
Account Your Name (Plan: Free)
Version 3.x.x
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding https://<unique-subdomain>.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Copy the https://<unique-subdomain>.ngrok-free.app URL. This is your public webhook URL for now.
Configure Webhook in Infobip Portal:
Log in to your Infobip account.
Navigate to the ""Numbers"" section (or equivalent where you manage your purchased phone numbers).
Find the specific phone number you are using for this application and go to its settings/configuration.
Look for a section related to ""Forwarding,"" ""Webhooks,"" ""Messaging Settings,"" or ""Inbound Messages.""
There should be an option to configure a URL for Incoming SMS Messages (or similar wording).
Paste your ngrok URL, followed by the specific path you defined in server.js:
https://<unique-subdomain>.ngrok-free.app/webhook/infobip/sms
Important: Ensure the method is set to POST.
Save the configuration changes in the Infobip portal.
(Dashboard navigation paths can change. Refer to the current Infobip documentation if you can't find the exact location.)
5. Error Handling, Logging, and Retries
Production applications need robust error handling and logging.
Consistent Error Handling:
Our current code uses try...catch blocks in key areas (infobipService.sendSms, webhookHandler.handleIncomingSms).
Distinguish between errors that should stop processing (like invalid config) and errors that affect a single message (like failed API call to send reply).
Consider creating custom error classes for better error categorization.
Logging:
console.log and console.error are used for basic logging.
For production, use a dedicated logging library like Winston or Pino. They offer:
Different log levels (debug, info, warn, error).
Structured logging (JSON format is common for easier parsing).
Multiple transports (log to console, files, external services).
Example (Conceptual with Winston):
JavaScript
// Example logging setup (e.g., in a logger.js module)const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
// Add file transport for production// new winston.transports.File({ filename: 'error.log', level: 'error' }),// new winston.transports.File({ filename: 'combined.log' }),
],
});
// Use logger.info(), logger.warn(), logger.error() instead of console.*
Retry Mechanisms:
Network issues or temporary Infobip API glitches can cause requests to fail. Implement retries for transient errors.
Use libraries like async-retry or implement a simple loop with exponential backoff (wait longer between each retry).
Example (Conceptual async-retry for sending):
JavaScript
// Inside infobipService.sendSms, wrap the API callconst retry = require('async-retry');
// ...try {
awaitretry(
async bail => { // bail is a function to call for non-retryable errorsconsole.log(`Attempting to send SMS (retry logic)...`);
const response = await infobipClient.channels.sms.send(payload);
console.log('Infobip SMS Send API Response:', JSON.stringify(response.data, null, 2));
return response.data;
},
{
retries: 3, // Number of retriesfactor: 2, // Exponential backoff factorminTimeout: 1000, // Initial wait time (ms)onRetry: (error, attempt) => {
console.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`);
}
}
);
} catch (error) {
console.error('Error sending SMS via Infobip after retries:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
throw error;
}
Caution: Be careful not to retry errors that are clearly permanent (e.g., invalid API key, invalid recipient number format - Infobip error codes can help identify these).
6. Database Schema and Data Layer (Optional for Stateful Apps)
Our current echo bot is stateless – it doesn't remember past interactions. For most real-world applications (support chats, surveys, multi-step workflows), you'll need a database.
Why a Database?
Store conversation history.
Manage user state (e.g., which step of a survey they are on).
Link SMS conversations to user accounts in your system.
Store user preferences or data collected via SMS.
Example Schema (Conceptual - PostgreSQL/Prisma):
You might have tables like:
User: Stores user information, potentially linked by phone number.
Conversation: Groups related messages.
Message: Stores individual inbound/outbound messages with details (sender, recipient, text, timestamp, Infobip message ID, status, direction - inbound/outbound).
Data Access:
Use an Object-Relational Mapper (ORM) like Prisma or Sequelize to interact with your database in a structured way.
Implement functions to:
Find or create a user based on phone number.
Create/retrieve conversation records.
Save inbound and outbound messages, linking them to conversations/users.
Query conversation history.
Example (webhookHandler.js modified conceptually):
Migrations: Use the ORM's migration tools (e.g., prisma migrate dev) to manage database schema changes safely.
7. Adding Security Features
Security is paramount, especially when handling user data and API keys.
Environment Variables: We're already using .env to keep API keys out of the codebase. Ensure the .env file has restrictive permissions (chmod 600 .env) and is never committed.
Input Validation/Sanitization:
Webhook Payload: While we have basic structure validation, you could add more specific checks (e.g., ensure from looks like a phone number, limit text length). Libraries like Joi or Zod are excellent for schema validation.
Database Inputs: Sanitize any user-provided text (receivedText) before storing it in the database or using it in replies to prevent potential Cross-Site Scripting (XSS) issues if this data is ever displayed in a web interface. Libraries like dompurify (if rendering as HTML) or simply escaping special characters might be needed depending on usage.
Webhook Security:
Problem: Anyone could potentially send a POST request to your /webhook/infobip/sms endpoint, pretending to be Infobip.
Solutions:
Signature Validation (Recommended): Check if Infobip offers webhook signature validation (often using HMAC-SHA signatures with a shared secret). This cryptographically verifies the request originated from Infobip. Consult Infobip's documentation for details on enabling and verifying signatures. If they offer it, implement the validation middleware.
IP Filtering: Configure your firewall or infrastructure (e.g., AWS Security Group, Cloudflare) to allow requests to the webhook endpoint only from Infobip's published IP address ranges. This is less secure than signature validation but better than nothing.
Secret in URL (Less Secure): Include a hard-to-guess secret token in the webhook URL path (/webhook/infobip/sms/YOUR_SECRET_TOKEN). This provides minimal obscurity but is vulnerable if the URL leaks.
Rate Limiting:
Protect your application from abuse (accidental or malicious) by limiting the number of requests from a single IP address or for a specific user.
Use middleware like express-rate-limit.
Example (server.js):
JavaScript
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowMsmessage: 'Too many requests from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply to the webhook route
app.use('/webhook/infobip/sms', webhookLimiter);
app.post('/webhook/infobip/sms', webhookHandler.handleIncomingSms);
Helmet Middleware:
Install and use Helmet to set security-related HTTP headers automatically.
Installation:npm install helmet
Usage in server.js:
JavaScript
const helmet = require('helmet');
// Apply Helmet early in middleware chain
app.use(helmet());
Helmet sets headers like Content-Security-Policy, Strict-Transport-Security, and removes the X-Powered-By header to reduce attack surface.
Regular Security Audits: Run npm audit regularly to check for known vulnerabilities in dependencies. Consider using tools like Snyk for continuous security monitoring.
8. Handling Special Cases
Real-world SMS involves nuances:
Phone Number Formatting: Infobip expects and provides numbers in E.164 format (e.g., +14155552671, 447123456789 – note the plus sign is optional in API requests but the format is required). Ensure your application consistently handles this format when sending replies.
Recommended: Use libphonenumber-js to parse, validate, and format numbers.
Character Limits & Concatenation: A standard SMS segment has 160 GSM-7 characters or 70 UCS-2 characters (for non-Latin alphabets or emojis). Longer messages are split into multiple segments (concatenated SMS).
Be mindful of reply length to manage costs (Infobip charges per segment).
Infobip handles the concatenation, but your application logic might need to consider length if splitting messages manually or truncating. The smsCount field in the inbound webhook indicates how many segments the incoming message used.
9. Implementing Performance Optimizations
For high-volume applications:
Fast Webhook Responses: As mentioned, respond to Infobip webhooks immediately (res.status(200).send()) before performing slow operations.
Background Processing: Offload time-consuming tasks (complex logic, multiple external API calls, database operations) to a background job queue (e.g., using libraries like BullMQ with Redis, or services like AWS SQS). The webhook handler simply adds a job to the queue, and a separate worker process handles it.
Database Optimization: Use indexing on frequently queried columns (e.g., phone numbers, conversation IDs, timestamps). Optimize complex queries.
Caching: Cache frequently accessed, rarely changing data (e.g., user profiles, configuration settings) using tools like Redis or Memcached to reduce database load.
Load Testing: Use tools like k6, Artillery, or ApacheBench to simulate high traffic volumes and identify bottlenecks in your webhook handler and API response times.
Profiling: Use Node.js built-in profiler or tools like Clinic.js to analyze CPU usage and memory allocation to pinpoint performance issues in your code.
Frequently Asked Questions (FAQ)
What is two-way SMS messaging?
Two-way SMS messaging allows both sending and receiving text messages programmatically. Unlike one-way SMS (broadcast only), two-way SMS enables interactive conversations where users can reply to messages and your application can respond automatically, creating chatbot-like experiences.
How do I get an Infobip API key?
Log in to your Infobip account and navigate to the API Keys section (usually under account settings or developer tools). Generate a new API key and copy it to your .env file as INFOBIP_API_KEY. Keep this key secure and never commit it to version control.
What phone number format does Infobip require?
Infobip expects phone numbers in E.164 international format: country code + national number without spaces or special characters (e.g., 447123456789 for a UK number). The optional + prefix is accepted but not required. Use libphonenumber-js to validate and format numbers correctly.
How do Infobip webhooks work?
When Infobip receives an SMS to your provisioned number, it sends an HTTP POST request to your configured webhook URL with a JSON payload containing the sender's number, message text, and metadata. Your Express application processes this webhook, extracts the data, and can send a reply using the Infobip SDK.
How can I secure my SMS webhook endpoint?
Implement multiple security layers: (1) Use webhook signature validation if Infobip provides it to verify request authenticity, (2) Apply rate limiting with express-rate-limit to prevent abuse, (3) Filter requests by Infobip's IP ranges, (4) Use Helmet middleware to set security headers, and (5) Validate all incoming payload data before processing.
What's the difference between MO and MT SMS?
MO (Mobile Originated) refers to messages sent from a user's phone to your application. MT (Mobile Terminated) refers to messages sent from your application to a user's phone. In two-way messaging, you receive MO messages via webhooks and send MT messages via the Infobip API.
How do I test webhooks locally?
Use ngrok or a similar tunneling tool to expose your local development server to the internet. Run ngrok http 3000 to get a public HTTPS URL, then configure this URL in your Infobip portal as your webhook endpoint. This allows Infobip to reach your local machine during development.
How much does SMS messaging with Infobip cost?
Infobip pricing varies by destination country and message volume. SMS messages are charged per segment (160 GSM-7 characters or 70 UCS-2 characters for Unicode). Check your Infobip account dashboard for specific pricing details and consider message length when designing replies to manage costs.
Next Steps and Production Deployment
Frequently asked questions
Use the Infobip Node.js SDK (@infobip-api/sdk) to send SMS messages. The sendSms function within the provided infobipService.js module handles constructing the API request and sending it to Infobip. Make sure you have your API key, base URL, and sender ID configured correctly in your .env file to initialize the Infobip client successfully.
The Infobip inbound SMS webhook sends a POST request with a JSON body containing message details. Key fields include results[0].from (sender's number), results[0].text (message content), results[0].to (your Infobip number), and results[0].messageId. The results array may contain multiple messages, although typically just one. Always check the current Infobip documentation for the full, updated schema.
INFOBIP_SENDER_ID represents your Infobip number or registered Alphanumeric Sender ID, and it is crucial for two-way communication. It allows users to reply to the SMS messages they receive, as replies will be routed back to the configured sender. If not set, replies likely won't function as expected.
Use the app.post('/webhook/infobip/sms', webhookHandler.handleIncomingSms) route in your Express application. The webhookHandler.handleIncomingSms function parses the webhook's JSON payload, extracts the sender and message text, and implements your application's logic (like sending a reply).
Use a tunneling tool like ngrok to create a public URL that forwards requests to your local server running on port 3000. Run ngrok http 3000 in your terminal. Ngrok provides a temporary public URL you can configure in the Infobip portal, so they can reach your local webhook endpoint during development.
You need an active Infobip account with a provisioned phone number, Node.js and npm installed, basic JavaScript and API knowledge, and a way to expose your local server (like ngrok). Familiarity with Express.js and RESTful APIs is also beneficial.
Install the SDK with npm install @infobip-api/sdk dotenv. Then initialize an Infobip client in your code, providing your API key, base URL, and AuthType.ApiKey. The dotenv package is used to manage credentials securely. The code example provides details on initializing and using the SDK for sending SMS.
Infobip recommends responding to webhooks (e.g., with a 200 OK) as quickly as possible, ideally within a few seconds, to prevent timeouts. This acknowledgment confirms receipt to Infobip. You can then perform potentially slower processing asynchronously.
If your SMS app needs to manage state (like a chatbot, survey, or multi-step interaction), you'll need a database to store user data, messages, and session information. Stateless apps (like the simple echo bot) may not require a database.
Store your Infobip API keys in a .env file, load them into your application using the dotenv package, and never commit the .env file to version control. This keeps sensitive credentials out of your codebase and repository.
Use try-catch blocks to handle potential errors when sending SMS messages or processing webhooks. Log errors with relevant context. For production, use a dedicated logging library like Winston or Pino. Implement retry mechanisms with exponential backoff for transient errors like network issues.
While you can potentially use a registered Alphanumeric Sender ID in some cases, for reliable two-way SMS, it's highly recommended to use your provisioned Infobip number as the sender. Users can then directly reply to the messages.
Use a library like async-retry to implement retry logic for API calls to the Infobip SDK. This helps manage transient errors, like network issues, by automatically retrying the request. Make sure to configure exponential backoff to prevent overloading the API.
How to Build Two-Way SMS Messaging with Node.js, Express, and Infobip
This comprehensive guide walks you through building a production-ready Node.js application using Express to handle two-way SMS messaging powered by the Infobip SMS API. You'll learn how to send and receive SMS messages, configure webhooks for inbound messages, implement error handling, secure your application, and deploy it to production.
We'll build a simple "echo bot" – an SMS automation application that automatically replies to any incoming text message with the same content. This serves as a solid foundation for more complex conversational SMS workflows, chatbots, and automated messaging systems.
Project Goals:
Technologies Used:
@infobip-api/sdk): Official SDK for interacting with Infobip's SMS services (uses/sms/2/text/advancedendpoint).System Architecture:
Prerequisites:
curlor Postman).Final Outcome:
By the end of this guide, you will have a running Node.js/Express application that can receive SMS messages sent to your Infobip number and automatically reply. You will also have a solid understanding of integrating Infobip for two-way SMS communication and best practices for building such applications.
1. Setting up the Project
Let's start by creating our project structure and installing the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for your project. Navigate into it:
mkdir infobip-two-way-sms cd infobip-two-way-smsInitialize Node.js Project: Initialize the project using npm. This creates a
package.jsonfile to manage dependencies and project metadata. You can accept the defaults or customize them.-y? This flag automatically accepts the default settings fornpm init, speeding up the process. You can omit it to configure settings manually.Install Dependencies: We need Express for our web server, the Infobip SDK to interact with their API, and
dotenvto manage environment variables.express: The web framework.@infobip-api/sdk: The official Infobip Node.js SDK simplifies API interactions.dotenv: Loads environment variables from a.envfile intoprocess.env.Create Project Structure: Let's organize our code logically.
mkdir src touch src/server.js src/infobipService.js src/webhookHandler.js touch .env touch .gitignoresrc/: Contains our main application source code.src/server.js: The main entry point for our Express application.src/infobipService.js: A dedicated module for Infobip API interactions (sending SMS).src/webhookHandler.js: A module to handle incoming Infobip webhooks..env: Stores sensitive configuration like API keys (DO NOT commit this file)..gitignore: Specifies intentionally untracked files that Git should ignore (likenode_modulesand.env).Configure
.gitignore: Add the following lines to your.gitignorefile to prevent committing sensitive information and unnecessary files:Set up Basic Express Server (
src/server.js): Opensrc/server.jsand add the following initial setup:// src/server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const webhookHandler = require('./webhookHandler'); // We'll create this next const app = express(); const PORT = process.env.PORT || 3000; // Use port from env or default to 3000 // Middleware to parse JSON bodies. Crucial for reading webhook payloads. app.use(express.json()); // Define a simple root route for health checks or basic info app.get('/', (req, res) => { res.status(200).send('Infobip Two-Way SMS Server Running!'); }); // Define the route for Infobip inbound SMS webhooks // We use POST as Infobip sends data via POST requests app.post('/webhook/infobip/sms', webhookHandler.handleIncomingSms); // Start the server app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });require('dotenv').config(): Must be called early to load variables before they are used.express.json(): This middleware is essential. Infobip sends webhook data as JSON in the request body. This middleware parses it and makes it available asreq.body./webhook/infobip/sms: This is the specific path where our application will listen for incoming SMS notifications from Infobip. You'll configure this URL in the Infobip portal later.Add Start Script to
package.json: Open yourpackage.jsonfile and add astartscript within the"scripts"section. This provides a standard way to run your application.// package.json (partial view) { // ... other configurations "scripts": { "start": "node src/server.js", // Add this line "test": "echo \"Error: no test specified\" && exit 1" }, // ... rest of the configurations }Now you can start your server (though it won't do much yet) by running
npm start.2. Implementing Core Functionality (Sending SMS)
We'll create a dedicated service to encapsulate the logic for sending SMS messages using the Infobip SDK.
Configure Environment Variables (
.env): Open the.envfile and add your Infobip API Key and Base URL.INFOBIP_API_KEY:a1b2c3d4e5f67890a1b2c3d4e5f67890-a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890).INFOBIP_BASE_URL:xxxxx.api.infobip.com. Do not includehttps://here; the SDK handles the protocol.INFOBIP_SENDER_ID:447123456789– no spaces, country code included) or a registered alphanumeric string (e.g.,InfoSMS).Security: Never commit your
.envfile to version control. Ensure.envhas restrictive file permissions (chmod 600 .env) and is never committed.Create Infobip Service (
src/infobipService.js): This module will initialize the Infobip client and provide a function to send messages.// src/infobipService.js const { Infobip, AuthType } = require('@infobip-api/sdk'); // Ensure required environment variables are set if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) { console.error(""INFOBIP_BASE_URL and INFOBIP_API_KEY must be set in the .env file""); process.exit(1); // Exit if configuration is missing } // Initialize the Infobip client const infobipClient = new Infobip({ baseUrl: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, authType: AuthType.ApiKey, // Specify API Key authentication }); /** * Sends an SMS message using the Infobip API. * @param {string} recipientNumber - The destination phone number in E.164 format (e.g., 447123456789). * @param {string} messageText - The text content of the SMS. * @param {string} [senderId=process.env.INFOBIP_SENDER_ID] - The sender ID (your Infobip number or registered alphanumeric). Defaults to env var. * @returns {Promise<object>} - A promise that resolves with the Infobip API response or rejects with an error. */ const sendSms = async (recipientNumber, messageText, senderId = process.env.INFOBIP_SENDER_ID) => { if (!recipientNumber || !messageText) { throw new Error('Recipient number and message text are required.'); } if (!senderId) { console.warn('INFOBIP_SENDER_ID is not set. Using default sender.'); // Infobip might assign a shared short code if no sender is provided, // which might affect reply capabilities and deliverability. // For two-way, always use your provisioned number. } const payload = { messages: [ { destinations: [{ to: recipientNumber }], from: senderId, // Use the configured sender ID or number text: messageText, }, ], }; console.log(`Attempting to send SMS to ${recipientNumber} from ${senderId || 'default'}`); try { // Use the SDK's send method const response = await infobipClient.channels.sms.send(payload); console.log('Infobip SMS Send API Response:', JSON.stringify(response.data, null, 2)); return response.data; // Return the body of the response } catch (error) { console.error('Error sending SMS via Infobip:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message); // Re-throw the error or handle it as needed throw error; } }; module.exports = { sendSms, };Infobipclient using the credentials from.env.AuthType.ApiKeyis specified.sendSmsFunction: Takes the recipient, message text, and optionally a sender ID. It constructs the payload according to the Infobip API specification (specifically for the/sms/2/text/advancedendpoint structure, which the SDK uses).try...catchblock to handle potential API errors during the send request and logs relevant information.INFOBIP_SENDER_IDfor two-way communication. If not set, Infobip might use a default sender, which likely won't work for replies.3. Building the API Layer (Handling Inbound SMS)
Now, let's implement the logic to receive and process the incoming SMS messages sent by Infobip to our webhook endpoint.
Understand the Infobip Inbound SMS Payload: When Infobip receives an SMS directed to your number, it will make a POST request to your configured webhook URL (
/webhook/infobip/smsin our case). The request body contains JSON data similar to this structure (refer to Infobip SMS API documentation for the complete, up-to-date schema):{ "results": [ { "messageId": "abc123xyz456", "from": "447123456789", // Sender's phone number "to": "447987654321", // Your Infobip number "text": "Hello from user!", // The message content "cleanText": "Hello from user!", "keyword": "HELLO", // Often the first word, capitalized "receivedAt": "2025-04-20T10:30:00.123Z", "smsCount": 1, "price": { "pricePerMessage": 0, "currency": "EUR" }, "callbackData": null // Data you might have sent with the outbound message } // Potentially more messages if received in quick succession ], "messageCount": 1, "pendingMessageCount": 0 }results[0].from(who sent the message) andresults[0].text(what they sent) are crucial for our echo bot.Implement the Webhook Handler (
src/webhookHandler.js): This module takes the incoming request, extracts the necessary information, and uses theinfobipServiceto send a reply.// src/webhookHandler.js const infobipService = require('./infobipService'); /** * Handles incoming SMS messages from Infobip webhooks. * Parses the message, logs it, and triggers a reply. * @param {import('express').Request} req - The Express request object. * @param {import('express').Response} res - The Express response object. */ const handleIncomingSms = async (req, res) => { console.log("Received Infobip Webhook:", JSON.stringify(req.body, null, 2)); // Basic validation: Check if the payload looks like an Infobip SMS notification if (!req.body || !req.body.results || !Array.isArray(req.body.results) || req.body.results.length === 0) { console.error("Invalid or empty payload received."); // Respond quickly to Infobip to acknowledge receipt, even if invalid return res.status(400).send({ message: "Invalid payload structure" }); } // Infobip recommends responding quickly to webhooks. // Acknowledge receipt immediately before processing. res.status(200).send({ message: "Webhook received successfully" }); // Process each message in the payload (usually just one) for (const message of req.body.results) { const sender = message.from; const receivedText = message.text; const messageId = message.messageId; // Useful for logging/tracking if (!sender || !receivedText) { console.warn(`Skipping message ${messageId}: Missing sender or text.`); continue; // Skip this message and proceed to the next if any } console.log(`Processing message ${messageId} from ${sender}: "${receivedText}"`); // --- Business Logic: Echo the message back --- const replyText = `You sent: "${receivedText}"`; try { // Use the Infobip service to send the reply // The 'sender' of the incoming message is the 'recipient' of our reply await infobipService.sendSms(sender, replyText); console.log(`Successfully sent reply to ${sender} for message ${messageId}`); } catch (error) { console.error(`Failed to send reply to ${sender} for message ${messageId}:`, error.message); // Implement retry logic or further error handling here if needed } // --- End Business Logic --- } }; module.exports = { handleIncomingSms, };200 OKresponse back to Infobip before doing potentially long-running processing (like calling the API to send the reply). Webhooks often have short timeouts.resultsarray, although typically it contains only one message per webhook request.sender(from) andreceivedText(text).infobipService.sendSms, passing the original sender's number as the recipient for the reply.try...catcharound thesendSmscall to handle failures specific to sending the reply.4. Integrating with Infobip (Webhook Configuration)
Your code is ready to send and receive, but you need to tell Infobip where to send the incoming SMS notifications.
Expose Your Local Server: Infobip's servers need to be able to reach your running application. During development, your machine is usually behind a firewall/NAT. Tools like
ngrokcreate a secure tunnel and provide a public URL.Install ngrok (Download ngrok).
Run your Node.js app:
npm start(it should log ""Server listening on port 3000"").In a new terminal window, start ngrok, telling it to forward to your application's port (e.g., 3000):
ngrok will display output similar to this:
Copy the
https://<unique-subdomain>.ngrok-free.appURL. This is your public webhook URL for now.Configure Webhook in Infobip Portal:
server.js:https://<unique-subdomain>.ngrok-free.app/webhook/infobip/smsPOST.(Dashboard navigation paths can change. Refer to the current Infobip documentation if you can't find the exact location.)
5. Error Handling, Logging, and Retries
Production applications need robust error handling and logging.
Consistent Error Handling:
try...catchblocks in key areas (infobipService.sendSms,webhookHandler.handleIncomingSms).Logging:
console.logandconsole.errorare used for basic logging.// Example logging setup (e.g., in a logger.js module) const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), // Add file transport for production // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }), ], }); // Use logger.info(), logger.warn(), logger.error() instead of console.*Retry Mechanisms:
async-retryor implement a simple loop with exponential backoff (wait longer between each retry).async-retryfor sending):// Inside infobipService.sendSms, wrap the API call const retry = require('async-retry'); // ... try { await retry( async bail => { // bail is a function to call for non-retryable errors console.log(`Attempting to send SMS (retry logic)...`); const response = await infobipClient.channels.sms.send(payload); console.log('Infobip SMS Send API Response:', JSON.stringify(response.data, null, 2)); return response.data; }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial wait time (ms) onRetry: (error, attempt) => { console.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`); } } ); } catch (error) { console.error('Error sending SMS via Infobip after retries:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message); throw error; }6. Database Schema and Data Layer (Optional for Stateful Apps)
Our current echo bot is stateless – it doesn't remember past interactions. For most real-world applications (support chats, surveys, multi-step workflows), you'll need a database.
Why a Database?
Example Schema (Conceptual - PostgreSQL/Prisma): You might have tables like:
User: Stores user information, potentially linked by phone number.Conversation: Groups related messages.Message: Stores individual inbound/outbound messages with details (sender, recipient, text, timestamp, Infobip message ID, status, direction - inbound/outbound).Data Access:
webhookHandler.jsmodified conceptually):// Inside handleIncomingSms, after extracting sender/text // const user = await db.findOrCreateUserByPhone(sender); // const conversation = await db.findOrCreateActiveConversation(user.id); // await db.saveMessage({ // conversationId: conversation.id, // direction: 'inbound', // infobipMessageId: messageId, // sender: sender, // recipient: message.to, // Your Infobip number // text: receivedText, // }); // --- Business Logic (now potentially stateful) --- // const currentState = conversation.state; // const replyText = determineReplyBasedOnState(currentState, receivedText); // conversation.state = determineNextState(...); // await db.updateConversationState(conversation.id, conversation.state); // --- End Business Logic --- // await infobipService.sendSms(sender, replyText); // await db.saveMessage({ ... direction: 'outbound', ... });Migrations: Use the ORM's migration tools (e.g.,
prisma migrate dev) to manage database schema changes safely.7. Adding Security Features
Security is paramount, especially when handling user data and API keys.
.envto keep API keys out of the codebase. Ensure the.envfile has restrictive permissions (chmod 600 .env) and is never committed.fromlooks like a phone number, limittextlength). Libraries like Joi or Zod are excellent for schema validation.receivedText) before storing it in the database or using it in replies to prevent potential Cross-Site Scripting (XSS) issues if this data is ever displayed in a web interface. Libraries likedompurify(if rendering as HTML) or simply escaping special characters might be needed depending on usage./webhook/infobip/smsendpoint, pretending to be Infobip./webhook/infobip/sms/YOUR_SECRET_TOKEN). This provides minimal obscurity but is vulnerable if the URL leaks.express-rate-limit.server.js):const rateLimit = require('express-rate-limit'); const webhookLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply to the webhook route app.use('/webhook/infobip/sms', webhookLimiter); app.post('/webhook/infobip/sms', webhookHandler.handleIncomingSms);npm install helmetserver.js:const helmet = require('helmet'); // Apply Helmet early in middleware chain app.use(helmet());Content-Security-Policy,Strict-Transport-Security, and removes theX-Powered-Byheader to reduce attack surface.npm auditregularly to check for known vulnerabilities in dependencies. Consider using tools like Snyk for continuous security monitoring.8. Handling Special Cases
Real-world SMS involves nuances:
+14155552671,447123456789– note the plus sign is optional in API requests but the format is required). Ensure your application consistently handles this format when sending replies.libphonenumber-jsto parse, validate, and format numbers.npm install libphonenumber-jsimport parsePhoneNumber from 'libphonenumber-js'; const phoneNumber = parsePhoneNumber(sender); if (phoneNumber && phoneNumber.isValid()) { const e164Number = phoneNumber.format('E.164'); // '+14155552671' await infobipService.sendSms(e164Number, replyText); }smsCountfield in the inbound webhook indicates how many segments the incoming message used.9. Implementing Performance Optimizations
For high-volume applications:
res.status(200).send()) before performing slow operations.k6,Artillery, orApacheBenchto simulate high traffic volumes and identify bottlenecks in your webhook handler and API response times.Frequently Asked Questions (FAQ)
What is two-way SMS messaging?
Two-way SMS messaging allows both sending and receiving text messages programmatically. Unlike one-way SMS (broadcast only), two-way SMS enables interactive conversations where users can reply to messages and your application can respond automatically, creating chatbot-like experiences.
How do I get an Infobip API key?
Log in to your Infobip account and navigate to the API Keys section (usually under account settings or developer tools). Generate a new API key and copy it to your
.envfile asINFOBIP_API_KEY. Keep this key secure and never commit it to version control.What phone number format does Infobip require?
Infobip expects phone numbers in E.164 international format: country code + national number without spaces or special characters (e.g.,
447123456789for a UK number). The optional+prefix is accepted but not required. Uselibphonenumber-jsto validate and format numbers correctly.How do Infobip webhooks work?
When Infobip receives an SMS to your provisioned number, it sends an HTTP POST request to your configured webhook URL with a JSON payload containing the sender's number, message text, and metadata. Your Express application processes this webhook, extracts the data, and can send a reply using the Infobip SDK.
How can I secure my SMS webhook endpoint?
Implement multiple security layers: (1) Use webhook signature validation if Infobip provides it to verify request authenticity, (2) Apply rate limiting with
express-rate-limitto prevent abuse, (3) Filter requests by Infobip's IP ranges, (4) Use Helmet middleware to set security headers, and (5) Validate all incoming payload data before processing.What's the difference between MO and MT SMS?
MO (Mobile Originated) refers to messages sent from a user's phone to your application. MT (Mobile Terminated) refers to messages sent from your application to a user's phone. In two-way messaging, you receive MO messages via webhooks and send MT messages via the Infobip API.
How do I test webhooks locally?
Use ngrok or a similar tunneling tool to expose your local development server to the internet. Run
ngrok http 3000to get a public HTTPS URL, then configure this URL in your Infobip portal as your webhook endpoint. This allows Infobip to reach your local machine during development.How much does SMS messaging with Infobip cost?
Infobip pricing varies by destination country and message volume. SMS messages are charged per segment (160 GSM-7 characters or 70 UCS-2 characters for Unicode). Check your Infobip account dashboard for specific pricing details and consider message length when designing replies to manage costs.
Next Steps and Production Deployment