code examples

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

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 .env file.
  • 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:

  1. 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
  2. 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:

bash
mkdir infobip-two-way-sms
cd infobip-two-way-sms

1.2. Set Up the Backend (Node.js/Express)

Navigate into the main directory and create the backend folder, initialize npm, and install dependencies.

bash
# 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 nodemon

Dependencies explained:

  • express: Web server framework
  • socket.io: Real-time communication library (server-side)
  • @infobip-api/sdk: Infobip's official Node.js SDK
  • dotenv: Loads environment variables from .env file
  • cors: 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 (like node_modules and .env)
  • package.json: Project manifest that tracks dependencies and scripts
  • server.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:

env
# 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_STRING

Add node_modules and .env to your .gitignore file:

.gitignore:

text
node_modules
.env
npm-debug.log

1.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.

bash
# 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-client
  • socket.io-client: Real-time communication library (client-side)

Vite scaffolds a React project structure for you inside the frontend folder.

Modify the scripts section in backend/package.json to use nodemon:

backend/package.json:

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:

javascript
// 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:

  1. Initialization: Load environment variables, import necessary modules, and set up Express, HTTP server, and Socket.IO with CORS configured for the frontend URL
  2. Infobip Client: Initialize the @infobip-api/sdk client with your Base URL and API Key
  3. Socket.IO: Listen for client connections and disconnections. Log events for debugging
  4. /health Endpoint: A simple endpoint to check if the server is running
  5. /api/send-sms Endpoint (POST):
    • Receives to (recipient number) and text (message content) from the frontend request body
    • Performs basic validation
    • Uses infobip.channels.sms.send() to send the message via the SDK. Your INFOBIP_NUMBER is used as the from address
    • Logs the response from Infobip
    • Emits a receiveMessage event via Socket.IO to push the sent message back to all connected clients (including the sender) so the UI updates immediately. Mark it as direction: 'outbound' and sender: 'Me'
    • Sends a JSON response back to the initial HTTP request indicating success or failure
  6. /webhooks/infobip Endpoint (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 results array from the Infobip payload
    • For each valid message in the results, it emits a receiveMessage event via Socket.IO, pushing the inbound message details (id, sender, text, timestamp, direction: 'inbound') to all connected clients
    • Sends a 200 OK status 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):

json
{
  "to": "+15551234567",
  "text": "Hello from the app!"
}

Success Response (200 OK, JSON):

json
{
  "success": true,
  "messageId": "some-infobip-message-id-string"
}

Error Response (400 Bad Request, JSON): Missing fields

json
{
  "error": "Missing 'to' or 'text' field"
}

Error Response (500 Internal Server Error, JSON): Infobip API error

json
{
  "error": "Failed to send SMS",
  "details": "Error message from Infobip or SDK"
}

Test with cURL:

bash
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):

json
{
  "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

  1. Log in to your Infobip account
  2. 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
  3. 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
  4. Get your Infobip Number:
    • Navigate to Channels and NumbersNumbers
    • 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:

env
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_secret

4.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.

  1. Start your backend server:

    bash
    # In the backend/ directory
    npm run dev

    Confirm it logs Server listening on port 4000

  2. Start ngrok: Open another terminal window and run:

    bash
    ngrok http 4000

    ngrok displays forwarding URLs. Copy the https URL (e.g., https://random-string.ngrok-free.app).

  3. Configure Infobip Forwarding:

    • Return to the Infobip portal
    • Navigate to Channels and NumbersNumbers
    • 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 like X-Webhook-Secret with the value matching your WEBHOOK_SECRET in .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 Request for missing fields in /api/send-sms.
  • Infobip SDK Errors: The try...catch block around infobip.channels.sms.send catches errors during the API call. It logs detailed error information (if available from the SDK's response) and returns a 500 Internal Server Error to the client.
  • Webhook Validation: Checks for the presence of the results array and logs warnings for invalid formats. Returns 400 Bad Request or 401 Unauthorized (if secret check is enabled and fails).
  • Logging: Uses console.log and console.error to output information about server startup, connections, API calls, responses, webhook receipts, and errors.

Improvements for Production:

  • Structured Logging: Use a library like Winston or Pino for 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, and status.description fields 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 2xx response. 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):

sql
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 with direction: 'outbound', sender: INFOBIP_NUMBER (or 'Me'), recipient: to, and the initial status (e.g., 'PENDING'). Store the messageId.
    • In /webhooks/infobip, when processing an inbound message, insert a record with direction: 'inbound', sender: message.from, recipient: message.to, and the received text/timestamp. Store the messageId.
    • 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 the messageId provided in the DLR payload. This is essential for knowing if a message was actually delivered.
  • 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 (.env locally, secure configuration management in deployment).
    • Ensure your .env file 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.
  • 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., ensure to looks like a phone number). Libraries like Joi or express-validator can 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.
  • Rate Limiting:
    • Protect the /api/send-sms endpoint from abuse. Implement rate limiting using libraries like express-rate-limit to restrict the number of requests a single IP address can make within a specific time window.
  • 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, the cors middleware is currently configured to allow requests only from FRONTEND_URL. Keep this restricted to trusted origins in production.
  • Dependency Security:
    • Regularly update dependencies (npm update) and use tools like npm audit or Snyk to check for known vulnerabilities in your project's 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, and useCallback appropriately 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-window or react-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.