Send SMS with Infobip API in Node.js: Complete Express Integration Guide
Build a robust Node.js application using Express to send SMS messages via the Infobip API. This step-by-step tutorial covers API authentication, 10DLC compliance, error handling, and production-ready logging with Winston.
Code examples16 min read
Frequently asked questions
Use the Infobip SMS API's /sms/2/text/advanced endpoint. Make a POST request to this endpoint with the necessary authorization and request body containing recipient number, message text, and optionally, the sender ID.
Express.js acts as the web framework for the Node.js application. It handles routing, middleware (like authentication), and HTTP request/response management. It simplifies building robust and scalable web APIs for sending SMS.
US regulations require Application-to-Person (A2P) 10DLC messages to be sent under registered campaigns. Registering a brand and campaign with Infobip ensures compliance, preventing message filtering and failure.
Use a custom sender ID only if it's registered and compliant with regulations for your target region. If unsure, rely on Infobip's default configured sender ID to avoid delivery issues.
Install Winston (npm install winston) and configure a logger instance. Use logger.* (e.g., logger.info, logger.error) to replace console.*. Set up transports for different environments (console, file, external services).
The .env file stores sensitive configuration data, such as your Infobip API key, base URL, and any internal API keys for your application. It is crucial for security to never commit the .env file to version control.
Implement comprehensive error handling at each level: service, controller, and global handlers. Log detailed errors server-side using Winston but provide generic error messages to clients. Use Axios interceptors or dedicated retry libraries for handling transient network issues.
The project uses Node.js with Express, Axios for making API requests, dotenv for environment variables, Winston for logging, and the Infobip API for sending SMS messages.
Create a project directory, initialize npm (npm init -y), install required packages (npm install express axios dotenv winston), structure your project (routes, services, controllers), and configure your .env file with credentials.
Input validation prevents sending messages to invalid numbers, exceeding character limits, or passing malicious data to the Infobip API. It improves security and reliability.
Implement retries for transient errors like network issues or 5xx server errors from Infobip. Use libraries like async-retry or axios-retry. Never retry on client errors (4xx) like validation failures.
Organize code into modules with distinct responsibilities: routes for defining API endpoints, controllers for handling requests, services for interacting with external APIs (like Infobip), and middleware for cross-cutting concerns.
The recommended structure includes src/ for source code, containing folders for routes, services, controllers, config, and middleware. The server.js file serves as the main entry point.
Yes, but free trials usually limit sending to the phone number verified during signup. Check Infobip's free trial terms for limitations.
One API. Every channel.
Contact Sales
Build a robust Node.js application using Express to send SMS messages via the Infobip API for marketing campaigns. This step-by-step tutorial covers API authentication, 10DLC compliance, error handling, and production-ready logging with Winston.
You'll create a backend service that accepts requests to send targeted SMS messages, integrates securely with Infobip, handles errors gracefully, and follows production-ready best practices.
What you'll build:
Technology Stack:
System Architecture:
Prerequisites:
Final Outcome:
You'll have a functional Express application with a secured API endpoint (
/api/campaign/send-sms) that:Configuration uses environment variables for security.
1. Node.js Project Setup for Infobip SMS Integration
Initialize the Node.js project and install dependencies.
Create Project Directory: Create a new directory and navigate into it:
mkdir infobip-sms-campaign-app cd infobip-sms-campaign-appInitialize npm: Initialize the project (accept defaults or customize):
Install Dependencies: Install Express, Axios, dotenv, and Winston:
Create Project Structure: Set up the directory structure:
mkdir src mkdir src/routes mkdir src/services mkdir src/controllers mkdir src/config mkdir src/middleware touch src/server.js touch src/routes/campaignRoutes.js touch src/services/infobipService.js touch src/controllers/campaignController.js touch src/config/logger.js touch src/middleware/auth.js touch .env touch .gitignoreDirectory structure:
src/: All source codesrc/routes/: Express route definitionssrc/services/: External service interaction logic (Infobip)src/controllers/: Request handlerssrc/config/: Configuration files (logger)src/middleware/: Custom middleware (authentication)src/server.js: Main application entry point.env: Environment variables (Never commit to Git).gitignore: Files Git should ignoreConfigure
.gitignore: Add these entries to prevent committing sensitive files:Set up Environment Variables: Add placeholders to
.env(replace with actual values later):Environment variables:
PORT: Express server portNODE_ENV: Environment (affects logging behavior)INFOBIP_API_KEY: API key from InfobipINFOBIP_BASE_URL: Your unique base URL (e.g.,xxxxx.api.infobip.com)INTERNAL_API_KEY: Secret key to protect your API endpoint2. Building the Infobip SMS Service with Error Handling
Create a dedicated service to handle all Infobip API interactions. This promotes separation of concerns and improves testability.
Configure Logger (
src/config/logger.js): Set up Winston for structured logging.// src/config/logger.js const winston = require('winston'); const logger = winston.createLogger({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', // Log level based on environment format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), // Log stack traces winston.format.splat(), winston.format.json() // Log in JSON format ), defaultMeta: { service: 'infobip-sms-service' }, // Add service context transports: [ // In production, you might write to files or external services // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }), ], }); // If we're not in production, log to the `console` with a simpler format. if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), })); } module.exports = logger;Edit
src/services/infobipService.js: This file will contain the logic for building the request and calling the Infobip ""Send SMS"" API endpoint (/sms/2/text/advanced), now using the logger.// src/services/infobipService.js const axios = require('axios'); const logger = require('../config/logger'); // Use the configured logger // Retrieve Infobip credentials from environment variables const INFOBIP_API_KEY = process.env.INFOBIP_API_KEY; const INFOBIP_BASE_URL = process.env.INFOBIP_BASE_URL; /** * Builds the full API endpoint URL. * @returns {string} The complete URL for the Infobip Send SMS API. * @throws {Error} If INFOBIP_BASE_URL is not set. */ const buildUrl = () => { if (!INFOBIP_BASE_URL) { logger.error('INFOBIP_BASE_URL environment variable is not set.'); throw new Error('Infobip configuration error: Base URL is missing.'); } return `https://${INFOBIP_BASE_URL}/sms/2/text/advanced`; }; /** * Constructs the necessary HTTP headers for Infobip API authentication. * @returns {object} Headers object including Content-Type and Authorization. * @throws {Error} If INFOBIP_API_KEY is not set. */ const buildHeaders = () => { if (!INFOBIP_API_KEY) { logger.error('INFOBIP_API_KEY environment variable is not set.'); throw new Error('Infobip configuration error: API Key is missing.'); } return { 'Authorization': `App ${INFOBIP_API_KEY}`, 'Content-Type': 'application/json', 'Accept': 'application/json', }; }; /** * Constructs the request body payload for the Infobip Send SMS API. * @param {string} destinationNumber - The recipient's phone number in E.164 format (e.g., +447...). * @param {string} messageText - The content of the SMS message. * @param {string} [sender] - Optional: The sender ID (Alphanumeric or Short Code/Long Code). **Crucially, this MUST be a valid, registered Sender ID appropriate for the destination country and regulations (e.g., pre-registered 10DLC for US A2P). Using an invalid/unregistered sender will likely cause message failure. Defaulting to 'InfoSMS' is often NOT valid for many regions/use cases.** Verify requirements with Infobip documentation and local regulations. * @returns {object} The request body payload. * @throws {Error} If destinationNumber or messageText is missing. */ const buildRequestBody = (destinationNumber, messageText, sender) => { if (!destinationNumber || !messageText) { logger.warn('Attempted to build request body with missing destination or text.', { destinationNumber, messageText }); throw new Error('Destination number and message text are required.'); } const payload = { messages: [ { destinations: [{ to: destinationNumber }], text: messageText, // Only include 'from' if a valid sender is provided. // Relying on Infobip's default/configured sender might be safer if unsure. ...(sender && { from: sender }), // Add campaignId, notifyUrl etc. here if required by your campaign setup }, ], // tracking: { // Optional: Add tracking if needed for campaign analytics // track: 'URL', // type: 'MY_CAMPAIGN_TRACKER' // } }; logger.debug('Built Infobip request payload.', { payload }); // Log payload at debug level return payload; }; /** * Sends an SMS message using the Infobip API. * @param {string} destinationNumber - The recipient's phone number (E.164 format recommended). * @param {string} messageText - The message content. * @param {string} [sender] - Optional: The sender ID (see buildRequestBody docs for critical usage notes). * @returns {Promise<object>} A promise that resolves with the relevant part of the Infobip API response (e.g., message status and ID). * @throws {Error} If the API call fails or returns an error status. */ const sendSms = async (destinationNumber, messageText, sender) => { let url, headers, requestBody; try { url = buildUrl(); headers = buildHeaders(); requestBody = buildRequestBody(destinationNumber, messageText, sender); logger.info(`Attempting to send SMS to ${destinationNumber} via Infobip...`); const response = await axios.post(url, requestBody, { headers: headers }); logger.info('Infobip API Response Status:', { status: response.status }); // Log only essential parts of the response, not the whole potentially large object logger.debug('Infobip API Response Body:', { data: response.data }); // Basic check for success indication in the response structure const messageInfo = response.data?.messages?.[0]; if (messageInfo) { const status = messageInfo.status; if (status?.groupName === 'PENDING' || status?.groupName === 'DELIVERED') { logger.info(`SMS submitted successfully for ${destinationNumber}. Status: ${status.name}, Message ID: ${messageInfo.messageId}`); } else { logger.warn(`SMS submission for ${destinationNumber} resulted in status: ${status?.name || 'Unknown'} (${status?.description || 'N/A'}), Group: ${status?.groupName || 'Unknown'}`, { messageId: messageInfo.messageId }); } // Return curated info return { messageId: messageInfo.messageId, status: status, to: messageInfo.to }; } else { logger.warn(`Unexpected response structure received from Infobip for ${destinationNumber}.`, { responseData: response.data }); // Return raw data if structure is unknown, but flag it return { rawResponse: response.data, warning: ""Unexpected response structure"" }; } } catch (error) { logger.error('Error sending SMS via Infobip.', { message: error.message, url: url, // Log the URL attempted destination: destinationNumber, // Log relevant context stack: error.stack, // Include stack trace }); let errorMessage = 'Failed to send SMS due to an internal error.'; // Generic message if (error.response) { // The request was made and the server responded with a non-2xx status code logger.error('Infobip API Error Response:', { status: error.response.status, headers: error.response.headers, data: error.response.data, // Log the full error body from Infobip }); // Attempt to extract a meaningful error message from Infobip's response const serviceException = error.response.data?.requestError?.serviceException; if (serviceException) { errorMessage = `Infobip API Error: ${serviceException.text || 'Unknown error'} (ID: ${serviceException.messageId || 'N/A'})`; } else { errorMessage = `Infobip API request failed with status ${error.response.status}.`; } throw new Error(errorMessage); // Throw curated error message } else if (error.request) { // The request was made but no response was received logger.error('Infobip Error: No response received.', { request: error.request }); errorMessage = 'No response received from Infobip API. Check network connectivity and Base URL.'; throw new Error(errorMessage); } else { // Something happened in setting up the request that triggered an Error errorMessage = `Error setting up Infobip request: ${error.message}`; throw new Error(errorMessage); // Throw the setup error message } } }; module.exports = { sendSms, // Expose helpers for potential testing _buildUrl: buildUrl, _buildHeaders: buildHeaders, _buildRequestBody: buildRequestBody };console.*withlogger.*calls using the Winston logger. Added more contextual information to logs.senderparameter in the JSDoc and implementation notes. It's now optional and only included if explicitly provided, encouraging reliance on Infobip's configured defaults if unsure.messageId,status,to) instead of the full raw response on success.3. Creating the Express API Endpoint
Create the Express controller and route that will use the
infobipService.Edit
src/controllers/campaignController.js: This controller handles the incoming HTTP request, performs validation, calls the service, and sends back a curated response.// src/controllers/campaignController.js const infobipService = require('../services/infobipService'); const logger = require('../config/logger'); // Basic E.164 format regex (simplified: starts with +, then digits) // For robust validation, consider libraries like 'libphonenumber-js' const E164_REGEX = /^\+[1-9]\d{1,14}$/; /** * Controller function to handle sending an SMS message. * POST /api/campaign/send-sms * Body: { "to": "+14155552671", "text": "messageContent", "sender": "OptionalSenderID" } */ const sendCampaignSms = async (req, res) => { const { to, text, sender } = req.body; // Include optional sender // Input Validation const errors = []; if (!to) { errors.push('Missing required field: `to` (phone number)'); } else if (!E164_REGEX.test(to)) { // Add more robust validation if needed (e.g., using libphonenumber-js) errors.push('Invalid phone number format for `to`. Use E.164 format (e.g., +14155552671).'); } if (!text) { errors.push('Missing required field: `text` (message content)'); } else if (text.length > 1600) { // Example: Arbitrary limit to prevent abuse errors.push('Message text exceeds maximum allowed length (1600 characters).'); } // Optional: Validate 'sender' format if provided if (errors.length > 0) { logger.warn('Invalid request to send SMS.', { errors, requestBody: req.body }); return res.status(400).json({ success: false, message: 'Invalid request payload.', errors: errors, }); } try { // Pass validated data to the service const result = await infobipService.sendSms(to, text, sender); // Pass sender if provided // Service logs details. Controller sends client-friendly, curated response. res.status(200).json({ success: true, message: 'SMS submitted successfully to Infobip.', // Return only necessary info, e.g., messageId for tracking details: { messageId: result?.messageId, statusGroup: result?.status?.groupName, recipient: result?.to, } }); } catch (error) { // The service layer already logged the detailed technical error. // Log the error context at the controller level. logger.error('Error processing send SMS request in controller.', { errorMessage: error.message, recipient: to, // Log context stack: error.stack // Log stack trace for controller-level issues }); // Send a generic server error response to the client. Avoid exposing internal details. res.status(500).json({ success: false, message: 'Failed to send SMS due to a server error. Please check server logs for details or contact support.', // Do NOT include error.message directly here in production // errorId: 'SOME_TRACKING_ID' // Optional: provide an ID for correlation }); } }; module.exports = { sendCampaignSms, };tofield and a length check fortext. Returns specific error messages using backticks for field names.messageId,statusGroup, andrecipientfrom the service result. Error response is generic, advising to check logs, and explicitly does not include the rawerror.message.Edit
src/routes/campaignRoutes.js: Define the Express route and link it to the controller function.// src/routes/campaignRoutes.js const express = require('express'); const campaignController = require('../controllers/campaignController'); // Optional: Import authentication middleware // const { requireApiKey } = require('../middleware/auth'); const router = express.Router(); // Define the route for sending SMS // POST /api/campaign/send-sms // Consider adding authentication middleware here: // router.post('/send-sms', requireApiKey, campaignController.sendCampaignSms); router.post('/send-sms', campaignController.sendCampaignSms); // You could add more campaign-related routes here (e.g., getting campaign status) module.exports = router;Edit
src/server.js: Set up the main Express application, load environment variables, configure middleware (including logging), and mount the routes.// src/server.js // Load environment variables from .env file FIRST require('dotenv').config(); const express = require('express'); const helmet = require('helmet'); // For security headers const campaignRoutes = require('./routes/campaignRoutes'); const logger = require('./config/logger'); // Import the logger // Create the Express application const app = express(); // Security Middleware app.use(helmet()); // Set various security HTTP headers app.use(express.json()); // Middleware to parse JSON request bodies app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies // HTTP Request Logging Middleware (using Winston) app.use((req, res, next) => { // Log basic request info logger.info(`Incoming Request: ${req.method} ${req.path}`, { ip: req.ip, userAgent: req.get('User-Agent'), body: req.body // Be cautious logging full bodies in production (PII/secrets) }); // Log response finish res.on('finish', () => { logger.info(`Request Finished: ${req.method} ${req.path} - Status: ${res.statusCode}`, { duration: `${Date.now() - res.locals.startTime}ms` // Requires setting startTime earlier }); }); res.locals.startTime = Date.now(); // Store start time for duration calculation next(); }); // Mount the campaign routes under the /api/campaign prefix // Make sure to apply any necessary authentication middleware here or in the router itself app.use('/api/campaign', campaignRoutes); // Simple health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); // 404 Handler for undefined routes app.use((req, res, next) => { res.status(404).json({ message: 'Resource not found at ' + req.originalUrl }); }); // Centralized Error Handler Middleware (Logs errors using Winston, sends generic response) // This catches errors passed via next(err) or uncaught synchronous errors in route handlers // Note: Asynchronous errors need to be caught and passed to next() or handled in controllers/services app.use((err, req, res, next) => { // Log the error using Winston logger.error('Unhandled error caught by central handler:', { message: err.message, stack: err.stack, url: req.originalUrl, method: req.method, ip: req.ip, }); // Avoid sending stack traces or detailed errors to the client in production const statusCode = err.status || 500; // Use error status or default to 500 const responseMessage = process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message; // Show more detail in dev res.status(statusCode).json({ message: 'An unexpected error occurred.', error: responseMessage // Provide minimal info }); }); // Start the server const PORT = process.env.PORT || 3000; // Default to 3000 if PORT not set app.listen(PORT, () => { logger.info(`Server listening on port ${PORT}`); logger.info(`Current environment: ${process.env.NODE_ENV || 'development'}`); // Verify essential environment variables are loaded if (!process.env.INFOBIP_API_KEY || !process.env.INFOBIP_BASE_URL) { logger.warn('Infobip API Key or Base URL not found in environment variables. API calls will fail.'); } if (!process.env.INTERNAL_API_KEY && process.env.NODE_ENV === 'production') { logger.warn('INTERNAL_API_KEY is not set. The API endpoint security is compromised in production!'); } }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection at:', { promise, reason: reason.stack || reason }); // Optionally exit process: process.exit(1); }); // Handle uncaught exceptions process.on('uncaughtException', (error) => { logger.error('Uncaught Exception:', { message: error.message, stack: error.stack }); // Graceful shutdown logic might go here process.exit(1); // Mandatory exit after uncaught exception });dotenv.config(): Still first.helmetmiddleware.unhandledRejectionanduncaughtException.Update
package.json(Optional but Recommended): Add scripts to easily start the server.// package.json (add inside "scripts") "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", // If you install nodemon: npm install --save-dev nodemon "lint": "eslint .", // Example if using ESLint "test": "echo \"Error: no test specified\" && exit 1" // Keep or replace },4. Infobip API Authentication and Configuration
This section details how to get your Infobip credentials and configure the application.
Obtain Infobip API Key and Base URL:
[your-unique-id].api.infobip.com). Copy this value.nodejs-campaign-app). Copy the generated key value immediately.Update
.envFile: Replace the placeholder values in your.envfile with the actual Base URL, API Key, and a secure internal API key.Environment Variable Purpose:
PORT: Network port the Express server listens on.NODE_ENV: Set toproductionin deployed environments.INFOBIP_API_KEY: Authenticates requests to the Infobip API (Authorization: App <key>).INFOBIP_BASE_URL: Correct domain for Infobip API endpoints (e.g.,yoursubdomain.api.infobip.com). Does not includehttps://.INTERNAL_API_KEY: Used to secure your application's API endpoint (see Section 7 - Note: Section 7 is not present in the provided text).Campaign Registration (Crucial Reminder):
campaignIdin the API request body (src/services/infobipService.js). Verify the requirements for your specific approved campaign. Failure to comply is a primary reason for message delivery issues.5. Production Error Handling with Winston Logging
Our implementation now includes structured logging with Winston and improved error handling. Let's discuss retries.
infobipService). Detailed technical info is logged there using Winston. A curated error is thrown upwards. The controller catches this, logs the context, and sends a generic, safe error response to the client. Centralized handlers catch uncaught exceptions/rejections.src/config/logger.js. It provides structured JSON logging, different levels based onNODE_ENV, and timestamping.503 Service Unavailable) might cause transient failures. Implementing retries can improve reliability for these cases.async-retryoraxios-retry.axios-retryintegrates directly with Axios, whileasync-retryis a more general-purpose promise retry library.