Send SMS with Node.js and Express: Complete Infobip API Integration Guide
Learn how to build a Node.js SMS API using Express and Infobip. Step-by-step tutorial covering setup, authentication, error handling, and best practices for sending SMS programmatically.
Build a Node.js application using Express to send SMS messages via the Infobip API. This tutorial covers project setup, implementation, API key management, error handling, and testing.
You'll create a functional Express API endpoint that accepts a phone number and message text, then dispatches the SMS through Infobip. Use this as a foundation for integrating SMS functionality into larger applications.
Project Overview and Goals
Goal: Create a backend service that sends SMS messages programmatically.
Problem Solved: Automates transactional SMS sending, eliminating manual processes and enabling system integration.
Technologies Used:
Node.js: JavaScript runtime for server-side applications (requires v18.x or v20.x LTS). Chosen for asynchronous I/O and extensive npm ecosystem.
Express: Minimal Node.js web framework. Chosen for simple API route setup.
Axios: Promise-based HTTP client. Chosen for reliable Infobip API requests.
dotenv: Loads environment variables from .env files. Chosen for secure API key management.
Infobip API: Third-party SMS gateway service (API v2).
Architecture:
Code
+-----------------+ +-----------------+ +-----------------+
| Client (e.g. UI,| ---> | Node.js/Express | ---> | Infobip API |
| Postman, curl) | | API Service | | (SMS Gateway) |
+-----------------+ +-----------------+ +-----------------+
(HTTP POST (Uses Axios to call (Sends SMS to
request with Infobip API with recipient)
number/text) API Key & Base URL)
Outcome: A running Express server with endpoint POST /api/v1/sms/send that triggers SMS delivery via Infobip.
Prerequisites:
Node.js v18.x or v20.x LTS and npm installed. Use Node Version Manager (nvm) to manage versions.
Active Infobip account. Create one at infobip.com.
Basic JavaScript, Node.js, REST APIs, and command-line knowledge.
Free Trial Limitation: Infobip trial accounts can only send SMS to the phone number verified during signup. Testing other numbers fails until you fund the account.
Setting Up Your Node.js SMS Project
Initialize your Node.js project and install dependencies.
Create Project Directory:
Open your terminal and create a new directory. Navigate into it.
Bash
mkdir infobip-sms-sender
cd infobip-sms-sender
Initialize Node.js Project:
Run npm init to create package.json. Accept defaults with the -y flag.
Bash
npm init -y
This creates package.json, which tracks project metadata and dependencies.
Install Dependencies:
Install express for the web server, axios for HTTP requests, and dotenv for environment variables.
Bash
npm install express axios dotenv
This downloads packages to node_modules and lists them in package.json.
Set up Project Structure:
Create this directory structure within infobip-sms-sender:
INFOBIP_API_KEY: Your secret API key for Infobip authentication.
INFOBIP_BASE_URL: Your account-specific domain for API access.
PORT: Port number for your Express server.
Why .env? Storing API keys in code is insecure and complicates deployment. Environment variables let you configure applications differently across environments (development, testing, production) without code changes. dotenv loads these into process.env.
Create src/services/infobipService.js:
This file builds requests and calls the Infobip API.
JavaScript
// src/services/infobipService.jsconst axios = require('axios');
// Helper to build the full API URLconstbuildUrl = (domain) => {
if (!domain) {
thrownewError('Infobip domain (Base URL) is mandatory in config');
}
// Ensure domain doesn't have protocol prefix for URL constructionconst cleanDomain = domain.replace(/^https?:\/\//, '');
return`https://${cleanDomain}/sms/2/text/advanced`;
};
// Helper to build the required HTTP headers for Infobip APIconstbuildHeaders = (apiKey) => {
if (!apiKey) {
thrownewError('Infobip API Key is mandatory in config');
}
return {
'Authorization': `App ${apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
};
// Helper to construct the request body according to Infobip specsconstbuildRequestBody = (destinationNumber, message) => {
if (!destinationNumber || !message) {
thrownewError('Destination number and message text are mandatory');
}
// Validate E.164 format: digits only, no '+' prefix// Production apps should use libphonenumber for robust validationif (!/^\d{7,15}$/.test(destinationNumber)) {
console.warn(`Destination number ${destinationNumber} may not conform to E.164 international format (7-15 digits, no '+' prefix).`);
}
const destinationObject = {
to: destinationNumber,
};
const messageObject = {
destinations: [destinationObject],
text: message,
// Add 'from' if you have a registered sender ID// from: "YourSenderID"
};
return {
messages: [messageObject],
};
};
// Helper function for basic argument validationconstvalidateNotEmpty = (value, fieldName) => {
if (!value) {
thrownewError(`${fieldName} parameter is mandatory`);
}
};
/**
* Sends an SMS message using the Infobip API.
* @param {string} destinationNumber - Recipient phone number in E.164 format (digits only, no '+').
* @param {string} message - SMS text content (160 chars for GSM-7, 70 chars for Unicode/UCS-2).
* @returns {Promise<object>} - Resolves with success response or rejects with error info.
*/constsendSms = async (destinationNumber, message) => {
const apiKey = process.env.INFOBIP_API_KEY;
const domain = process.env.INFOBIP_BASE_URL;
validateNotEmpty(domain, 'Infobip Base URL (INFOBIP_BASE_URL in .env)');
validateNotEmpty(apiKey, 'Infobip API Key (INFOBIP_API_KEY in .env)');
validateNotEmpty(destinationNumber, 'destinationNumber');
validateNotEmpty(message, 'message');
const url = buildUrl(domain);
const headers = buildHeaders(apiKey);
const requestBody = buildRequestBody(destinationNumber, message);
console.log(`Attempting to send SMS to ${destinationNumber} via Infobip...`);
try {
const response = await axios.post(url, requestBody, { headers });
console.log('Infobip API call successful.');
returnparseSuccessResponse(response);
} catch (error) {
console.error('Infobip API call failed:', error.message);
throwparseFailedResponse(error);
}
};
// Parses the successful response from InfobipconstparseSuccessResponse = (axiosResponse) => {
const responseBody = axiosResponse.data;
const singleMessageResponse = responseBody.messages && responseBody.messages[0];
if (!singleMessageResponse) {
console.warn('Infobip success response structure differs from expected format.', responseBody);
return { success: true, data: responseBody };
}
return {
success: true,
messageId: singleMessageResponse.messageId,
status: singleMessageResponse.status.name,
description: singleMessageResponse.status.description,
category: singleMessageResponse.status.groupName,
to: singleMessageResponse.to,
};
};
// Parses the error response from Infobip or network errorsconstparseFailedResponse = (axiosError) => {
let errorInfo = {
success: false,
errorMessage: 'An unknown error occurred.',
details: null,
statusCode: null,
};
if (axiosError.response) {
// Error response from Infobip API (e.g., 4xx, 5xx)
errorInfo.statusCode = axiosError.response.status;
const responseBody = axiosError.response.data;
if (responseBody && responseBody.requestError && responseBody.requestError.serviceException) {
errorInfo.errorMessage = responseBody.requestError.serviceException.text || 'Infobip service exception';
errorInfo.details = responseBody.requestError.serviceException;
} else {
errorInfo.errorMessage = `Infobip API Error: Status ${errorInfo.statusCode}`;
errorInfo.details = responseBody;
}
} elseif (axiosError.request) {
errorInfo.errorMessage = 'Network Error: No response received from Infobip API.';
} else {
errorInfo.errorMessage = `Request Setup Error: ${axiosError.message}`;
}
errorInfo.rawError = axiosError.message;
return errorInfo;
};
module.exports = {
sendSms,
};
async/await: Makes asynchronous operations cleaner than Promise chains.
Separate helpers: Breaking logic into buildUrl, buildHeaders, buildRequestBody, parseSuccessResponse, parseFailedResponse improves readability, maintainability, and testability.
E.164 Format Validation: Enhanced validation checks for 7-15 digit format per E.164 international standard.
Creating the Express API Routes and Controller
Define the Express route and controller that receives requests and uses infobipService.
Create src/controllers/smsController.js:
This controller handles request logic.
JavaScript
// src/controllers/smsController.jsconst infobipService = require('../services/infobipService');
consthandleSendSms = async (req, res) => {
const { to, text } = req.body;
if (!to) {
return res.status(400).json({ success: false, message: 'Missing required field: "to" (destination phone number)' });
}
if (!text) {
return res.status(400).json({ success: false, message: 'Missing required field: "text" (message content)' });
}
// Enhanced validation: Check E.164 format (7-15 digits)const cleanNumber = to.replace(/[\s+()-]/g, '');
if (!/^\d{7,15}$/.test(cleanNumber)) {
console.warn(`Received invalid phone number format: ${to}. Must be E.164 format (7-15 digits, e.g., 15551234567).`);
return res.status(400).json({
success: false,
message: 'Invalid "to" field format. Use E.164 international format: country code + number, 7-15 digits total (e.g., 15551234567).'
});
}
try {
console.log(`Controller received request to send SMS to: ${to}`);
const result = await infobipService.sendSms(cleanNumber, text);
console.log('SMS processed successfully by Infobip service:', result);
return res.status(200).json(result);
} catch (error) {
console.error('Error in handleSendSms controller:', error);
const statusCode = error.statusCode || 500;
return res.status(statusCode).json({
success: false,
message: error.errorMessage || 'Failed to send SMS due to an internal error.',
details: error.details || error.rawError,
});
}
};
module.exports = {
handleSendSms,
};
Input Validation: Validates required fields and enforces E.164 format (7-15 digits).
Format Cleaning: Strips common phone number formatting characters before validation.
Error Responses: Returns appropriate HTTP status codes with detailed error messages.
Create src/routes/smsRoutes.js:
This file defines the API endpoint.
JavaScript
// src/routes/smsRoutes.jsconst express = require('express');
const smsController = require('../controllers/smsController');
const router = express.Router();
// Define the route for sending SMS// POST /api/v1/sms/send
router.post('/send', smsController.handleSendSms);
module.exports = router;
Set up the Main Application File (app.js):
Initialize Express, load environment variables, configure middleware, and mount routes.
JavaScript
// app.jsconst express = require('express');
const dotenv = require('dotenv');
const smsRoutes = require('./src/routes/smsRoutes');
// Load environment variables from .env file
dotenv.config();
const app = express();
// Middleware to parse JSON request bodies
app.use(express.json());
// Middleware for basic logging
app.use((req, res, next) => {
console.log(`[${newDate().toISOString()}] ${req.method}${req.url}`);
next();
});
// Mount the SMS routes under the /api/v1/sms prefix
app.use('/api/v1/sms', smsRoutes);
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: newDate().toISOString() });
});
// 404 handler for undefined routes
app.use((req, res, next) => {
res.status(404).json({ success: false, message: 'Resource not found' });
});
// Error handler middleware
app.use((err, req, res, next) => {
console.error('Unhandled Error:', err);
res.status(500).json({ success: false, message: 'An unexpected internal server error occurred.' });
});
constPORT = process.env.PORT || 3000;
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
console.log(`Infobip Base URL configured: ${process.env.INFOBIP_BASE_URL}`);
if (process.env.INFOBIP_API_KEY) {
console.log('Infobip API Key is configured.');
} else {
console.warn('WARNING: Infobip API Key (INFOBIP_API_KEY) is missing in .env file!');
}
});
dotenv.config(): Loads .env variables into process.env before accessing them.
express.json(): Parses incoming JSON payloads (available via req.body).
Route Mounting: Mounts smsRoutes under /api/v1/sms, creating final endpoint http://localhost:3000/api/v1/sms/send.
Health Check: Includes /health endpoint for monitoring.
Configuring Infobip API Credentials
Integration happens in src/services/infobipService.js using credentials from .env.
Configuration Files:
.env: Stores INFOBIP_API_KEY and INFOBIP_BASE_URL.
INFOBIP_API_KEY: (String, Required) Your unique secret key from Infobip. Used in Authorization: App <key> header.
INFOBIP_BASE_URL: (String, Required) Your account-specific domain (e.g., xyz123.api.infobip.com). Used to construct API endpoint URLs.
PORT: (Number, Optional, Default: 3000) Your local server port.
Error Handling and Logging Best Practices
Error handling is built into the service and controller:
Strategy:
infobipService uses try...catch around axios calls.
Calls parseFailedResponse to create standardized error objects for API errors (4xx/5xx) or network errors.
Service throws parsed error objects.
smsController catches errors and inspects statusCode and errorMessage to return appropriate HTTP responses.
Uses console.log and console.error for basic logging.
Logging:
Requests logged in app.js.
Service calls and outcomes logged in infobipService.js.
Errors logged in service and controller catch blocks.
Production Enhancement: Use Winston or Pino for structured logging with different levels and external service routing.
Retry Mechanisms:
Not implemented in this basic example.
Production Enhancement: For transient network errors or rate limit responses (429 Too Many Requests), implement retries with exponential backoff using axios-retry or custom logic.
Testing Errors:
Invalid API Key: Change INFOBIP_API_KEY in .env to incorrect value. Expect 401 Unauthorized.
Invalid Base URL: Change INFOBIP_BASE_URL to non-existent domain. Expect network error (ENOTFOUND or timeout).
Missing Fields: Send request without to or text. Expect 400 Bad Request.
Invalid Phone Number: Send clearly invalid to value (e.g., 'abcde'). Expect 400 Bad Request from controller validation.
Security Best Practices for SMS APIs
Basic security is included; production systems need more.
API Key Security: Handled via .env and .gitignore. Critical.
Input Validation: Enhanced E.164 format validation in smsController.js.
Production Enhancement: Use joi or express-validator for schema validation, type checking, and length limits.
Rate Limiting:Not implemented. Essential for public APIs to prevent abuse.
Production Enhancement: Use express-rate-limit middleware to limit requests per IP or user.
Common Vulnerabilities:
Injection: Always validate and sanitize input used in database queries or commands.
Authentication/Authorization: This endpoint is unauthenticated. For production, implement API key validation, JWT tokens, or other auth methods.
HTTPS: Deploy behind reverse proxy (Nginx, Caddy) configured for HTTPS, or use platforms (Heroku, Render) handling TLS termination.
Understanding SMS Format Requirements and Limitations
Phone Number Formatting: Infobip API expects E.164 international format (e.g., 15551234567 for US, 447911123456 for UK). Enhanced validation enforces 7-15 digit format. Production apps should use libphonenumber for robust validation and formatting.
Message Encoding & Length:
GSM-7 encoding: 160 characters per SMS segment
UCS-2/UTF-16 (Unicode): 70 characters per segment for non-Latin scripts
Infobip handles concatenation for longer messages; billing is per segment. This example doesn't handle encoding or splitting.
Sender ID: The from field in the Infobip payload specifies sender ID (alphanumeric or phone number). Requires pre-registration and approval with Infobip. The example code comments out from.
Free Trial Limitations: Infobip trial accounts only send SMS to the registered phone number. Sending to other numbers fails until account funding.
Delivery Status Webhooks: Infobip supports webhooks for delivery reports. Configure webhook URLs in the Infobip Portal to receive real-time status updates (DELIVERED, FAILED, etc.).
Performance Optimization for High-Volume SMS
For simple, single API calls per request, performance bottlenecks are unlikely. Main latency is network round-trip to Infobip.
Connection Pooling:axios and Node.js HTTP agent handle connection pooling/reuse automatically.
Caching: Not applicable for unique transactional SMS messages.
Asynchronous I/O: Node.js non-blocking I/O keeps server responsive during Infobip API waits.
Production Enhancement: For high volumes, monitor event loop lag, CPU, and memory. Consider horizontal scaling (multiple instances behind load balancer). Analyze Infobip API response times.
Monitoring Your Node.js SMS Application
Basic logging is included. Production systems need robust monitoring.
Health Checks:/health endpoint provides basic status.
Logging:console.log/console.error provide minimal visibility.
Production Enhancement: Integrate Winston/Pino for structured logging to Datadog, Logz.io, or ELK stack.
Metrics: Track request rates, error rates, and latency (especially Infobip API calls).
Production Enhancement: Use prom-client for Prometheus metrics or integrate APM tools (Datadog APM, New Relic, Dynatrace).
Error Tracking: Capture and aggregate unhandled exceptions.
Production Enhancement: Use Sentry or Bugsnag for real-time error capture with stack traces.
Dashboards: Visualize key metrics (requests/sec, error rate, Infobip latency, SMS success/failure rates). Use Grafana with Prometheus, Datadog, or New Relic.
Alerting: Configure alerts for thresholds (error rate > 5%, Infobip latency > 2s, health check failures).
Troubleshooting and Caveats
Common Errors:
401 Unauthorized (from Infobip): Incorrect INFOBIP_API_KEY. Verify key in .env and Infobip dashboard.
Network Error/ENOTFOUND/ECONNREFUSED/Timeout: Incorrect INFOBIP_BASE_URL, DNS issues, firewall blocking outgoing connections, or temporary Infobip unavailability. Verify Base URL and network connectivity.
400 Bad Request (from Infobip): Invalid input. Check:
to number format (must be E.164: 7-15 digits, no '+').
Missing required fields in JSON payload.
Invalid from sender ID (if used).
Message content issues (unsupported characters).
Review details field in error response for specific Infobip error codes (e.g., Invalid destination address).
TypeError: Cannot read property '...' of undefined: Missing environment variables (dotenv.config() not called early, variables missing in .env) or unexpected Infobip response structures. Add logging to trace variable values.
Frequently Asked Questions (FAQ)
Q: What Node.js versions are compatible with Infobip?
A: Node.js v18.x or v20.x LTS versions are recommended. Earlier versions may work but lack current security patches and performance improvements.
Q: Do I need a paid Infobip account to send SMS?
A: Free trial accounts can send SMS to verified phone numbers only. For production use or testing with multiple numbers, you'll need to fund your account.
Q: What phone number format does Infobip require?
A: Infobip requires E.164 international format: country code + national number, 7-15 digits total, without the '+' prefix. Example: 15551234567 (US) or 447911123456 (UK).
Q: How many characters can I send in one SMS?
A: Standard SMS supports 160 characters (GSM-7 encoding) or 70 characters (UCS-2/Unicode for non-Latin scripts). Longer messages are automatically split into multiple segments, with separate billing per segment.
Q: How do I track SMS delivery status?
A: Configure webhooks in the Infobip Portal to receive real-time delivery reports (DELIVERED, FAILED, etc.). The messageId returned from the send request can be used to query delivery status via Infobip's API.
Q: Can I customize the sender ID for my SMS messages?
A: Yes, use the from field in the message payload. Alphanumeric sender IDs require pre-registration and approval with Infobip. Some countries restrict sender ID customization.
Q: How do I handle rate limits with Infobip?
A: Implement rate limiting in your application using express-rate-limit. For transient rate limit errors (429 responses), add retry logic with exponential backoff using libraries like axios-retry.
Q: Is this code production-ready?
A: This tutorial provides a foundation. For production, add: robust input validation (use joi or express-validator), comprehensive error handling, retry mechanisms, structured logging (Winston/Pino), monitoring (Datadog/New Relic), rate limiting, and authentication/authorization.
Q: How do I test my integration without sending real SMS?
A: Use your verified trial phone number for testing, or upgrade to a paid account and use test numbers. Infobip also offers simulation modes in some regions—check their documentation.
Q: What's the typical latency for sending SMS through Infobip?
A: Network latency to Infobip's API typically ranges from 200ms to 2 seconds depending on your location and network conditions. Actual SMS delivery to the recipient's device depends on carrier networks (usually 1-10 seconds).
Q: Can I send SMS to international numbers?
A: Yes, Infobip supports international SMS delivery to 190+ countries. Ensure phone numbers are in proper E.164 format with the correct country code. Pricing varies by destination country.
Q: How do I handle special characters in SMS messages?
A: GSM-7 encoding supports basic Latin characters and common symbols. For special characters (emojis, non-Latin scripts), messages use UCS-2 encoding, reducing the character limit to 70 per segment. Infobip handles encoding automatically based on message content.
Frequently asked questions
This guide details setting up a Node.js application with Express to send SMS messages using the Infobip API. It covers project setup, API integration, error handling, and testing. You'll create an API endpoint that accepts a phone number and message, then dispatches the SMS via Infobip.
The Infobip API is a third-party service used in this tutorial to send SMS messages programmatically. It handles the actual delivery of the SMS to the recipient's phone number. This automation is useful for transactional messages, alerts, and user communication within applications.
Express.js simplifies the process of building a web server and API endpoints in Node.js. Its minimal structure and routing capabilities make it ideal for creating the SMS sending endpoint required in this tutorial.
Phone number validation should occur before sending any SMS via the Infobip API. The controller in this example performs a basic check but stricter format validation is recommended. It's crucial for preventing errors and ensuring deliverability, especially when using international numbers.
Yes, you can use an Infobip free trial account, but it has limitations. Testing is typically restricted to sending SMS messages only to the phone number verified during signup. Other numbers will likely fail until you upgrade to a paid account.
Create a .env file in your project's root directory. Add your INFOBIP_API_KEY and INFOBIP_BASE_URL obtained from your Infobip account dashboard. The dotenv package loads these variables into process.env for secure access within your Node.js application.
Axios is a promise-based HTTP client used to make requests to the Infobip API. It simplifies sending the POST request with the recipient's number and message text to trigger the SMS sending process. The example code demonstrates how to use axios and parse responses.
Error handling involves using try...catch blocks around the API call with axios within the infobipService and controller. The parseFailedResponse function helps standardize error objects, and you can add logging and retry mechanisms. The example includes basic error parsing and response handling.
This tutorial focuses on demonstrating a simple, stateless SMS sending function where incoming requests are handled directly via the Infobip API. A database isn't needed to store application state or message details, reducing complexity for the basic example.
The tutorial recommends a structure with controllers, routes, and services directories within a src folder. Controllers manage requests, routes define API endpoints, and services encapsulate the Infobip API interaction logic.
You need Node.js and npm (or yarn) installed, an active Infobip account, and basic understanding of JavaScript, Node.js, REST APIs, and command-line usage. Familiarity with environment variables and asynchronous operations is also beneficial.
After setting up the project and configuring your .env file, you can test by sending a POST request to the /api/v1/sms/send endpoint with a valid phone number and message text in the request body. Tools like Postman or curl can be used to make these test requests.
Protecting your API key is critical. The .env file keeps it out of version control. Input validation is essential to prevent issues and robust phone number formatting is recommended. Additional security measures like rate limiting and HTTPS in production are important considerations.
Log in to your Infobip account portal and navigate to the main dashboard or homepage. Your unique API Key and Base URL will be displayed there. Copy these values into the .env file in your project.
Build a Node.js application using Express to send SMS messages via the Infobip API. This tutorial covers project setup, implementation, API key management, error handling, and testing.
You'll create a functional Express API endpoint that accepts a phone number and message text, then dispatches the SMS through Infobip. Use this as a foundation for integrating SMS functionality into larger applications.
Project Overview and Goals
.envfiles. Chosen for secure API key management.POST /api/v1/sms/sendthat triggers SMS delivery via Infobip.nvm) to manage versions.Setting Up Your Node.js SMS Project
Initialize your Node.js project and install dependencies.
Create Project Directory: Open your terminal and create a new directory. Navigate into it.
mkdir infobip-sms-sender cd infobip-sms-senderInitialize Node.js Project: Run
npm initto createpackage.json. Accept defaults with the-yflag.This creates
package.json, which tracks project metadata and dependencies.Install Dependencies: Install
expressfor the web server,axiosfor HTTP requests, anddotenvfor environment variables.This downloads packages to
node_modulesand lists them inpackage.json.Set up Project Structure: Create this directory structure within
infobip-sms-sender:src/: Application source code.controllers/: Handles incoming requests and orchestrates responses.routes/: Defines API endpoints and maps them to controllers.services/: Business logic for interacting with the Infobip API..env: Stores environment variables (API keys, configuration). Never commit this file..gitignore: Specifies files Git should ignore (node_modules,.env).app.js: Main entry point for the Express application.Configure
.gitignore: Create.gitignorein the project root:Configure Environment Variables (
.env): Create.envin the project root. You need your Infobip API Key and Base URL.youruniqueid.api.infobip.com).Add these lines to
.env, replacing placeholders with your credentials:INFOBIP_API_KEY: Your secret API key for Infobip authentication.INFOBIP_BASE_URL: Your account-specific domain for API access.PORT: Port number for your Express server.Why
.env? Storing API keys in code is insecure and complicates deployment. Environment variables let you configure applications differently across environments (development, testing, production) without code changes.dotenvloads these intoprocess.env.Building the Infobip SMS Service Layer
Create the service that interacts with the Infobip API. This logic adapts the Infobip developer blog post.
Create
src/services/infobipService.js: This file builds requests and calls the Infobip API.// src/services/infobipService.js const axios = require('axios'); // Helper to build the full API URL const buildUrl = (domain) => { if (!domain) { throw new Error('Infobip domain (Base URL) is mandatory in config'); } // Ensure domain doesn't have protocol prefix for URL construction const cleanDomain = domain.replace(/^https?:\/\//, ''); return `https://${cleanDomain}/sms/2/text/advanced`; }; // Helper to build the required HTTP headers for Infobip API const buildHeaders = (apiKey) => { if (!apiKey) { throw new Error('Infobip API Key is mandatory in config'); } return { 'Authorization': `App ${apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json', }; }; // Helper to construct the request body according to Infobip specs const buildRequestBody = (destinationNumber, message) => { if (!destinationNumber || !message) { throw new Error('Destination number and message text are mandatory'); } // Validate E.164 format: digits only, no '+' prefix // Production apps should use libphonenumber for robust validation if (!/^\d{7,15}$/.test(destinationNumber)) { console.warn(`Destination number ${destinationNumber} may not conform to E.164 international format (7-15 digits, no '+' prefix).`); } const destinationObject = { to: destinationNumber, }; const messageObject = { destinations: [destinationObject], text: message, // Add 'from' if you have a registered sender ID // from: "YourSenderID" }; return { messages: [messageObject], }; }; // Helper function for basic argument validation const validateNotEmpty = (value, fieldName) => { if (!value) { throw new Error(`${fieldName} parameter is mandatory`); } }; /** * Sends an SMS message using the Infobip API. * @param {string} destinationNumber - Recipient phone number in E.164 format (digits only, no '+'). * @param {string} message - SMS text content (160 chars for GSM-7, 70 chars for Unicode/UCS-2). * @returns {Promise<object>} - Resolves with success response or rejects with error info. */ const sendSms = async (destinationNumber, message) => { const apiKey = process.env.INFOBIP_API_KEY; const domain = process.env.INFOBIP_BASE_URL; validateNotEmpty(domain, 'Infobip Base URL (INFOBIP_BASE_URL in .env)'); validateNotEmpty(apiKey, 'Infobip API Key (INFOBIP_API_KEY in .env)'); validateNotEmpty(destinationNumber, 'destinationNumber'); validateNotEmpty(message, 'message'); const url = buildUrl(domain); const headers = buildHeaders(apiKey); const requestBody = buildRequestBody(destinationNumber, message); console.log(`Attempting to send SMS to ${destinationNumber} via Infobip...`); try { const response = await axios.post(url, requestBody, { headers }); console.log('Infobip API call successful.'); return parseSuccessResponse(response); } catch (error) { console.error('Infobip API call failed:', error.message); throw parseFailedResponse(error); } }; // Parses the successful response from Infobip const parseSuccessResponse = (axiosResponse) => { const responseBody = axiosResponse.data; const singleMessageResponse = responseBody.messages && responseBody.messages[0]; if (!singleMessageResponse) { console.warn('Infobip success response structure differs from expected format.', responseBody); return { success: true, data: responseBody }; } return { success: true, messageId: singleMessageResponse.messageId, status: singleMessageResponse.status.name, description: singleMessageResponse.status.description, category: singleMessageResponse.status.groupName, to: singleMessageResponse.to, }; }; // Parses the error response from Infobip or network errors const parseFailedResponse = (axiosError) => { let errorInfo = { success: false, errorMessage: 'An unknown error occurred.', details: null, statusCode: null, }; if (axiosError.response) { // Error response from Infobip API (e.g., 4xx, 5xx) errorInfo.statusCode = axiosError.response.status; const responseBody = axiosError.response.data; if (responseBody && responseBody.requestError && responseBody.requestError.serviceException) { errorInfo.errorMessage = responseBody.requestError.serviceException.text || 'Infobip service exception'; errorInfo.details = responseBody.requestError.serviceException; } else { errorInfo.errorMessage = `Infobip API Error: Status ${errorInfo.statusCode}`; errorInfo.details = responseBody; } } else if (axiosError.request) { errorInfo.errorMessage = 'Network Error: No response received from Infobip API.'; } else { errorInfo.errorMessage = `Request Setup Error: ${axiosError.message}`; } errorInfo.rawError = axiosError.message; return errorInfo; }; module.exports = { sendSms, };async/await: Makes asynchronous operations cleaner than Promise chains.buildUrl,buildHeaders,buildRequestBody,parseSuccessResponse,parseFailedResponseimproves readability, maintainability, and testability.Creating the Express API Routes and Controller
Define the Express route and controller that receives requests and uses
infobipService.Create
src/controllers/smsController.js: This controller handles request logic.// src/controllers/smsController.js const infobipService = require('../services/infobipService'); const handleSendSms = async (req, res) => { const { to, text } = req.body; if (!to) { return res.status(400).json({ success: false, message: 'Missing required field: "to" (destination phone number)' }); } if (!text) { return res.status(400).json({ success: false, message: 'Missing required field: "text" (message content)' }); } // Enhanced validation: Check E.164 format (7-15 digits) const cleanNumber = to.replace(/[\s+()-]/g, ''); if (!/^\d{7,15}$/.test(cleanNumber)) { console.warn(`Received invalid phone number format: ${to}. Must be E.164 format (7-15 digits, e.g., 15551234567).`); return res.status(400).json({ success: false, message: 'Invalid "to" field format. Use E.164 international format: country code + number, 7-15 digits total (e.g., 15551234567).' }); } try { console.log(`Controller received request to send SMS to: ${to}`); const result = await infobipService.sendSms(cleanNumber, text); console.log('SMS processed successfully by Infobip service:', result); return res.status(200).json(result); } catch (error) { console.error('Error in handleSendSms controller:', error); const statusCode = error.statusCode || 500; return res.status(statusCode).json({ success: false, message: error.errorMessage || 'Failed to send SMS due to an internal error.', details: error.details || error.rawError, }); } }; module.exports = { handleSendSms, };Create
src/routes/smsRoutes.js: This file defines the API endpoint.// src/routes/smsRoutes.js const express = require('express'); const smsController = require('../controllers/smsController'); const router = express.Router(); // Define the route for sending SMS // POST /api/v1/sms/send router.post('/send', smsController.handleSendSms); module.exports = router;Set up the Main Application File (
app.js): Initialize Express, load environment variables, configure middleware, and mount routes.// app.js const express = require('express'); const dotenv = require('dotenv'); const smsRoutes = require('./src/routes/smsRoutes'); // Load environment variables from .env file dotenv.config(); const app = express(); // Middleware to parse JSON request bodies app.use(express.json()); // Middleware for basic logging app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); }); // Mount the SMS routes under the /api/v1/sms prefix app.use('/api/v1/sms', smsRoutes); // 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({ success: false, message: 'Resource not found' }); }); // Error handler middleware app.use((err, req, res, next) => { console.error('Unhandled Error:', err); res.status(500).json({ success: false, message: 'An unexpected internal server error occurred.' }); }); const PORT = process.env.PORT || 3000; // Start the server app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); console.log(`Infobip Base URL configured: ${process.env.INFOBIP_BASE_URL}`); if (process.env.INFOBIP_API_KEY) { console.log('Infobip API Key is configured.'); } else { console.warn('WARNING: Infobip API Key (INFOBIP_API_KEY) is missing in .env file!'); } });dotenv.config(): Loads.envvariables intoprocess.envbefore accessing them.express.json(): Parses incoming JSON payloads (available viareq.body).smsRoutesunder/api/v1/sms, creating final endpointhttp://localhost:3000/api/v1/sms/send./healthendpoint for monitoring.Configuring Infobip API Credentials
Integration happens in
src/services/infobipService.jsusing credentials from.env..env: StoresINFOBIP_API_KEYandINFOBIP_BASE_URL.process.env.INFOBIP_API_KEY..envin.gitignoreto prevent commits..envfile.INFOBIP_API_KEY: (String, Required) Your unique secret key from Infobip. Used inAuthorization: App <key>header.INFOBIP_BASE_URL: (String, Required) Your account-specific domain (e.g.,xyz123.api.infobip.com). Used to construct API endpoint URLs.PORT: (Number, Optional, Default: 3000) Your local server port.Error Handling and Logging Best Practices
Error handling is built into the service and controller:
infobipServiceusestry...catcharoundaxioscalls.parseFailedResponseto create standardized error objects for API errors (4xx/5xx) or network errors.smsControllercatches errors and inspectsstatusCodeanderrorMessageto return appropriate HTTP responses.console.logandconsole.errorfor basic logging.app.js.infobipService.js.WinstonorPinofor structured logging with different levels and external service routing.429 Too Many Requests), implement retries with exponential backoff usingaxios-retryor custom logic.INFOBIP_API_KEYin.envto incorrect value. Expect401 Unauthorized.INFOBIP_BASE_URLto non-existent domain. Expect network error (ENOTFOUNDor timeout).toortext. Expect400 Bad Request.tovalue (e.g.,'abcde'). Expect400 Bad Requestfrom controller validation.Security Best Practices for SMS APIs
Basic security is included; production systems need more.
.envand.gitignore. Critical.smsController.js.joiorexpress-validatorfor schema validation, type checking, and length limits.express-rate-limitmiddleware to limit requests per IP or user.Nginx,Caddy) configured for HTTPS, or use platforms (Heroku, Render) handling TLS termination.npm audit).Understanding SMS Format Requirements and Limitations
15551234567for US,447911123456for UK). Enhanced validation enforces 7-15 digit format. Production apps should uselibphonenumberfor robust validation and formatting.fromfield in the Infobip payload specifies sender ID (alphanumeric or phone number). Requires pre-registration and approval with Infobip. The example code comments outfrom.Performance Optimization for High-Volume SMS
For simple, single API calls per request, performance bottlenecks are unlikely. Main latency is network round-trip to Infobip.
axiosand Node.js HTTP agent handle connection pooling/reuse automatically.Monitoring Your Node.js SMS Application
Basic logging is included. Production systems need robust monitoring.
/healthendpoint provides basic status.console.log/console.errorprovide minimal visibility.Winston/Pinofor structured logging toDatadog,Logz.io, or ELK stack.prom-clientfor Prometheus metrics or integrate APM tools (Datadog APM,New Relic,Dynatrace).SentryorBugsnagfor real-time error capture with stack traces.GrafanawithPrometheus,Datadog, orNew Relic.Troubleshooting and Caveats
401 Unauthorized(from Infobip): IncorrectINFOBIP_API_KEY. Verify key in.envand Infobip dashboard.Network Error/ENOTFOUND/ECONNREFUSED/Timeout: IncorrectINFOBIP_BASE_URL, DNS issues, firewall blocking outgoing connections, or temporary Infobip unavailability. Verify Base URL and network connectivity.400 Bad Request(from Infobip): Invalid input. Check:tonumber format (must be E.164: 7-15 digits, no '+').fromsender ID (if used).detailsfield in error response for specific Infobip error codes (e.g.,Invalid destination address).TypeError: Cannot read property '...' of undefined: Missing environment variables (dotenv.config()not called early, variables missing in.env) or unexpected Infobip response structures. Add logging to trace variable values.Frequently Asked Questions (FAQ)
Q: What Node.js versions are compatible with Infobip? A: Node.js v18.x or v20.x LTS versions are recommended. Earlier versions may work but lack current security patches and performance improvements.
Q: Do I need a paid Infobip account to send SMS? A: Free trial accounts can send SMS to verified phone numbers only. For production use or testing with multiple numbers, you'll need to fund your account.
Q: What phone number format does Infobip require? A: Infobip requires E.164 international format: country code + national number, 7-15 digits total, without the '+' prefix. Example:
15551234567(US) or447911123456(UK).Q: How many characters can I send in one SMS? A: Standard SMS supports 160 characters (GSM-7 encoding) or 70 characters (UCS-2/Unicode for non-Latin scripts). Longer messages are automatically split into multiple segments, with separate billing per segment.
Q: How do I track SMS delivery status? A: Configure webhooks in the Infobip Portal to receive real-time delivery reports (DELIVERED, FAILED, etc.). The
messageIdreturned from the send request can be used to query delivery status via Infobip's API.Q: Can I customize the sender ID for my SMS messages? A: Yes, use the
fromfield in the message payload. Alphanumeric sender IDs require pre-registration and approval with Infobip. Some countries restrict sender ID customization.Q: How do I handle rate limits with Infobip? A: Implement rate limiting in your application using
express-rate-limit. For transient rate limit errors (429 responses), add retry logic with exponential backoff using libraries likeaxios-retry.Q: Is this code production-ready? A: This tutorial provides a foundation. For production, add: robust input validation (use
joiorexpress-validator), comprehensive error handling, retry mechanisms, structured logging (Winston/Pino), monitoring (Datadog/New Relic), rate limiting, and authentication/authorization.Q: How do I test my integration without sending real SMS? A: Use your verified trial phone number for testing, or upgrade to a paid account and use test numbers. Infobip also offers simulation modes in some regions—check their documentation.
Q: What's the typical latency for sending SMS through Infobip? A: Network latency to Infobip's API typically ranges from 200ms to 2 seconds depending on your location and network conditions. Actual SMS delivery to the recipient's device depends on carrier networks (usually 1-10 seconds).
Q: Can I send SMS to international numbers? A: Yes, Infobip supports international SMS delivery to 190+ countries. Ensure phone numbers are in proper E.164 format with the correct country code. Pricing varies by destination country.
Q: How do I handle special characters in SMS messages? A: GSM-7 encoding supports basic Latin characters and common symbols. For special characters (emojis, non-Latin scripts), messages use UCS-2 encoding, reducing the character limit to 70 per segment. Infobip handles encoding automatically based on message content.