code examples
code examples
Build Two-Way SMS Messaging with Node.js, React, and Infobip: Complete Guide
Create a real-time two-way SMS application using Node.js, Express, React, Vite, and Infobip webhooks. Send and receive messages with Socket.IO for instant updates.
Build Two-Way SMS Messaging with Node.js, React, and Infobip
This step-by-step guide shows you how to build a web application that sends SMS messages via Infobip and receives replies in real-time. You'll use Node.js with Express for the backend API and webhook handling, the official Infobip Node.js SDK for sending messages, Socket.IO for real-time communication, and Vite with React for the frontend interface.
What you'll build:
- Send SMS: Enable users to send SMS messages to any phone number via a web interface using the Infobip API
- Receive SMS: Handle inbound SMS messages sent to your dedicated Infobip number via webhooks
- Real-time Updates: Display both sent and received messages in the user interface instantly using WebSockets (Socket.IO)
- Scalable Backend: Create a simple but organized Node.js backend capable of handling Infobip interactions
- Modern Frontend: Build a responsive user interface with React and Vite
Technologies Used:
- Node.js: JavaScript runtime for the backend server.
- Express: Web framework for Node.js, used to build the API and handle webhooks.
- Infobip: Communications Platform as a Service (CPaaS) provider for SMS functionality (sending and receiving via API/webhooks).
- @infobip-api/sdk: The official Infobip Node.js SDK for easy API interaction.
- Socket.IO: Library for real-time, bidirectional, event-based communication between the browser and the server.
- Vite: Next-generation frontend tooling for fast development builds.
- React: JavaScript library for building user interfaces.
- dotenv: Module to load environment variables from a
.envfile. - ngrok (for development): Tool to expose local servers to the public internet, necessary for receiving Infobip webhooks during development.
System Architecture:
(Note: An image diagram (SVG/PNG) would be preferable here for better readability across platforms.)
Prerequisites:
- Node.js and npm (or yarn): Install Node.js v18, v20, or v22 (LTS versions recommended) on your system. Node.js v14 reached end-of-life on April 30, 2023.
- Infobip Account: Sign up for a free trial or use an existing account.
- Infobip Phone Number: Obtain an SMS-enabled number from your Infobip account that supports two-way messaging.
- Basic Knowledge: Familiarize yourself with JavaScript, Node.js, React, and basic terminal commands.
- ngrok (Optional but Recommended for Local Dev): Install ngrok to test webhooks locally by exposing your development server to the public internet.
What you'll have by the end:
By completing this guide, you'll have a functional web application consisting of:
- A Node.js backend server that can:
- Send SMS messages using the Infobip SDK
- Receive inbound SMS webhook notifications from Infobip
- Push received messages to connected clients via Socket.IO
- A React frontend application where users can:
- Enter a phone number and message text
- Send SMS via the backend API
- See both sent and received messages appear in a chat-like interface in real-time
GitHub Repository:
Find a complete working example of the code on GitHub: [repository link placeholder]
1. Set Up the Project
Create a monorepo structure with separate folders for the backend and frontend.
1.1. Create Project Directory
Open your terminal and create the main project folder:
mkdir infobip-two-way-sms
cd infobip-two-way-sms1.2. Set Up the Backend (Node.js/Express)
Navigate into the main directory and create the backend folder, initialize npm, and install dependencies.
# Inside infobip-two-way-sms/
mkdir backend
cd backend
# Initialize npm project
npm init -y
# Install dependencies
npm install express socket.io @infobip-api/sdk dotenv cors
# Install development dependency (optional, for auto-reloading server)
npm install --save-dev nodemonDependencies explained:
express: Web server frameworksocket.io: Real-time communication library (server-side)@infobip-api/sdk: Infobip's official Node.js SDKdotenv: Loads environment variables from.envfilecors: Enables Cross-Origin Resource Sharing (necessary for frontend communication)nodemon: Monitors for changes and automatically restarts the server (useful during development)
1.3. Create Backend Project Structure
Create the following basic structure within the backend folder:
backend/
├── node_modules/
├── .env.example
├── .gitignore
├── package.json
└── server.js
.env.example: A template for required environment variables.gitignore: Specifies files/folders git should ignore (likenode_modulesand.env)package.json: Project manifest that tracks dependencies and scriptsserver.js: Main application file for the backend server
1.4. Configure Backend Environment
Create a file named .env in the backend directory (copy .env.example). You'll populate this later with your Infobip credentials.
.env.example:
# Infobip Credentials
INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL # e.g., yz9qj9.api.infobip.com
INFOBIP_NUMBER=YOUR_INFOBIP_PHONE_NUMBER # The number you got from Infobip
# Server Configuration
PORT=4000
FRONTEND_URL=http://localhost:5173
# (Optional) Webhook Security
WEBHOOK_SECRET=YOUR_CHOSEN_RANDOM_SECRET_STRINGAdd node_modules and .env to your .gitignore file:
.gitignore:
node_modules
.env
npm-debug.log1.5. Set Up the Frontend (Vite/React)
Navigate back to the root project directory (infobip-two-way-sms/) and create the frontend project using Vite.
# Make sure you're in infobip-two-way-sms/
cd ..
# Create Vite React project named 'frontend'
npm create vite@latest frontend -- --template react
# Navigate into the frontend project
cd frontend
# Install dependencies
npm install
npm install socket.io-clientsocket.io-client: Real-time communication library (client-side)
Vite scaffolds a React project structure for you inside the frontend folder.
1.6. Add Development Scripts (Recommended)
Modify the scripts section in backend/package.json to use nodemon:
backend/package.json:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
}Now run npm run dev in the backend directory for auto-reloading development.
2. Implement Core Backend Functionality
Build the backend server logic in backend/server.js.
backend/server.js:
// Load environment variables
require('dotenv').config();
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const { Infobip, AuthType } = require('@infobip-api/sdk');
const cors = require('cors');
// --- Configuration ---
const PORT = process.env.PORT || 4000;
const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
const INFOBIP_API_KEY = process.env.INFOBIP_API_KEY;
const INFOBIP_BASE_URL = process.env.INFOBIP_BASE_URL;
const INFOBIP_NUMBER = process.env.INFOBIP_NUMBER; // Your Infobip sender number
// const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // Optional: for webhook security
// Basic validation for essential environment variables
if (!INFOBIP_API_KEY || !INFOBIP_BASE_URL || !INFOBIP_NUMBER) {
console.error('Missing required Infobip environment variables!');
process.exit(1); // Exit if essential config is missing
}
// --- Initialize Express App & HTTP Server ---
const app = express();
// Middleware to parse JSON request bodies
app.use(express.json());
// Middleware for enabling CORS
app.use(cors({ origin: FRONTEND_URL }));
const server = http.createServer(app);
// --- Initialize Socket.IO ---
const io = new Server(server, {
cors: {
origin: FRONTEND_URL, // Allow connections from our frontend
methods: ['GET', 'POST'],
},
});
// --- Initialize Infobip Client ---
const infobip = new Infobip({
baseUrl: INFOBIP_BASE_URL,
apiKey: INFOBIP_API_KEY,
authType: AuthType.ApiKey,
});
// --- Socket.IO Connection Handling ---
io.on('connection', (socket) => {
console.log('A user connected:', socket.id);
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
// Optional: Handle potential connection errors
socket.on('connect_error', (err) => {
console.error(`Socket connect_error due to ${err.message}`);
});
});
// --- API Endpoints ---
// Simple health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Endpoint to send an SMS
app.post('/api/send-sms', async (req, res) => {
const { to, text } = req.body;
// Basic validation
if (!to || !text) {
// Use single quotes for the error message string
return res.status(400).json({ error: 'Missing \'to\' or \'text\' field' });
}
// Add more robust validation for phone number format if needed
console.log(`Attempting to send SMS to ${to} from ${INFOBIP_NUMBER}`);
try {
const smsResponse = await infobip.channels.sms.send({
messages: [
{
destinations: [{ to: to }],
from: INFOBIP_NUMBER, // Use your Infobip number as sender
text: text,
},
],
});
console.log('Infobip SMS Response:', JSON.stringify(smsResponse.data, null, 2));
// Optional: Check response status from Infobip
const messageStatus = smsResponse.data.messages?.[0]?.status;
if (messageStatus?.groupName === 'REJECTED' || messageStatus?.groupName === 'FAILED') {
console.error('Infobip rejected the message:', messageStatus.description);
// Don't emit to frontend if rejected immediately
return res.status(500).json({ error: 'Infobip failed to send message', details: messageStatus.description });
}
// Emit the sent message via Socket.IO to update frontend
io.emit('receiveMessage', {
id: smsResponse.data.messages?.[0]?.messageId || Date.now().toString(), // Use Infobip ID or fallback
sender: 'Me', // Indicate it's an outgoing message
text: text,
timestamp: new Date().toISOString(),
direction: 'outbound',
});
res.status(200).json({ success: true, messageId: smsResponse.data.messages?.[0]?.messageId });
} catch (error) {
console.error('Error sending SMS via Infobip:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
res.status(500).json({ error: 'Failed to send SMS', details: error.message });
}
});
// --- Webhook Endpoint ---
// Endpoint to receive inbound SMS from Infobip
app.post('/webhooks/infobip', (req, res) => {
console.log('Received Infobip Webhook:');
console.log(JSON.stringify(req.body, null, 2));
// Optional: Basic Webhook Security Check
// const receivedSecret = req.headers['x-webhook-secret']; // Example header
// if (!WEBHOOK_SECRET || receivedSecret !== WEBHOOK_SECRET) {
// console.warn('Unauthorized webhook attempt');
// return res.status(401).send('Unauthorized');
// }
// Extract results from the webhook payload
const results = req.body.results;
if (!results || !Array.isArray(results) || results.length === 0) {
console.warn('Webhook received empty or invalid results');
return res.status(400).send('Bad Request: No results found');
}
// Process each message received in the webhook
results.forEach(message => {
if (message.messageId && message.from && message.text) {
console.log(`Processing inbound message ${message.messageId} from ${message.from}`);
// Emit the received message via Socket.IO
io.emit('receiveMessage', {
id: message.messageId,
sender: message.from, // The actual sender's number
text: message.text,
timestamp: message.receivedAt || new Date().toISOString(),
direction: 'inbound',
});
} else {
console.warn('Skipping invalid message format in webhook:', message);
}
});
// Acknowledge receipt to Infobip
res.sendStatus(200);
});
// --- Start Server ---
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Allowed frontend origin: ${FRONTEND_URL}`);
console.log(`Infobip Number (Sender ID): ${INFOBIP_NUMBER}`);
});
// Optional: Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
server.close(() => {
console.log('HTTP server closed');
// Close Socket.IO connections if necessary
io.close();
});
});How the code works:
- Initialization: Load environment variables, import necessary modules, and set up Express, HTTP server, and Socket.IO with CORS configured for the frontend URL
- Infobip Client: Initialize the
@infobip-api/sdkclient with your Base URL and API Key - Socket.IO: Listen for client connections and disconnections. Log events for debugging
/healthEndpoint: A simple endpoint to check if the server is running/api/send-smsEndpoint (POST):- Receives
to(recipient number) andtext(message content) from the frontend request body - Performs basic validation
- Uses
infobip.channels.sms.send()to send the message via the SDK. YourINFOBIP_NUMBERis used as thefromaddress - Logs the response from Infobip
- Emits a
receiveMessageevent via Socket.IO to push the sent message back to all connected clients (including the sender) so the UI updates immediately. Mark it asdirection: 'outbound'andsender: 'Me' - Sends a JSON response back to the initial HTTP request indicating success or failure
- Receives
/webhooks/infobipEndpoint (POST):- This endpoint receives data from Infobip when an SMS is received on your Infobip number
- Logs the incoming webhook payload (useful for debugging)
- (Optional Security): Includes commented-out code showing how you could check for a secret header to verify the webhook source
- Parses the
resultsarray from the Infobip payload - For each valid message in the
results, it emits areceiveMessageevent via Socket.IO, pushing the inbound message details (id,sender,text,timestamp,direction: 'inbound') to all connected clients - Sends a
200 OKstatus back to Infobip to acknowledge receipt – Infobip expects this acknowledgment
3. Build the API Layer
The backend code in server.js already includes the necessary API endpoints:
POST /api/send-sms
Purpose: Send an outgoing SMS message
Authentication: Implicitly authenticated via the Infobip API Key configured on the server. No user-level auth is implemented in this basic example.
Request Validation: Checks for the presence of to and text in the JSON body
Request Body (JSON):
{
"to": "+15551234567",
"text": "Hello from the app!"
}Success Response (200 OK, JSON):
{
"success": true,
"messageId": "some-infobip-message-id-string"
}Error Response (400 Bad Request, JSON): Missing fields
{
"error": "Missing 'to' or 'text' field"
}Error Response (500 Internal Server Error, JSON): Infobip API error
{
"error": "Failed to send SMS",
"details": "Error message from Infobip or SDK"
}Test with cURL:
curl -X POST http://localhost:4000/api/send-sms \
-H "Content-Type: application/json" \
-d '{"to": "+1555YOURNUMBER", "text": "Test via cURL"}'Replace +1555YOURNUMBER with a valid test number, likely your own during free trial.
POST /webhooks/infobip
Purpose: Receive inbound SMS messages from Infobip
Authentication: Relies on the obscurity of the URL and optionally a shared secret header (commented out). For production, verify the source IP or use stronger signature verification methods provided by Infobip.
Request Validation: Checks for the presence and format of the results array in the JSON body
Request Body (JSON – Example from Infobip):
{
"results": [
{
"messageId": "inbound-message-id-string",
"from": "+1555SENDERNUMBER",
"to": "YOUR_INFOBIP_NUMBER",
"text": "This is a reply!",
"cleanText": "This is a reply!",
"keyword": "KEYWORD",
"receivedAt": "2025-04-20T10:30:00.123Z",
"smsCount": 1,
"price": {
"pricePerMessage": 0,
"currency": "EUR"
},
"callbackData": "CallbackData"
}
],
"messageCount": 1,
"pendingMessageCount": 0
}Success Response: Empty body with status 200 acknowledges receipt
Error Responses:
- 400 Bad Request: Invalid payload format
- 401 Unauthorized: Webhook secret validation failed (when implemented)
Testing: Requires configuring the Infobip webhook and sending an SMS to your Infobip number.
4. Integrate with Infobip
4.1. Obtain Infobip Credentials
- Log in to your Infobip account
- Get your API Key:
- Navigate to the Homepage or Developers section
- Find the API Keys management area
- If you don't have one, create a new API Key with a descriptive name (e.g., "Node Two-Way App")
- Copy the API Key value immediately and store it securely – you won't be able to see it again
- Get your Base URL:
- On the same API Keys page or developer overview, find your account's unique Base URL (e.g.,
xxxxx.api.infobip.com) and copy it
- On the same API Keys page or developer overview, find your account's unique Base URL (e.g.,
- Get your Infobip Number:
- Navigate to Channels and Numbers → Numbers
- Acquire a new number (ensure it's SMS enabled and supports two-way communication for your region) or use an existing one
- Copy the phone number in E.164 format (e.g.,
+14155550100)
4.2. Update Your .env File
Open backend/.env and paste the values you copied:
INFOBIP_API_KEY=paste_your_api_key_here
INFOBIP_BASE_URL=paste_your_base_url_here # e.g., yz9qj9.api.infobip.com
INFOBIP_NUMBER=paste_your_infobip_number_here # e.g., +14155550100
# Server Configuration
PORT=4000
FRONTEND_URL=http://localhost:5173
# (Optional) Webhook Security – Choose a strong random string if you use this
# WEBHOOK_SECRET=replace_with_a_strong_secret4.3. Configure Infobip Webhook for Inbound SMS
This step is crucial for receiving messages. Since your backend runs locally during development, use ngrok to expose it to Infobip.
-
Start your backend server:
bash# In the backend/ directory npm run devConfirm it logs
Server listening on port 4000 -
Start ngrok: Open another terminal window and run:
bashngrok http 4000ngrok displays forwarding URLs. Copy the
httpsURL (e.g.,https://random-string.ngrok-free.app). -
Configure Infobip Forwarding:
- Return to the Infobip portal
- Navigate to Channels and Numbers → Numbers
- Find your number and click it to manage its configuration
- Look for Forwarding or Webhook settings for Incoming Messages or SMS MO (Mobile Originated)
- Set the URL to:
https://<your-ngrok-subdomain>.ngrok-free.app/webhooks/infobip - (Optional Security): If you implemented the secret header check in
server.js, add a custom HTTP header likeX-Webhook-Secretwith the value matching yourWEBHOOK_SECRETin.env - Save the configuration
When someone sends an SMS to your Infobip number, Infobip forwards it via POST request to your ngrok URL, which tunnels it to your local http://localhost:4000/webhooks/infobip endpoint.
Important: ngrok URLs are temporary – you get a new one each time you restart ngrok. For production, use a permanent public URL hosted on your deployment platform.
5. Implementing Error Handling and Logging
Our server.js includes basic error handling and logging:
- Environment Variable Check: Exits gracefully if essential Infobip config is missing.
- API Request Validation: Returns
400 Bad Requestfor missing fields in/api/send-sms. - Infobip SDK Errors: The
try...catchblock aroundinfobip.channels.sms.sendcatches errors during the API call. It logs detailed error information (if available from the SDK's response) and returns a500 Internal Server Errorto the client. - Webhook Validation: Checks for the presence of the
resultsarray and logs warnings for invalid formats. Returns400 Bad Requestor401 Unauthorized(if secret check is enabled and fails). - Logging: Uses
console.logandconsole.errorto output information about server startup, connections, API calls, responses, webhook receipts, and errors.
Improvements for Production:
- Structured Logging: Use a library like
WinstonorPinofor structured JSON logging, which is easier to parse and analyze with log management tools. Include request IDs for tracing. - Centralized Error Handling Middleware: Implement Express middleware to catch unhandled errors consistently.
- Detailed Error Responses: Provide more specific error codes or messages to the frontend where appropriate, without exposing sensitive backend details.
- Infobip Status Codes: Pay close attention to the
status.groupName,status.name, andstatus.descriptionfields in both the API response and webhook payloads for detailed insights into message delivery or failures. (Refer to Infobip documentation for code meanings). - Retry Mechanisms: For transient network errors when sending SMS, implement a retry strategy (e.g., exponential backoff) using libraries like
async-retry. This is less critical for SMS compared to other API calls but can improve reliability. - Webhook Retries: Infobip typically retries sending webhooks if it doesn't receive a
2xxresponse. Ensure your webhook handler is idempotent (processing the same message multiple times doesn't cause issues) if possible, or implement logic to detect and ignore duplicate message IDs.
6. Creating a Database Schema (Conceptual)
While this guide doesn't implement database persistence for brevity, a real-world application would need to store messages for history and status tracking.
Conceptual Schema (e.g., using PostgreSQL):
CREATE TABLE messages (
id SERIAL PRIMARY KEY, -- Or UUID
infobip_message_id VARCHAR(100) UNIQUE, -- Store Infobip's ID for correlation
direction VARCHAR(10) NOT NULL CHECK (direction IN ('inbound', 'outbound')), -- 'inbound' or 'outbound'
sender_number VARCHAR(20) NOT NULL, -- Actual sender ('Me' for outbound, E.164 for inbound)
recipient_number VARCHAR(20) NOT NULL, -- Actual recipient (E.164 for outbound, your Infobip number for inbound)
message_text TEXT NOT NULL,
status_group_name VARCHAR(50), -- e.g., PENDING, DELIVERED, FAILED (from Infobip status/DLR)
status_name VARCHAR(50), -- e.g., PENDING_ACCEPTED, DELIVERED_TO_HANDSET (from Infobip status/DLR)
status_description TEXT, -- Detailed status description
infobip_timestamp TIMESTAMPTZ, -- receivedAt or time of API call
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Index for querying messages by Infobip ID or potentially sender/recipient
CREATE INDEX idx_messages_infobip_id ON messages(infobip_message_id);
CREATE INDEX idx_messages_numbers ON messages(sender_number, recipient_number);
CREATE INDEX idx_messages_created_at ON messages(created_at);
-- Optional: Trigger to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_messages_updated_at
BEFORE UPDATE ON messages
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();Implementation Notes:
- ORM/Query Builder: Use libraries like Prisma, Sequelize (Node.js ORMs), or Knex.js (Query Builder) to interact with the database.
- Data Access Layer: Abstract database logic into dedicated functions or services.
- Migrations: Use migration tools (like Prisma Migrate or
db-migrate) to manage schema changes. - Saving Messages:
- In
/api/send-sms, after a successful Infobip API call, insert a record withdirection: 'outbound',sender: INFOBIP_NUMBER(or 'Me'),recipient: to, and the initial status (e.g., 'PENDING'). Store themessageId. - In
/webhooks/infobip, when processing an inbound message, insert a record withdirection: 'inbound',sender: message.from,recipient: message.to, and the received text/timestamp. Store themessageId. - Crucially, a production application requires handling Delivery Reports (DLRs). A critical next step, not covered in detail in this guide, is implementing a separate webhook handler (e.g.,
/webhooks/infobip-dlr) specifically for receiving DLRs from Infobip. This handler would update the status fields (status_group_name,status_name, etc.) of the corresponding outbound message in your database using themessageIdprovided in the DLR payload. This is essential for knowing if a message was actually delivered.
- In
- Performance: Index relevant columns (
infobip_message_id,sender_number,recipient_number, timestamps) for efficient querying. Use connection pooling.
7. Adding Security Features
Security is paramount, especially when dealing with APIs and external webhooks.
- Secure API Key Storage:
- NEVER commit API keys or secrets directly into your code or version control.
- Use environment variables (
.envlocally, secure configuration management in deployment). - Ensure your
.envfile is listed in.gitignore.
- Webhook Security:
- Shared Secret: Implement the commented-out shared secret header check (
X-Webhook-Secret) for basic verification. Choose a strong, random secret. - IP Whitelisting (Production): Configure your firewall or load balancer to only allow requests to the webhook endpoint from Infobip's known IP address ranges (check Infobip documentation for these).
- Signature Verification (More Secure): Infobip may offer more advanced signature-based verification for webhooks. If available, implement this for stronger security.
- Shared Secret: Implement the commented-out shared secret header check (
- Input Validation and Sanitization:
- Backend: Validate incoming data strictly in both the API (
/api/send-sms) and webhook (/webhooks/infobip) handlers. Check data types, lengths, and formats (e.g., ensuretolooks like a phone number). Libraries likeJoiorexpress-validatorcan help. Sanitize any data before storing it or potentially rendering it (though React helps prevent XSS). - Frontend: Perform basic client-side validation for a better user experience, but always rely on backend validation as the source of truth.
- Backend: Validate incoming data strictly in both the API (
- Rate Limiting:
- Protect the
/api/send-smsendpoint from abuse. Implement rate limiting using libraries likeexpress-rate-limitto restrict the number of requests a single IP address can make within a specific time window.
- Protect the
- HTTPS:
- Always use HTTPS for communication between the frontend, backend, and Infobip (ngrok provides this locally; ensure your production deployment uses HTTPS).
- CORS Configuration:
- In
server.js, thecorsmiddleware is currently configured to allow requests only fromFRONTEND_URL. Keep this restricted to trusted origins in production.
- In
- Dependency Security:
- Regularly update dependencies (
npm update) and use tools likenpm auditor Snyk to check for known vulnerabilities in your project's dependencies.
- Regularly update dependencies (
- Preventing Log Injection:
- Be cautious about logging raw user input directly. Sanitize or encode data if necessary before logging to prevent log injection attacks.
8. Handling Special Cases
Real-world SMS messaging involves nuances:
- Character Encoding: Infobip typically handles standard GSM-7 and UCS-2 (for non-Latin characters) encoding automatically. Be mindful if sending unusual characters. Test thoroughly.
- Long SMS Messages (Concatenation): Standard SMS messages have length limits (160 chars for GSM-7, 70 for UCS-2). Longer messages are split into multiple parts (concatenated SMS). Infobip handles sending these, but be aware they consume more credits. The SDK/API manages the splitting. Ensure your UI reflects potential costs if necessary.
- Phone Number Formatting: While the backend expects E.164 format (
+15551234567), users might enter numbers differently. Consider adding frontend and/or backend logic to normalize inputs to E.164 before sending to the API. Libraries exist for phone number parsing and validation (e.g.,libphonenumber-js). - Internationalization (i18n): If your app supports multiple languages, ensure message text sent via the API is correctly localized. The frontend UI would also need i18n support.
- Delivery Reports (DLRs): As mentioned in Section 6, this guide focuses on sending and receiving initial messages. A complete two-way solution must handle Infobip's Delivery Reports (DLRs). These are typically sent via a separate webhook configured in Infobip. Receiving and processing DLRs is essential to update the status of outbound messages (e.g., Delivered, Failed, Rejected) in your system/database. Implementing a DLR webhook handler is a critical step for any production application needing reliable status tracking, but its implementation is beyond the scope of this introductory guide.
- Opt-Out Handling (STOP keywords): Regulations require handling opt-out requests (e.g., users replying
""STOP""). Infobip often has features to manage opt-out lists automatically. Ensure you comply with local regulations. Your inbound webhook handler (/webhooks/infobip) might need to check incoming messages for keywords like STOP, HELP, etc., and take appropriate action (e.g., marking the number as opted-out in your database or relying on Infobip's platform features). - Infobip Rate Limits: Be aware of any rate limits imposed by Infobip on your account for sending messages. Implement appropriate delays or throttling in your backend if sending bulk messages.
9. Implementing Performance Optimizations
For this simple application, performance isn't a major concern, but consider these for scaling:
- Efficient WebSocket Handling: Socket.IO is generally efficient. Avoid sending excessively large payloads over WebSockets. Emit events only when necessary.
- Database Query Optimization: If storing messages, ensure proper indexing (as shown in Section 6) and write efficient queries. Avoid N+1 query problems if fetching related data.
- Backend Load: If sending a very high volume of SMS, consider:
- Asynchronous Processing: Move the Infobip API call out of the main request/response cycle. Respond quickly to the frontend HTTP request and process the SMS sending in a background job queue (e.g., using BullMQ, Kue). Update the frontend via WebSockets once the background job completes or fails.
- Connection Pooling: Ensure your database connection strategy uses pooling to efficiently manage connections under load.
- Load Balancing: Deploy multiple instances of your backend server behind a load balancer. Ensure your Socket.IO setup works correctly across multiple instances (often requires a shared adapter like the Redis adapter).
- Frontend Performance:
- Code Splitting: Vite handles this well automatically, ensuring users only download necessary code.
- Memoization: Use
React.memo,useMemo, anduseCallbackappropriately to prevent unnecessary re-renders in your React components, especially in the message list. - Virtualization: If the message list can grow very large, consider using a virtualization library (like
react-windoworreact-virtualized) to render only the visible items.
- Caching: Cache frequently accessed, non-dynamic data where appropriate (e.g., using Redis on the backend).
Frequently Asked Questions
What is two-way SMS messaging?
Two-way SMS messaging enables bidirectional communication where users can both send SMS messages to recipients and receive replies back. This requires configuring webhooks to receive inbound messages from your SMS provider (like Infobip) and real-time communication (like Socket.IO) to update the user interface when replies arrive.
How do webhooks work with Infobip?
Webhooks are HTTP POST requests that Infobip sends to your server when an event occurs (like receiving an inbound SMS). You configure a webhook URL in your Infobip portal that points to your server endpoint (e.g., /webhooks/infobip). When someone sends an SMS to your Infobip number, Infobip forwards the message data to your webhook endpoint.
Why use Socket.IO for real-time updates?
Socket.IO provides real-time, bidirectional communication between the browser and server using WebSockets. This allows your application to instantly push received SMS messages to all connected clients without requiring them to poll the server repeatedly. It creates a chat-like experience where messages appear immediately.
Do I need ngrok for production deployment?
No. ngrok is only necessary for local development to expose your localhost server to the public internet for webhook testing. In production, deploy your application to a hosting platform with a permanent public URL (like AWS, Heroku, or DigitalOcean) and configure that URL as your Infobip webhook endpoint.
Which Node.js versions are compatible with this implementation?
This implementation works with Node.js LTS versions v18, v20, and v22. Node.js v14 reached end-of-life on April 30, 2023, and should not be used. Always use an actively maintained LTS version for production deployments.
How do I secure my webhook endpoint?
Implement multiple security layers: (1) Use a shared secret header that Infobip includes in webhook requests and verify it in your handler, (2) Whitelist Infobip's IP addresses in your firewall, (3) Use signature verification if Infobip provides it, and (4) Always use HTTPS for the webhook URL.
What's the difference between SMS sending and delivery reports?
Sending an SMS means the message was successfully submitted to Infobip's API. Delivery reports (DLRs) are separate webhook notifications that tell you the final status of the message (delivered, failed, rejected). You need a separate webhook endpoint to handle DLRs if you want to track delivery status.
Can I use Vue.js instead of React for the frontend?
Yes. The backend code remains the same. For the frontend, replace the React/Vite setup with Vue 3 and Vite (using npm create vite@latest frontend -- --template vue), then use the Socket.IO client in your Vue components with the Composition API or Options API to connect to the backend and handle real-time updates.
How do I handle message history and persistence?
Implement a database (PostgreSQL, MongoDB, or MySQL) to store all sent and received messages with fields like message ID, direction (inbound/outbound), sender, recipient, text, timestamp, and status. Query this database to load message history when users open the application. Use the conceptual schema in Section 6 as a starting point.
What are the common rate limits for Infobip SMS?
Rate limits vary by account type and region. Free trial accounts typically have strict limits on message volume and destination numbers. Check your Infobip account dashboard or contact their support for your specific limits. Implement rate limiting on your API endpoints to prevent exceeding these limits.
Summary
You've now built a complete two-way SMS messaging application using Node.js, Express, Socket.IO, React, Vite, and the Infobip API. This implementation provides real-time bidirectional communication where users can send SMS messages through a web interface and receive replies instantly via webhooks and WebSocket connections.
The application architecture demonstrates key concepts including REST API design, webhook handling, real-time server-to-client communication, and third-party API integration. You've learned how to configure Infobip for inbound SMS forwarding, expose local development servers with ngrok, implement proper error handling and validation, and apply security best practices.
For production deployment, consider adding database persistence for message history, implementing delivery report webhooks for status tracking, adding user authentication, scaling with Redis-backed Socket.IO adapters for multiple server instances, and implementing comprehensive logging and monitoring. This foundation can be extended to support multiple users, conversation threads, media messaging (MMS), and integration with other communication channels.
Frequently Asked Questions
How to send SMS with Infobip and Node.js
Use the Infobip Node.js SDK and the `/api/send-sms` endpoint. Send a POST request to this endpoint with a JSON body containing the recipient's number (`to`) and the message text (`text`). The server uses your configured Infobip credentials to send the SMS.
What is the Infobip Node.js SDK used for
The @infobip-api/sdk is Infobip's official Node.js library, simplifying interaction with the Infobip API. It handles authentication and provides methods for sending SMS messages and other communication services.
Why does the project use Socket.IO
Socket.IO enables real-time, bidirectional communication between the server and the frontend. It allows the server to push updates about sent and received messages instantly to the user interface, creating a dynamic chat-like experience without constant polling.
When should I use ngrok with Infobip
ngrok is recommended for local development to expose your local server to the internet, allowing Infobip to send webhooks to your development machine. In production, replace the ngrok URL with your application's public URL.
Can I use yarn instead of npm for this project
Yes, you can use yarn. The instructions use npm, but yarn is a compatible package manager. After creating the project, use yarn commands (like `yarn add`, `yarn dev`) instead of their npm equivalents.
How to receive SMS replies with Infobip
Set up a webhook endpoint (`/webhooks/infobip` in this project) and configure this URL in your Infobip number settings. When someone replies to your Infobip number, Infobip will send the message data as a POST request to your webhook, which then pushes the message to the frontend via Socket.IO.
What is the project structure for the two-way SMS app
The project uses a monorepo structure with `backend` and `frontend` directories at the root. The backend contains the Node.js/Express server code, while the frontend contains the Vite/React application.
Why does the backend need CORS configuration
Cross-Origin Resource Sharing (CORS) is essential for the frontend (running on a potentially different port) to communicate with the backend API. The `cors` middleware enables this communication by specifying allowed origins.
How to set up the Infobip webhook for local development
Use ngrok to expose your local server, then paste the HTTPS ngrok forwarding URL followed by `/webhooks/infobip` into the webhook settings in your Infobip number configuration.
What is the purpose of the .env file
The `.env` file stores sensitive configuration data, like your Infobip API Key, Base URL, and sender number. This approach keeps secrets out of version control.
How to handle Infobip webhook security
Basic security can be added with a shared secret header. For stronger security in production, use IP whitelisting and consider Infobip's signature verification methods if available. Do not rely solely on the obscurity of the webhook URL.
What database schema would be suitable for storing messages
A `messages` table could include columns for sender, recipient, message content, direction (inbound/outbound), Infobip status/DLR information, and timestamps. Indexing is crucial for efficient queries. Consider using a trigger to automatically update timestamps.
Why is error handling important in the two-way SMS app
Error handling is essential for robustness. It includes validation of inputs, managing potential Infobip API errors, logging important events, and handling invalid or incomplete data. This enables appropriate responses and debugging.
How to manage long SMS messages with Infobip
Infobip's SDK handles long SMS messages by automatically splitting them into multiple parts. Be aware of the higher credit cost for these concatenated SMS, and ensure the UI provides visibility if needed.