Build a Node.js Express SMS Service with Infobip API Integration
Learn how to build a production-ready Node.js Express application to send SMS messages using the Infobip API. Complete guide with security, error handling, and validation.
Build a production-ready Node.js and Express application to send SMS messages using the Infobip API. This comprehensive guide covers creating a scalable SMS microservice for marketing campaigns, transactional notifications, and automated messaging with secure API integration, error handling, and validation.
You'll implement the official Infobip Node.js SDK for simplified API integration, configure secure environment variables for API authentication, build RESTful endpoints with Express, and implement production-grade error handling. By the end, you'll have a fully functional SMS service with security features and rate limiting.
Overview: Building an SMS microservice with Node.js and Infobip
Goal: Create a dedicated Node.js Express microservice to send SMS messages via the Infobip platform. This service exposes a simple API endpoint that other applications within your infrastructure can call to trigger SMS sends.
Problem solved: Centralizes SMS sending logic, securely manages Infobip credentials, provides a consistent interface for sending messages, and simplifies integration for other services needing SMS capabilities.
Technologies:
Node.js: A JavaScript runtime built on Chrome's V8 engine, ideal for building fast, scalable network applications.
Express: A minimal and flexible Node.js web application framework, providing a robust set of features for web and mobile applications.
Infobip API: A comprehensive communication platform-as-a-service (CPaaS) offering various APIs, including SMS.
Infobip Node.js SDK (@infobip-api/sdk): Simplifies interaction with the Infobip API by providing pre-built methods and handling authentication.
dotenv: A zero-dependency module that loads environment variables from a .env file into process.env.
Why these choices?
Node.js and Express offer a popular, efficient, and widely supported stack for building APIs.
Infobip provides reliable global SMS delivery with carrier-grade infrastructure.
The official Infobip SDK significantly reduces boilerplate code compared to direct HTTP requests, handles authentication details, and receives ongoing maintenance from Infobip.
dotenv is standard practice for managing environment-specific configurations and secrets securely in development.
Node.js (LTS version recommended) and npm (or yarn) installed.
A text editor or IDE (like VS Code).
Basic understanding of JavaScript, Node.js, Express, and REST APIs.
A phone number registered and verified with your Infobip account (required for trial accounts).
Final outcome: A running Express application with a /api/sms/send endpoint that accepts a phone number and message text, sends the SMS via Infobip, and returns a success or error response.
Set up your Node.js SMS project
Initialize your Node.js project and install the necessary dependencies.
Create project directory:
Open your terminal and create a new directory for the project.
Bash
mkdir infobip-sms-service
cd infobip-sms-service
Initialize Node.js project:
Create a package.json file to manage project details and dependencies.
Bash
npm init -y
(Alternatively, use yarn init -y if you prefer Yarn)
Install dependencies:
Install Express for the web server, the Infobip SDK for SMS sending, and dotenv for managing environment variables.
Bash
npm install express @infobip-api/sdk dotenv
(Or yarn add express @infobip-api/sdk dotenv)
Create project structure:
Set up a basic structure for organization.
Create basic Express server (src/server.js):
Create a minimal Express server to verify your setup works.
JavaScript
// src/server.jsrequire('dotenv').config(); // Load .env variables earlyconst express = require('express');
const smsRoutes = require('./routes/smsRoutes');
const app = express();
constPORT = process.env.PORT || 3000;
// Middleware to parse JSON bodies
app.use(express.json());
// Mount the SMS routes
app.use('/api/sms', smsRoutes);
// Basic health check route
app.get('/health', (req, res) => {
res.status(200).json({ status: 'OK', timestamp: newDate().toISOString() });
});
// Global error handler (basic example)// Production apps need more robust error handling.
app.use((err, req, res, next) => {
console.error('Unhandled Error:', err);
// In production, log the full error stack trace using a proper logger// and potentially send error details to an error tracking service (e.g., Sentry).// Avoid sending detailed internal error messages to the client.
res.status(500).json({ success: false, error: 'Internal Server Error' });
});
app.listen(PORT, () => {
console.log(`SMS Service listening on port ${PORT}`);
});
require('dotenv').config(): Loads variables from the .env file.
express.json(): Middleware to parse incoming JSON request bodies.
Define a basic /health check endpoint.
Mount placeholder routes for /api/sms.
Include a simple global error handler. Note: This is intentionally minimal for the example; a production application requires more sophisticated error handling, including structured logging and potentially integration with error tracking services.
Update package.json scripts:
Add a convenient start script to run the server.
JSON
{"name":"infobip-sms-service","version":"1.0.0","description":"Express service to send SMS via Infobip","main":"src/server.js","scripts":{"start":"node src/server.js","test":"echo \"Error: no test specified\" && exit 1"},"keywords":["infobip","sms","express","node"],"license":"ISC","dependencies":{"@infobip-api/sdk":"^2.0.0","dotenv":"^16.0.0","express":"^4.17.0"}}
Note: The dependency versions listed (^2.0.0, ^16.0.0, ^4.17.0) are examples. Install compatible versions or update them based on the latest stable releases.
You now have a basic Node.js Express project structure ready for implementing the SMS functionality.
Implement the Infobip SMS service layer
Encapsulate the Infobip SDK interaction within a dedicated service file.
Configure environment variables (.env):
Open the .env file and add placeholders for your Infobip Base URL and API Key. Obtain these from your Infobip account dashboard in the next step.
Create Infobip service (src/services/infobipService.js):
This service initializes the Infobip client and provides a function to send SMS.
JavaScript
// src/services/infobipService.jsconst { Infobip, AuthType } = require('@infobip-api/sdk');
// Validate that environment variables are setif (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) {
console.error('ERROR: Infobip Base URL or API Key not found in environment variables.');
console.error('Ensure INFOBIP_BASE_URL and INFOBIP_API_KEY are set in your .env file or environment.');
process.exit(1); // Exit if configuration is missing
}
// Initialize the Infobip client instanceconst infobipClient = newInfobip({
baseUrl: process.env.INFOBIP_BASE_URL,
apiKey: process.env.INFOBIP_API_KEY,
authType: AuthType.ApiKey, // Use API Key authentication
});
/**
* Sends an SMS message using the Infobip SDK.
* @param {string} recipientNumber - The destination phone number in international format (e.g., 447123456789).
* @param {string} messageText - The text content of the SMS message.
* @param {string} [senderId] - Optional sender ID (alphanumeric). Defaults to process.env.INFOBIP_SENDER_ID or undefined.
* @returns {Promise<object>} - A promise that resolves with the Infobip API response data.
* @throws {Error} - Throws an error if the API call fails.
*/constsendSms = async (recipientNumber, messageText, senderId = process.env.INFOBIP_SENDER_ID) => {
console.log(`Attempting to send SMS to ${recipientNumber}`);
// Basic validation (CRITICAL: Enhance significantly for production)if (!recipientNumber || !messageText) {
thrownewError('Recipient number and message text are required.');
// Production requires robust validation:// - Use regex or libraries like libphonenumber-js for phone number format.// - Check message length against SMS limits (e.g., 160 characters for GSM-7).// - Sanitize inputs if necessary.
}
const payload = {
messages: [
{
destinations: [{ to: recipientNumber }],
text: messageText,
// Only include 'from' if senderId is provided
...(senderId && { from: senderId }),
},
],
// Add more options here, e.g., tracking, scheduling, flash SMS, etc.// See Infobip documentation for the /sms/2/text/advanced endpoint options
};
try {
// Use the SDK's method to send the SMSconst response = await infobipClient.channels.sms.send(payload);
console.log('Infobip API Response:', JSON.stringify(response.data, null, 2));
// Check for success indication within the response if needed// The SDK might throw an error for non-2xx responses, but you can add checks hereif (response.data && response.data.messages && response.data.messages.length > 0) {
const messageStatus = response.data.messages[0].status;
console.log(`Message status for ${recipientNumber}: ${messageStatus.groupName} (${messageStatus.name})`);
// PENDING or ACCEPTED usually means success, REJECTED means failure at Infobip's end.
}
return response.data; // Return the body of the response
} catch (error) {
console.error('Error sending SMS via Infobip:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
// Re-throw a more specific error or handle it as neededthrownewError(`Failed to send SMS: ${error.response?.data?.requestError?.serviceException?.text || error.message}`);
}
};
// Export the function(s) for use in controllersmodule.exports = {
sendSms,
// Add other Infobip-related functions here if needed (e.g., check balance, get logs)
};
Import the Infobip client and AuthType enum from the SDK.
Check if the required environment variables are present before initializing the client.
Instantiate the infobipClient using the Base URL and API Key from .env.
The sendSms function takes the recipient, message, and optional sender ID.
Perform basic validation, but enhance this with stricter checks (e.g., phone number format using regex or libraries like libphonenumber-js, message length) for production use.
Construct the payload according to the Infobip API specification (/sms/2/text/advanced endpoint, which the SDK uses).
Call infobipClient.channels.sms.send() within a try...catch block.
Log the response and status for debugging.
Handle errors, logging relevant details from the Infobip error response if available.
Build the Express API for SMS sending
Create the Express route and controller to handle incoming requests to send SMS.
Create SMS controller (src/controllers/smsController.js):
This controller handles the logic for the /send route. You'll add more robust validation in section 7.
JavaScript
// src/controllers/smsController.jsconst infobipService = require('../services/infobipService');
constsendSingleSms = async (req, res, next) => {
// 1. Extract data from request bodyconst { recipientNumber, messageText, senderId } = req.body;
// 2. Basic Input Validation (Will be replaced by middleware later)if (!recipientNumber || !messageText) {
return res.status(400).json({
success: false,
error: 'Missing required fields: recipientNumber and messageText are required.',
});
}
// Note: More robust validation will be added using middleware in the Security section.try {
// 3. Call the Infobip service functionconst result = await infobipService.sendSms(recipientNumber, messageText, senderId);
// 4. Send success response// The result object from Infobip contains details like messageId and status
res.status(200).json({
success: true,
message: 'SMS submitted successfully to Infobip.',
infobipResponse: result, // Include the response from Infobip
});
} catch (error) {
// 5. Handle errors from the serviceconsole.error('Error in sendSingleSms controller:', error.message);
// Pass error to the global error handler, or return a specific error response// Depending on the error type, you might return different status codes
res.status(500).json({
success: false,
error: `Failed to send SMS: ${error.message}`,
});
// Alternatively: next(error); // To use the global error handler in server.js
}
};
// Add other controller functions here (e.g., sendBulkSms, getSmsStatus)module.exports = {
sendSingleSms,
// Export validation rules later
};
Require the infobipService.
Extract recipientNumber and messageText from req.body.
Perform minimal validation initially. Replace this with dedicated validation middleware later for better structure and robustness.
Call infobipService.sendSms within a try...catch block.
Return a 200 OK response with the Infobip result on success.
Return a 400 Bad Request for validation errors or 500 Internal Server Error for service errors.
Define SMS routes (src/routes/smsRoutes.js):
This file defines the specific endpoints under the /api/sms prefix.
JavaScript
// src/routes/smsRoutes.jsconst express = require('express');
const smsController = require('../controllers/smsController');
const router = express.Router();
// POST /api/sms/send - Endpoint to send a single SMS// Validation middleware will be added here later
router.post('/send', smsController.sendSingleSms);
// Define other SMS-related routes here if needed// e.g., router.post('/send-bulk', smsController.sendBulkSms);// e.g., router.get('/status/:messageId', smsController.getSmsStatus);module.exports = router;
Require the smsController.
Define a POST route at /send that maps to the sendSingleSms controller function.
Your API layer is now set up. The server.js file already mounts these routes under /api/sms.
Configure Infobip API credentials
Connect your application to your Infobip account.
Log in to Infobip: Go to the Infobip Portal and log in. The exact navigation within the Infobip portal may change over time.
Find your API Key:
Navigate to the "Developers" section or your account settings. Look for API Keys management.
If you don't have an API key, create a new one. Give it a descriptive name (e.g., "NodeExpressSMSService").
Important: Copy the generated API key immediately and store it securely. You often cannot view the key again after closing the creation dialog.
Find your Base URL:
In the Developers section or API documentation area, find your personalized Base URL. It usually looks like xxxxx.api.infobip.com.
Copy this Base URL.
Update .env file:
Paste the copied values into your .env file:
.env
# .env
INFOBIP_BASE_URL=xxxxx.api.infobip.com # Replace with your actual Base URL
INFOBIP_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Replace with your actual API Key
# Optional: Default sender ID allowed for your account
# INFOBIP_SENDER_ID=YourBrand
Sender ID (optional but recommended):
Using a custom Sender ID (like your brand name) instead of a generic number improves recognition.
Sender ID registration and regulations vary by country. Check Infobip documentation and local regulations. You may need to register your desired Sender ID via the Infobip portal.
If you have an approved Sender ID, set it as default in .env or pass it in the API request body (senderId field).
Security: Your Infobip API key grants access to your account and potentially incurs costs. Never commit your .env file or expose your API key in client-side code or public repositories. Use environment variables in your deployment environment.
Implement production-grade error handling
Robust error handling and logging are vital for production systems.
Service layer (infobipService.js):
The try...catch block already catches errors from the SDK call.
Log the detailed error response from Infobip (error.response.data) when available, which is crucial for debugging API issues (e.g., invalid number format, insufficient funds, permission errors).
Throw a new, cleaner error message for the controller layer.
Controller layer (smsController.js):
The try...catch block catches errors thrown by the service layer.
Log the error message.
Return appropriate HTTP status codes:
400 Bad Request / 422 Unprocessable Entity for client-side errors (missing parameters, invalid format – see validation section).
500 Internal Server Error for server-side or downstream errors (Infobip API unavailable, unexpected exceptions).
The response body includes a success: false flag and an error message.
Server layer (server.js):
The basic global error handler catches any unhandled exceptions. As noted earlier, this is minimal. For production, use a more sophisticated handler, integrating structured logging (e.g., winston, pino) and potentially error tracking services (e.g., Sentry).
Logging:
Currently using console.log and console.error. For production:
Use a structured logging library (Winston, Pino) to output logs in JSON format, making them easier to parse and analyze.
Include timestamps, log levels (INFO, WARN, ERROR), and potentially request IDs for tracing.
Configure log destinations (e.g., file, standard output, external logging service).
Retry mechanisms (optional enhancement):
For transient network errors or temporary Infobip issues (e.g., 5xx status codes), implement a retry strategy with exponential backoff to improve resilience. This is an optional enhancement for robustness, not part of the core initial setup.
Libraries like async-retry can simplify this. Wrap the infobipClient.channels.sms.send call within the retry logic in infobipService.js.
Be cautious not to retry non-retryable errors (like 4xx errors indicating bad input or invalid credentials).
JavaScript
// Example using async-retry (install: npm install async-retry)// This is an OPTIONAL enhancement for resilience, place inside src/services/infobipService.js sendSms functionconst retry = require('async-retry');
// ... inside sendSms function, replace the direct SDK call ...try {
const response = awaitretry(
async (bail, attempt) => {
console.log(`Attempting Infobip API call, attempt number ${attempt}...`);
// Check if the error is a client error (4xx) which shouldn't be retried.// The SDK throws an error which you catch below. If it's a non-retryable error,// the 'bail' function can be called to stop retrying.// Example: if (error.response && error.response.status >= 400 && error.response.status < 500) { bail(error); return; }const sdkResponse = await infobipClient.channels.sms.send(payload);
// Optional: Check for specific Infobip statuses within the response data that might warrant a retry,// though typically relying on HTTP status codes (handled by the SDK's error throwing) is sufficient.return sdkResponse; // Return successful response from SDK
},
{
retries: 3, // Number of retriesfactor: 2, // Exponential backoff factorminTimeout: 1000, // Initial delay in msonRetry: (error, attempt) => {
console.warn(`Retrying Infobip API call (attempt ${attempt}) due to error: ${error.message}`);
// Add check here to prevent retrying client errorsif (error.response && error.response.status >= 400 && error.response.status < 500) {
console.warn(`Not retrying on client error (${error.response.status}).`);
throw error; // Re-throw the original error to stop retries
}
},
}
);
console.log('Infobip API Response:', JSON.stringify(response.data, null, 2));
// ... rest of success handling ...return response.data;
} catch (error) {
// This catch block now handles errors after retries have failed, or non-retryable errorsconsole.error('Error sending SMS via Infobip after retries (or non-retryable error):', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
thrownewError(`Failed to send SMS after retries: ${error.response?.data?.requestError?.serviceException?.text || error.message}`);
}
Design your SMS database schema (optional)
While this guide focuses purely on the sending mechanism, a real-world marketing campaign system requires data persistence. This service could be extended or interact with other services managing:
Contacts/subscribers: Store phone numbers, opt-in status, names, segments, etc.
Typically use an ORM (like Prisma, Sequelize, TypeORM) or a query builder (like Knex.js) to interact with a database (e.g., PostgreSQL, MySQL).
Database migrations are essential for managing schema changes.
This SMS service might read contact lists or receive campaign instructions from another service, or it could be expanded to include this logic itself, depending on your overall architecture.
Scope: Implementing the full database layer is beyond the scope of this specific Infobip integration guide.
Secure your SMS API with validation and rate limiting
Security is paramount, especially when handling user data and external APIs.
Secure API key management: Already covered by using .env and environment variables in production. Never hardcode keys.
Input validation:
The initial controller (smsController.js) has minimal validation. Replace it using express-validator for robust, declarative validation.
Update controller (src/controllers/smsController.js): Define validation rules and a middleware function to handle errors. Export these along with the main controller function.
JavaScript
// src/controllers/smsController.jsconst { body, validationResult } = require('express-validator');
const infobipService = require('../services/infobipService');
// Define validation rules for the send SMS endpointconstsendSingleSmsValidationRules = () => {
return [
body('recipientNumber')
.trim() // Remove leading/trailing whitespace
.notEmpty().withMessage('recipientNumber is required.')
// Use a more specific validator like isMobilePhone or a custom regex for international format// Example: .matches(/^\d{11,15}$/).withMessage('recipientNumber must be 11 to 15 digits.')
.isMobilePhone('any', { strictMode: false }).withMessage('recipientNumber must be a valid phone number format.'), // Basic checkbody('messageText')
.trim()
.notEmpty().withMessage('messageText is required.')
.isLength({ min: 1, max: 1600 }).withMessage('messageText must be between 1 and 1600 characters.'), // Consider SMS segment limitsbody('senderId')
.optional() // Allows senderId to be absent
.trim()
.isAlphanumeric().withMessage('senderId must be alphanumeric.')
.isLength({ min: 1, max: 11 }).withMessage('senderId must be between 1 and 11 characters.'),
];
};
// Middleware to handle validation resultsconstvalidate = (req, res, next) => {
const errors = validationResult(req);
if (errors.isEmpty()) {
returnnext(); // Proceed to the controller if no errors
}
// Format errors for the responseconst extractedErrors = [];
errors.array().map(err => extractedErrors.push({ [err.path]: err.msg }));
// Return 422 Unprocessable Entity for validation failuresreturn res.status(422).json({
success: false,
errors: extractedErrors,
});
};
// Controller function (no longer needs internal basic validation)constsendSingleSms = async (req, res, next) => {
// Validation has already passed via middlewareconst { recipientNumber, messageText, senderId } = req.body;
try {
const result = await infobipService.sendSms(recipientNumber, messageText, senderId);
res.status(200).json({
success: true,
message: 'SMS submitted successfully to Infobip.',
infobipResponse: result,
});
} catch (error) {
console.error('Error in sendSingleSms controller:', error.message);
// Optionally, pass to global error handler: next(error)
res.status(500).json({
success: false,
error: `Failed to send SMS: ${error.message}`,
});
}
};
module.exports = {
sendSingleSmsValidationRules, // Export rules array function
validate, // Export validation middleware
sendSingleSms, // Export controller function
};
Update routes (src/routes/smsRoutes.js): Apply the validation rules and middleware to the route definition before the controller function.
JavaScript
// src/routes/smsRoutes.jsconst express = require('express');
// Import the specific functions needed from the controllerconst {
sendSingleSms,
sendSingleSmsValidationRules,
validate
} = require('../controllers/smsController');
const router = express.Router();
// POST /api/sms/send - Apply validation rules and middleware, then the controller
router.post(
'/send',
sendSingleSmsValidationRules(), // Define the rules for this route
validate, // Run the validation check
sendSingleSms // If validation passes, proceed to the controller
);
// Define other SMS-related routes here if neededmodule.exports = router;
Rate limiting: Protect your API from abuse and accidental loops. Use middleware like express-rate-limit.
// src/server.js// ... other requiresconst rateLimit = require('express-rate-limit');
const smsRoutes = require('./routes/smsRoutes');
// ... app setup ...
app.use(express.json());
// Apply rate limiting specifically to the SMS API routesconst smsApiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowMsmessage: { success: false, error: 'Too many requests, please try again after 15 minutes.' },
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply the limiter only to paths starting with /api/sms
app.use('/api/sms', smsApiLimiter);
// Mount the SMS routes (AFTER the limiter)
app.use('/api/sms', smsRoutes);
// ... health check, error handler, app.listen ...
HTTPS: Always use HTTPS in production to encrypt data in transit. This is typically handled at the load balancer or reverse proxy level (e.g., Nginx, Caddy, Cloudflare, AWS ELB), not directly in the Node.js application code.
Authentication/authorization: If this service is exposed externally or consumed by multiple internal clients, implement proper authentication (e.g., API keys required in headers, JWT validation) to ensure only authorized systems can trigger SMS sends. This is beyond the scope of this basic setup but crucial for real-world deployments.
Testing your Infobip SMS integration
Before deploying to production, test your SMS service thoroughly:
Success: 200 OK with success: true and Infobip response data
Validation error: 422 Unprocessable Entity with error details
Server error: 500 Internal Server Error with error message
Check Infobip dashboard:
Log into the Infobip portal
Navigate to the SMS logs section
Verify message status (PENDING, DELIVERED, FAILED)
Review delivery reports and error codes
Test error scenarios:
Invalid phone number format
Missing required fields
Rate limit exceeded
Invalid API credentials
Deploying your Node.js SMS service
Deploy your SMS service to a production environment:
Environment variables:
Set INFOBIP_BASE_URL and INFOBIP_API_KEY in your hosting environment
Configure PORT if needed (default: 3000)
Set NODE_ENV=production for production deployments
Hosting options:
Docker: Containerize your application for consistent deployments
Cloud platforms: Deploy to AWS (Elastic Beanstalk, ECS), Google Cloud (App Engine), Azure (App Service), or Heroku
Serverless: Adapt to AWS Lambda or Google Cloud Functions for event-driven SMS sending
Production checklist:
✅ Enable HTTPS at the load balancer or reverse proxy level
✅ Configure structured logging (Winston, Pino) with log aggregation
✅ Set up monitoring and alerting for API errors and rate limits
✅ Implement authentication/authorization for API access
✅ Configure proper rate limiting based on your Infobip account limits
✅ Set up error tracking (Sentry, Rollbar) for production debugging
✅ Test failover and retry logic for transient errors
Frequently asked questions
How do I get an Infobip API key?
Log into the Infobip Portal, navigate to the "Developers" section or account settings, and create a new API key. Copy the key immediately as you cannot view it again after creation.
What phone number format does Infobip require?
Infobip requires phone numbers in international E.164 format without the + prefix (e.g., 447123456789 for a UK number). Use the libphonenumber-js library to validate and format phone numbers correctly.
How much does it cost to send SMS via Infobip?
SMS costs vary by destination country and message type. Check the Infobip pricing page or your account dashboard for current rates. Most countries charge per message segment (160 characters for GSM-7 encoding).
Can I send SMS to any country?
Infobip supports SMS delivery to 190+ countries, but some countries have restrictions or require sender ID registration. Check Infobip's country-specific documentation for regulatory requirements.
How do I handle SMS delivery failures?
Check the status field in the Infobip API response. Common failure reasons include invalid phone numbers, insufficient account balance, or carrier rejections. Implement webhook handlers to receive delivery reports for message status updates.
What's the difference between sender ID and short code?
A sender ID is an alphanumeric identifier (up to 11 characters) that appears as the message sender. A short code is a 5-6 digit number used primarily in the US for high-volume messaging. Sender ID availability and registration requirements vary by country.
How do I implement SMS opt-out compliance?
Store opt-out requests in your database and check before sending. Include unsubscribe instructions in marketing messages (e.g., "Reply STOP to unsubscribe"). Process opt-out keywords automatically by implementing Infobip webhooks for incoming messages.
Can I schedule SMS messages for later delivery?
Yes, use the sendAt parameter in the Infobip API payload to schedule messages. Pass an ISO 8601 timestamp for the desired delivery time. Note that scheduling is subject to Infobip's scheduling limits.
Next steps for your SMS service
Enhance your SMS service with these features:
Bulk SMS sending: Implement batch endpoints to send messages to multiple recipients efficiently
Template management: Create reusable message templates with variable substitution
Delivery webhooks: Configure Infobip webhooks to receive real-time delivery status updates
Two-way SMS: Implement incoming message handlers for interactive SMS conversations
A/B testing: Split test message content, timing, and sender IDs for optimization
Integration with marketing automation: Connect with tools like HubSpot, Salesforce, or custom CRM systems
Multi-channel support: Extend the service to support WhatsApp, Viber, and other Infobip channels
Frequently asked questions
Use the Infobip Node.js SDK and Express to build a service that interacts with the Infobip API. This allows you to send SMS messages programmatically by making API calls to your Infobip account, simplifying the integration process.
The Infobip Node.js SDK (@infobip-api/sdk) is a pre-built library that simplifies interaction with the Infobip API. It handles authentication and provides convenient methods for sending SMS messages and other communication tasks, reducing boilerplate code.
Node.js and Express provide a popular, efficient, and well-supported foundation for building APIs and microservices. Their scalability makes them suitable for handling potentially high volumes of SMS requests.
A dedicated microservice is beneficial when you need to centralize SMS sending logic, securely manage API credentials, provide a consistent interface, and simplify integration for multiple applications that require SMS capabilities.
Yes, you can use a custom alphanumeric sender ID (e.g., your brand name) to improve message recognition, subject to Infobip's guidelines and local regulations, which may require registration.
Store your Infobip Base URL and API Key as environment variables (INFOBIP_BASE_URL and INFOBIP_API_KEY) in a .env file for development. Never hardcode these credentials directly in your code, and in production, use proper environment variable management within your deployment platform.
Dotenv is a package that loads environment variables from a .env file into process.env, making it easy to manage configuration values, especially API keys and other sensitive data that shouldn't be committed to version control.
Implement try...catch blocks to handle errors from the Infobip SDK and service layer. Provide informative error messages and log details like the error response from Infobip for debugging. Consider using a dedicated error tracking service in production.
You should consider storing contact details (phone numbers, opt-in status), campaign information (messages, schedules), message logs (status, recipient, costs), and opt-out records for analytics, reporting, and compliance.
Use the isMobilePhone validator from express-validator or regular expressions to ensure phone numbers are in a valid international format and sanitize input to prevent vulnerabilities. Dedicated libraries like libphonenumber-js can help improve validation accuracy.
Secure your API keys, implement robust input validation using libraries like express-validator, use rate limiting (express-rate-limit) to prevent abuse, enforce HTTPS, and add authentication/authorization if your service is exposed externally.
Express-rate-limit is middleware that helps protect your API from abuse by limiting the number of requests from a given IP address within a specified time window. This can prevent overload and denial-of-service attacks.
Organize your project with separate directories for routes, controllers, and services. Routes define API endpoints, controllers handle requests and interact with services, and services encapsulate the logic for sending SMS using the Infobip SDK.
Logging provides crucial information for debugging, monitoring, and troubleshooting. Use a structured logging library to record timestamps, log levels, request IDs, and error details, and configure appropriate log destinations for analysis.
The /health endpoint is a simple way to check the status and availability of your service. It typically returns a 200 OK response with a timestamp or other relevant information, allowing monitoring systems to verify that the service is running.
Build a production-ready Node.js and Express application to send SMS messages using the Infobip API. This comprehensive guide covers creating a scalable SMS microservice for marketing campaigns, transactional notifications, and automated messaging with secure API integration, error handling, and validation.
You'll implement the official Infobip Node.js SDK for simplified API integration, configure secure environment variables for API authentication, build RESTful endpoints with Express, and implement production-grade error handling. By the end, you'll have a fully functional SMS service with security features and rate limiting.
Overview: Building an SMS microservice with Node.js and Infobip
Goal: Create a dedicated Node.js Express microservice to send SMS messages via the Infobip platform. This service exposes a simple API endpoint that other applications within your infrastructure can call to trigger SMS sends.
Problem solved: Centralizes SMS sending logic, securely manages Infobip credentials, provides a consistent interface for sending messages, and simplifies integration for other services needing SMS capabilities.
Technologies:
@infobip-api/sdk): Simplifies interaction with the Infobip API by providing pre-built methods and handling authentication..envfile intoprocess.env.Why these choices?
dotenvis standard practice for managing environment-specific configurations and secrets securely in development.System architecture:
Prerequisites:
Final outcome: A running Express application with a
/api/sms/sendendpoint that accepts a phone number and message text, sends the SMS via Infobip, and returns a success or error response.Set up your Node.js SMS project
Initialize your Node.js project and install the necessary dependencies.
Create project directory: Open your terminal and create a new directory for the project.
mkdir infobip-sms-service cd infobip-sms-serviceInitialize Node.js project: Create a
package.jsonfile to manage project details and dependencies.(Alternatively, use
yarn init -yif you prefer Yarn)Install dependencies: Install Express for the web server, the Infobip SDK for SMS sending, and
dotenvfor managing environment variables.(Or
yarn add express @infobip-api/sdk dotenv)Create project structure: Set up a basic structure for organization.
mkdir src mkdir src/routes mkdir src/controllers mkdir src/services touch src/server.js touch src/routes/smsRoutes.js touch src/controllers/smsController.js touch src/services/infobipService.js touch .env touch .gitignoresrc/: Contains your main application code.src/routes/: Defines API routes.src/controllers/: Handles incoming requests and interacts with services.src/services/: Contains business logic, like interacting with the Infobip SDK.src/server.js: The main entry point for your Express application..env: Stores environment variables (API keys, etc.). Never commit this file to Git..gitignore: Specifies files and directories that Git should ignore.Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing them.Create basic Express server (
src/server.js): Create a minimal Express server to verify your setup works.// src/server.js require('dotenv').config(); // Load .env variables early const express = require('express'); const smsRoutes = require('./routes/smsRoutes'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware to parse JSON bodies app.use(express.json()); // Mount the SMS routes app.use('/api/sms', smsRoutes); // Basic health check route app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); }); // Global error handler (basic example) // Production apps need more robust error handling. app.use((err, req, res, next) => { console.error('Unhandled Error:', err); // In production, log the full error stack trace using a proper logger // and potentially send error details to an error tracking service (e.g., Sentry). // Avoid sending detailed internal error messages to the client. res.status(500).json({ success: false, error: 'Internal Server Error' }); }); app.listen(PORT, () => { console.log(`SMS Service listening on port ${PORT}`); });require('dotenv').config(): Loads variables from the.envfile.express.json(): Middleware to parse incoming JSON request bodies./healthcheck endpoint./api/sms.Update
package.jsonscripts: Add a convenientstartscript to run the server.{ "name": "infobip-sms-service", "version": "1.0.0", "description": "Express service to send SMS via Infobip", "main": "src/server.js", "scripts": { "start": "node src/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "infobip", "sms", "express", "node" ], "license": "ISC", "dependencies": { "@infobip-api/sdk": "^2.0.0", "dotenv": "^16.0.0", "express": "^4.17.0" } }Note: The dependency versions listed (
^2.0.0,^16.0.0,^4.17.0) are examples. Install compatible versions or update them based on the latest stable releases.You now have a basic Node.js Express project structure ready for implementing the SMS functionality.
Implement the Infobip SMS service layer
Encapsulate the Infobip SDK interaction within a dedicated service file.
Configure environment variables (
.env): Open the.envfile and add placeholders for your Infobip Base URL and API Key. Obtain these from your Infobip account dashboard in the next step.Create Infobip service (
src/services/infobipService.js): This service initializes the Infobip client and provides a function to send SMS.// src/services/infobipService.js const { Infobip, AuthType } = require('@infobip-api/sdk'); // Validate that environment variables are set if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) { console.error('ERROR: Infobip Base URL or API Key not found in environment variables.'); console.error('Ensure INFOBIP_BASE_URL and INFOBIP_API_KEY are set in your .env file or environment.'); process.exit(1); // Exit if configuration is missing } // Initialize the Infobip client instance const infobipClient = new Infobip({ baseUrl: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, authType: AuthType.ApiKey, // Use API Key authentication }); /** * Sends an SMS message using the Infobip SDK. * @param {string} recipientNumber - The destination phone number in international format (e.g., 447123456789). * @param {string} messageText - The text content of the SMS message. * @param {string} [senderId] - Optional sender ID (alphanumeric). Defaults to process.env.INFOBIP_SENDER_ID or undefined. * @returns {Promise<object>} - A promise that resolves with the Infobip API response data. * @throws {Error} - Throws an error if the API call fails. */ const sendSms = async (recipientNumber, messageText, senderId = process.env.INFOBIP_SENDER_ID) => { console.log(`Attempting to send SMS to ${recipientNumber}`); // Basic validation (CRITICAL: Enhance significantly for production) if (!recipientNumber || !messageText) { throw new Error('Recipient number and message text are required.'); // Production requires robust validation: // - Use regex or libraries like libphonenumber-js for phone number format. // - Check message length against SMS limits (e.g., 160 characters for GSM-7). // - Sanitize inputs if necessary. } const payload = { messages: [ { destinations: [{ to: recipientNumber }], text: messageText, // Only include 'from' if senderId is provided ...(senderId && { from: senderId }), }, ], // Add more options here, e.g., tracking, scheduling, flash SMS, etc. // See Infobip documentation for the /sms/2/text/advanced endpoint options }; try { // Use the SDK's method to send the SMS const response = await infobipClient.channels.sms.send(payload); console.log('Infobip API Response:', JSON.stringify(response.data, null, 2)); // Check for success indication within the response if needed // The SDK might throw an error for non-2xx responses, but you can add checks here if (response.data && response.data.messages && response.data.messages.length > 0) { const messageStatus = response.data.messages[0].status; console.log(`Message status for ${recipientNumber}: ${messageStatus.groupName} (${messageStatus.name})`); // PENDING or ACCEPTED usually means success, REJECTED means failure at Infobip's end. } return response.data; // Return the body of the response } catch (error) { console.error('Error sending SMS via Infobip:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message); // Re-throw a more specific error or handle it as needed throw new Error(`Failed to send SMS: ${error.response?.data?.requestError?.serviceException?.text || error.message}`); } }; // Export the function(s) for use in controllers module.exports = { sendSms, // Add other Infobip-related functions here if needed (e.g., check balance, get logs) };Infobipclient andAuthTypeenum from the SDK.infobipClientusing the Base URL and API Key from.env.sendSmsfunction takes the recipient, message, and optional sender ID.libphonenumber-js, message length) for production use./sms/2/text/advancedendpoint, which the SDK uses).infobipClient.channels.sms.send()within atry...catchblock.Build the Express API for SMS sending
Create the Express route and controller to handle incoming requests to send SMS.
Create SMS controller (
src/controllers/smsController.js): This controller handles the logic for the/sendroute. You'll add more robust validation in section 7.// src/controllers/smsController.js const infobipService = require('../services/infobipService'); const sendSingleSms = async (req, res, next) => { // 1. Extract data from request body const { recipientNumber, messageText, senderId } = req.body; // 2. Basic Input Validation (Will be replaced by middleware later) if (!recipientNumber || !messageText) { return res.status(400).json({ success: false, error: 'Missing required fields: recipientNumber and messageText are required.', }); } // Note: More robust validation will be added using middleware in the Security section. try { // 3. Call the Infobip service function const result = await infobipService.sendSms(recipientNumber, messageText, senderId); // 4. Send success response // The result object from Infobip contains details like messageId and status res.status(200).json({ success: true, message: 'SMS submitted successfully to Infobip.', infobipResponse: result, // Include the response from Infobip }); } catch (error) { // 5. Handle errors from the service console.error('Error in sendSingleSms controller:', error.message); // Pass error to the global error handler, or return a specific error response // Depending on the error type, you might return different status codes res.status(500).json({ success: false, error: `Failed to send SMS: ${error.message}`, }); // Alternatively: next(error); // To use the global error handler in server.js } }; // Add other controller functions here (e.g., sendBulkSms, getSmsStatus) module.exports = { sendSingleSms, // Export validation rules later };infobipService.recipientNumberandmessageTextfromreq.body.infobipService.sendSmswithin atry...catchblock.200 OKresponse with the Infobip result on success.400 Bad Requestfor validation errors or500 Internal Server Errorfor service errors.Define SMS routes (
src/routes/smsRoutes.js): This file defines the specific endpoints under the/api/smsprefix.// src/routes/smsRoutes.js const express = require('express'); const smsController = require('../controllers/smsController'); const router = express.Router(); // POST /api/sms/send - Endpoint to send a single SMS // Validation middleware will be added here later router.post('/send', smsController.sendSingleSms); // Define other SMS-related routes here if needed // e.g., router.post('/send-bulk', smsController.sendBulkSms); // e.g., router.get('/status/:messageId', smsController.getSmsStatus); module.exports = router;smsController.POSTroute at/sendthat maps to thesendSingleSmscontroller function.Your API layer is now set up. The
server.jsfile already mounts these routes under/api/sms.Configure Infobip API credentials
Connect your application to your Infobip account.
Log in to Infobip: Go to the Infobip Portal and log in. The exact navigation within the Infobip portal may change over time.
Find your API Key:
Find your Base URL:
xxxxx.api.infobip.com.Update
.envfile: Paste the copied values into your.envfile:Sender ID (optional but recommended):
.envor pass it in the API request body (senderIdfield).Security: Your Infobip API key grants access to your account and potentially incurs costs. Never commit your
.envfile or expose your API key in client-side code or public repositories. Use environment variables in your deployment environment.Implement production-grade error handling
Robust error handling and logging are vital for production systems.
infobipService.js):try...catchblock already catches errors from the SDK call.error.response.data) when available, which is crucial for debugging API issues (e.g., invalid number format, insufficient funds, permission errors).smsController.js):try...catchblock catches errors thrown by the service layer.400 Bad Request/422 Unprocessable Entityfor client-side errors (missing parameters, invalid format – see validation section).500 Internal Server Errorfor server-side or downstream errors (Infobip API unavailable, unexpected exceptions).success: falseflag and anerrormessage.server.js):winston,pino) and potentially error tracking services (e.g., Sentry).console.logandconsole.error. For production:async-retrycan simplify this. Wrap theinfobipClient.channels.sms.sendcall within the retry logic ininfobipService.js.// Example using async-retry (install: npm install async-retry) // This is an OPTIONAL enhancement for resilience, place inside src/services/infobipService.js sendSms function const retry = require('async-retry'); // ... inside sendSms function, replace the direct SDK call ... try { const response = await retry( async (bail, attempt) => { console.log(`Attempting Infobip API call, attempt number ${attempt}...`); // Check if the error is a client error (4xx) which shouldn't be retried. // The SDK throws an error which you catch below. If it's a non-retryable error, // the 'bail' function can be called to stop retrying. // Example: if (error.response && error.response.status >= 400 && error.response.status < 500) { bail(error); return; } const sdkResponse = await infobipClient.channels.sms.send(payload); // Optional: Check for specific Infobip statuses within the response data that might warrant a retry, // though typically relying on HTTP status codes (handled by the SDK's error throwing) is sufficient. return sdkResponse; // Return successful response from SDK }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial delay in ms onRetry: (error, attempt) => { console.warn(`Retrying Infobip API call (attempt ${attempt}) due to error: ${error.message}`); // Add check here to prevent retrying client errors if (error.response && error.response.status >= 400 && error.response.status < 500) { console.warn(`Not retrying on client error (${error.response.status}).`); throw error; // Re-throw the original error to stop retries } }, } ); console.log('Infobip API Response:', JSON.stringify(response.data, null, 2)); // ... rest of success handling ... return response.data; } catch (error) { // This catch block now handles errors after retries have failed, or non-retryable errors console.error('Error sending SMS via Infobip after retries (or non-retryable error):', error.response ? JSON.stringify(error.response.data, null, 2) : error.message); throw new Error(`Failed to send SMS after retries: ${error.response?.data?.requestError?.serviceException?.text || error.message}`); }Design your SMS database schema (optional)
While this guide focuses purely on the sending mechanism, a real-world marketing campaign system requires data persistence. This service could be extended or interact with other services managing:
Contacts (contact_id PK, phone_number UNIQUE, first_name, last_name, opt_in_status, created_at, updated_at)Campaigns (campaign_id PK, name, message_template, target_segment, schedule_time, status, created_at)MessageLogs (log_id PK, infobip_message_id UNIQUE, contact_id FK, campaign_id FK, status, status_group, error_code, sent_at, delivered_at, cost)OptOuts (phone_number PK, opted_out_at)Implementation:
Scope: Implementing the full database layer is beyond the scope of this specific Infobip integration guide.
Secure your SMS API with validation and rate limiting
Security is paramount, especially when handling user data and external APIs.
Secure API key management: Already covered by using
.envand environment variables in production. Never hardcode keys.Input validation:
The initial controller (
smsController.js) has minimal validation. Replace it usingexpress-validatorfor robust, declarative validation.Install the library:
npm install express-validator # or: yarn add express-validatorUpdate controller (
src/controllers/smsController.js): Define validation rules and a middleware function to handle errors. Export these along with the main controller function.// src/controllers/smsController.js const { body, validationResult } = require('express-validator'); const infobipService = require('../services/infobipService'); // Define validation rules for the send SMS endpoint const sendSingleSmsValidationRules = () => { return [ body('recipientNumber') .trim() // Remove leading/trailing whitespace .notEmpty().withMessage('recipientNumber is required.') // Use a more specific validator like isMobilePhone or a custom regex for international format // Example: .matches(/^\d{11,15}$/).withMessage('recipientNumber must be 11 to 15 digits.') .isMobilePhone('any', { strictMode: false }).withMessage('recipientNumber must be a valid phone number format.'), // Basic check body('messageText') .trim() .notEmpty().withMessage('messageText is required.') .isLength({ min: 1, max: 1600 }).withMessage('messageText must be between 1 and 1600 characters.'), // Consider SMS segment limits body('senderId') .optional() // Allows senderId to be absent .trim() .isAlphanumeric().withMessage('senderId must be alphanumeric.') .isLength({ min: 1, max: 11 }).withMessage('senderId must be between 1 and 11 characters.'), ]; }; // Middleware to handle validation results const validate = (req, res, next) => { const errors = validationResult(req); if (errors.isEmpty()) { return next(); // Proceed to the controller if no errors } // Format errors for the response const extractedErrors = []; errors.array().map(err => extractedErrors.push({ [err.path]: err.msg })); // Return 422 Unprocessable Entity for validation failures return res.status(422).json({ success: false, errors: extractedErrors, }); }; // Controller function (no longer needs internal basic validation) const sendSingleSms = async (req, res, next) => { // Validation has already passed via middleware const { recipientNumber, messageText, senderId } = req.body; try { const result = await infobipService.sendSms(recipientNumber, messageText, senderId); res.status(200).json({ success: true, message: 'SMS submitted successfully to Infobip.', infobipResponse: result, }); } catch (error) { console.error('Error in sendSingleSms controller:', error.message); // Optionally, pass to global error handler: next(error) res.status(500).json({ success: false, error: `Failed to send SMS: ${error.message}`, }); } }; module.exports = { sendSingleSmsValidationRules, // Export rules array function validate, // Export validation middleware sendSingleSms, // Export controller function };Update routes (
src/routes/smsRoutes.js): Apply the validation rules and middleware to the route definition before the controller function.// src/routes/smsRoutes.js const express = require('express'); // Import the specific functions needed from the controller const { sendSingleSms, sendSingleSmsValidationRules, validate } = require('../controllers/smsController'); const router = express.Router(); // POST /api/sms/send - Apply validation rules and middleware, then the controller router.post( '/send', sendSingleSmsValidationRules(), // Define the rules for this route validate, // Run the validation check sendSingleSms // If validation passes, proceed to the controller ); // Define other SMS-related routes here if needed module.exports = router;Rate limiting: Protect your API from abuse and accidental loops. Use middleware like
express-rate-limit.npm install express-rate-limit # or: yarn add express-rate-limit// src/server.js // ... other requires const rateLimit = require('express-rate-limit'); const smsRoutes = require('./routes/smsRoutes'); // ... app setup ... app.use(express.json()); // Apply rate limiting specifically to the SMS API routes const smsApiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: { success: false, error: 'Too many requests, please try again after 15 minutes.' }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply the limiter only to paths starting with /api/sms app.use('/api/sms', smsApiLimiter); // Mount the SMS routes (AFTER the limiter) app.use('/api/sms', smsRoutes); // ... health check, error handler, app.listen ...HTTPS: Always use HTTPS in production to encrypt data in transit. This is typically handled at the load balancer or reverse proxy level (e.g., Nginx, Caddy, Cloudflare, AWS ELB), not directly in the Node.js application code.
Authentication/authorization: If this service is exposed externally or consumed by multiple internal clients, implement proper authentication (e.g., API keys required in headers, JWT validation) to ensure only authorized systems can trigger SMS sends. This is beyond the scope of this basic setup but crucial for real-world deployments.
Testing your Infobip SMS integration
Before deploying to production, test your SMS service thoroughly:
Test with curl or Postman:
curl -X POST http://localhost:3000/api/sms/send \ -H "Content-Type: application/json" \ -d '{ "recipientNumber": "447123456789", "messageText": "Test message from Node.js SMS service", "senderId": "YourBrand" }'Verify response structure:
200 OKwithsuccess: trueand Infobip response data422 Unprocessable Entitywith error details500 Internal Server Errorwith error messageCheck Infobip dashboard:
Test error scenarios:
Deploying your Node.js SMS service
Deploy your SMS service to a production environment:
Environment variables:
INFOBIP_BASE_URLandINFOBIP_API_KEYin your hosting environmentPORTif needed (default: 3000)NODE_ENV=productionfor production deploymentsHosting options:
Production checklist:
Frequently asked questions
How do I get an Infobip API key? Log into the Infobip Portal, navigate to the "Developers" section or account settings, and create a new API key. Copy the key immediately as you cannot view it again after creation.
What phone number format does Infobip require? Infobip requires phone numbers in international E.164 format without the
+prefix (e.g.,447123456789for a UK number). Use thelibphonenumber-jslibrary to validate and format phone numbers correctly.How much does it cost to send SMS via Infobip? SMS costs vary by destination country and message type. Check the Infobip pricing page or your account dashboard for current rates. Most countries charge per message segment (160 characters for GSM-7 encoding).
Can I send SMS to any country? Infobip supports SMS delivery to 190+ countries, but some countries have restrictions or require sender ID registration. Check Infobip's country-specific documentation for regulatory requirements.
How do I handle SMS delivery failures? Check the
statusfield in the Infobip API response. Common failure reasons include invalid phone numbers, insufficient account balance, or carrier rejections. Implement webhook handlers to receive delivery reports for message status updates.What's the difference between sender ID and short code? A sender ID is an alphanumeric identifier (up to 11 characters) that appears as the message sender. A short code is a 5-6 digit number used primarily in the US for high-volume messaging. Sender ID availability and registration requirements vary by country.
How do I implement SMS opt-out compliance? Store opt-out requests in your database and check before sending. Include unsubscribe instructions in marketing messages (e.g., "Reply STOP to unsubscribe"). Process opt-out keywords automatically by implementing Infobip webhooks for incoming messages.
Can I schedule SMS messages for later delivery? Yes, use the
sendAtparameter in the Infobip API payload to schedule messages. Pass an ISO 8601 timestamp for the desired delivery time. Note that scheduling is subject to Infobip's scheduling limits.Next steps for your SMS service
Enhance your SMS service with these features: