This guide provides a comprehensive walkthrough for sending SMS messages using Node.js and the Infobip API. We'll cover setting up your project, integrating with Infobip using two different approaches (manual HTTP requests and the official SDK), building a simple API layer, and crucial considerations like error handling, security, and deployment.
By the end of this guide, you'll have a functional Node.js application capable of sending SMS messages via Infobip, along with the knowledge to build upon this foundation for more complex use cases.
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- npm (or yarn): Package managers for Node.js.
- Infobip API: The communication platform service we'll use to send SMS messages.
- Axios (Optional): A promise-based HTTP client for making requests manually.
- @infobip-api/sdk (Optional): The official Infobip Node.js SDK for simplified API interaction.
- Express (Optional): A minimal web framework for Node.js, used to create a simple API endpoint.
- dotenv: A module to load environment variables from a
.env
file.
We assume you have a basic understanding of JavaScript, Node.js, and REST APIs.
Prerequisites
Before you begin, ensure you have the following:
- Node.js and npm (or yarn): Installed on your development machine. You can download them from nodejs.org.
- An Infobip Account: Sign up for a free trial or use your existing account at infobip.com. A free trial account is sufficient to complete this guide, though it has limitations (e.g., sending only to your verified phone number), which will be noted where relevant.
- Infobip API Key and Base URL: You'll need these credentials to authenticate your API requests.
Getting Your Infobip API Credentials
You need two key pieces of information from your Infobip account:
- API Key: This authenticates your requests.
- Base URL: This is the specific API endpoint domain assigned to your account.
Steps to find your credentials:
- Log in to your Infobip account.
- Navigate to the homepage dashboard after login.
- Your Base URL should be prominently displayed (e.g.,
xxxxx.api.infobip.com
). - Click your username or profile icon, usually in the top-right corner.
- Look for an option like
API Keys
or navigate to theDevelopers
section. - You can either view existing API keys or generate a new one. Securely copy the API Key value. Treat this key like a password — do not share it publicly or commit it to version control.
Keep these two values handy; we'll use them shortly.
1. Setting up the Project
Let's create a new Node.js project.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir node-infobip-sms cd node-infobip-sms
-
Initialize Node.js Project: Run
npm init
and follow the prompts. You can accept the defaults by pressing Enter repeatedly, or customize the details. This creates apackage.json
file.npm init -y
(The
-y
flag accepts all defaults) -
Install Core Dependencies: We'll need
dotenv
to manage our API credentials securely.npm install dotenv
(If using yarn:
yarn add dotenv
) -
Create
.gitignore
: It's crucial to prevent committing sensitive information and unnecessary files. Create a file named.gitignore
in your project root:# .gitignore # Dependencies node_modules/ # Environment variables .env # Log files *.log # OS generated files .DS_Store Thumbs.db
-
Create
.env
File: Create a file named.env
in the project root. This file will store your sensitive Infobip credentials. Add your API Key and Base URL here:# .env INFOBIP_API_KEY=YOUR_API_KEY_HERE INFOBIP_BASE_URL=YOUR_BASE_URL_HERE # Optional: Your phone number for testing (must be verified for free trial accounts) YOUR_VERIFIED_PHONE_NUMBER=YOUR_PHONE_NUMBER_HERE
Important: Replace the placeholder values with your actual Infobip API Key, Base URL, and optionally, your verified phone number (including the country code, e.g.,
+15551234567
).
Now your basic project structure is ready.
2. Implementing SMS Sending Functionality
We'll explore two ways to send an SMS:
- Approach 1: Manually constructing and sending HTTP requests using
axios
. This provides a deeper understanding of the API interaction. - Approach 2: Using the official
@infobip-api/sdk
. This simplifies the process and is generally recommended for production use.
Approach 1: Manual Integration with Axios
This approach involves using the axios
HTTP client to directly interact with the Infobip Send SMS API endpoint (/sms/2/text/advanced
).
-
Install Axios:
npm install axios
(If using yarn:
yarn add axios
) -
Create
sendManual.js
: Create a new file namedsendManual.js
. This file will contain the logic for sending the SMS.// sendManual.js const axios = require('axios'); // Helper function to build the API URL const buildUrl = (domain) => { if (!domain) { throw new Error('Infobip domain (Base URL) is mandatory in config.'); } // Ensure the domain doesn't include 'https://' as we add it here const cleanDomain = domain.replace(/^https?:\/\//, ''); return `https://${cleanDomain}/sms/2/text/advanced`; }; // Helper function to build request headers const buildHeaders = (apiKey) => { if (!apiKey) { throw new Error('Infobip API Key is mandatory in config.'); } return { 'Content-Type': 'application/json', 'Accept': 'application/json', // Good practice to accept JSON responses 'Authorization': `App ${apiKey}` }; }; // Helper function to construct the request body const buildRequestBody = (destinationNumber, message, sender = 'InfoSMS') => { // Basic validation: optional '+' followed by 10-15 digits. if (!/^\+?\d{10,15}$/.test(destinationNumber)) { console.warn(`Destination number ""${destinationNumber}"" may not be in the correct international format (e.g., +15551234567).`); // Depending on requirements, you might throw an error here instead. } if (!message || typeof message !== 'string' || message.trim() === '') { throw new Error('Message content cannot be empty.'); } const destinationObject = { to: destinationNumber }; const messageObject = { destinations: [destinationObject], text: message, // Optional: Sender ID ('from' field). Using defaults like 'InfoSMS'. // Note: Alphanumeric Sender IDs often require registration and may not work reliably everywhere without it. See 'Special Cases' section. from: sender }; return { messages: [messageObject] }; }; // Helper function to parse successful responses const parseSuccessResponse = (axiosResponse) => { const responseBody = axiosResponse.data; // Handle cases where the response might not contain messages (unlikely for success, but defensive) if (!responseBody || !responseBody.messages || responseBody.messages.length === 0) { return { success: true, data: responseBody, warning: ""Successful response but no message details found."" }; } const singleMessageResponse = responseBody.messages[0]; return { success: true, messageId: singleMessageResponse.messageId, status: singleMessageResponse.status.name, groupName: singleMessageResponse.status.groupName, // More descriptive status group description: singleMessageResponse.status.description, bulkId: responseBody.bulkId // Include bulkId if present }; }; // Helper function to parse error responses const parseFailedResponse = (axiosError) => { let errorInfo = { success: false, errorMessage: axiosError.message, errorDetails: null, statusCode: null }; if (axiosError.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx const responseBody = axiosError.response.data; errorInfo.statusCode = axiosError.response.status; if (responseBody && responseBody.requestError && responseBody.requestError.serviceException) { errorInfo.errorMessage = responseBody.requestError.serviceException.text || 'Unknown Infobip service exception'; errorInfo.errorDetails = responseBody; // Include the full error body } else { errorInfo.errorMessage = `Infobip API Error (Status ${errorInfo.statusCode}): ${axiosError.response.statusText}`; errorInfo.errorDetails = responseBody || axiosError.response.statusText; } } else if (axiosError.request) { // The request was made but no response was received errorInfo.errorMessage = 'No response received from Infobip API. Check network connectivity or Base URL.'; } else { // Something happened in setting up the request that triggered an Error errorInfo.errorMessage = `Axios request setup error: ${axiosError.message}`; } return errorInfo; }; // Main function to send SMS const sendSmsManually = async (config, destinationNumber, message) => { try { // Validate input configuration if (!config || !config.domain || !config.apiKey) { throw new Error('Configuration object with domain and apiKey is required.'); } if (!destinationNumber) throw new Error('destinationNumber parameter is mandatory'); if (!message) throw new Error('message parameter is mandatory'); const url = buildUrl(config.domain); const requestBody = buildRequestBody(destinationNumber, message); const headers = buildHeaders(config.apiKey); console.log(`Sending SMS manually to ${destinationNumber} via ${url}`); const response = await axios.post(url, requestBody, { headers: headers }); return parseSuccessResponse(response); } catch (error) { const parsedError = parseFailedResponse(error); console.error('Error sending SMS manually:', parsedError); return parsedError; // Return the structured error object } }; module.exports = { sendSmsManually };
Why this structure? Breaking down the logic into smaller, focused functions (URL building, header construction, body construction, response parsing) makes the code cleaner, easier to test, and more maintainable. The main
sendSmsManually
function orchestrates these steps and handles the overall success/error flow usingasync/await
for better readability. We added basic input validation and more robust error parsing. -
Create
index.js
for Testing: Create a file namedindex.js
in the project root to test thesendManual.js
module.// index.js require('dotenv').config(); // Load environment variables from .env file const { sendSmsManually } = require('./sendManual'); // Import the manual function // --- Configuration --- const infobipConfig = { domain: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, }; const recipientPhoneNumber = process.env.YOUR_VERIFIED_PHONE_NUMBER; // Use the number from .env const messageText = `Hello from Node.js (Manual Axios)! Time: ${new Date().toLocaleTimeString()}`; // --- Validate Configuration --- if (!infobipConfig.domain || !infobipConfig.apiKey) { console.error('ERROR: INFOBIP_BASE_URL and INFOBIP_API_KEY must be set in the .env file.'); process.exit(1); // Exit if configuration is missing } if (!recipientPhoneNumber) { console.error('ERROR: YOUR_VERIFIED_PHONE_NUMBER must be set in the .env file for testing.'); process.exit(1); } // --- Send SMS --- console.log(`Attempting to send SMS to: ${recipientPhoneNumber}`); sendSmsManually(infobipConfig, recipientPhoneNumber, messageText) .then(result => { console.log('Manual Send Result:', JSON.stringify(result, null, 2)); if(result.success) { console.log(`SMS submitted successfully! Message ID: ${result.messageId}, Status: ${result.status}`); } else { console.error(`SMS sending failed. Error: ${result.errorMessage}`); } }) .catch(error => { // This catch is for unexpected errors not handled within sendSmsManually console.error('Unexpected error in index.js:', error); });
-
Run the Test: Execute the
index.js
file from your terminal:node index.js
You should see output indicating the attempt and then either a success message with details or an error message. Check your phone for the SMS! Remember, free trial accounts can typically only send to the phone number used during signup.
Approach 2: Using the Official Infobip Node.js SDK
The official SDK (@infobip-api/sdk
) abstracts away the direct HTTP calls, providing a cleaner interface.
-
Install the SDK:
npm install @infobip-api/sdk
(If using yarn:
yarn add @infobip-api/sdk
) -
Create
sendSdk.js
: Create a new file namedsendSdk.js
.// sendSdk.js const { Infobip, AuthType } = require('@infobip-api/sdk'); let infobipClient; // Keep client instance reusable // Function to initialize the client (can be called once) const initializeInfobipClient = (config) => { if (!config || !config.domain || !config.apiKey) { throw new Error('Configuration object with domain and apiKey is required for SDK initialization.'); } if (!infobipClient) { console.log(""Initializing Infobip SDK client...""); infobipClient = new Infobip({ baseUrl: config.domain, // SDK expects the full base URL apiKey: config.apiKey, authType: AuthType.ApiKey, // Specify API Key authentication }); } return infobipClient; }; // Main function to send SMS using the SDK const sendSmsWithSdk = async (config, destinationNumber, message, sender = 'InfoSDK') => { try { const client = initializeInfobipClient(config); // Get or initialize client if (!destinationNumber) throw new Error('destinationNumber parameter is mandatory'); if (!message) throw new Error('message parameter is mandatory'); // Basic validation: optional '+' followed by 10-15 digits. if (!/^\+?\d{10,15}$/.test(destinationNumber)) { console.warn(`Destination number ""${destinationNumber}"" may not be in the correct international format (e.g., +15551234567).`); } console.log(`Sending SMS via SDK to ${destinationNumber}`); // Construct the payload using the SDK's structure const sendSmsPayload = { messages: [{ destinations: [{ to: destinationNumber }], // Optional: Sender ID ('from' field). Using defaults like 'InfoSDK'. // Note: Alphanumeric Sender IDs often require registration and may not work reliably everywhere without it. See 'Special Cases' section. from: sender, text: message, }], // You can add more options here like bulkId, notifyUrl etc. // See SDK documentation for details. }; // Use the client instance to send the SMS const infobipResponse = await client.channels.sms.send(sendSmsPayload); // Parse the SDK response (structure is slightly different from manual) const responseData = infobipResponse.data; // SDK wraps response in 'data' if (!responseData || !responseData.messages || responseData.messages.length === 0) { return { success: true, data: responseData, warning: ""Successful response but no message details found."" }; } const singleMessageResponse = responseData.messages[0]; return { success: true, messageId: singleMessageResponse.messageId, status: singleMessageResponse.status.name, groupName: singleMessageResponse.status.groupName, description: singleMessageResponse.status.description, bulkId: responseData.bulkId }; } catch (error) { console.error('Error sending SMS with SDK:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message); // Parse SDK error structure (often nested in error.response.data) let errorMessage = error.message; let errorDetails = null; let statusCode = error.response ? error.response.status : null; if (error.response && error.response.data && error.response.data.requestError && error.response.data.requestError.serviceException) { errorMessage = error.response.data.requestError.serviceException.text || 'Unknown Infobip SDK service exception'; errorDetails = error.response.data; } else if (error.response && error.response.data) { errorMessage = `Infobip SDK Error (Status ${statusCode})`; errorDetails = error.response.data; } return { success: false, errorMessage: errorMessage, errorDetails: errorDetails, statusCode: statusCode }; } }; module.exports = { sendSmsWithSdk, initializeInfobipClient }; // Export both
Why this structure? The SDK simplifies interaction significantly. We initialize the client once (or check if it exists) for efficiency. The
sendSmsWithSdk
function focuses on building the correct payload structure expected by the SDK method (client.channels.sms.send
) and handling its specific response and error formats. -
Update
index.js
for SDK Testing: Modifyindex.js
to use the SDK function.// index.js require('dotenv').config(); // Choose one implementation to test: // const { sendSmsManually } = require('./sendManual'); const { sendSmsWithSdk } = require('./sendSdk'); // Import the SDK function // --- Configuration --- const infobipConfig = { domain: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, }; const recipientPhoneNumber = process.env.YOUR_VERIFIED_PHONE_NUMBER; // Change message slightly to distinguish SDK test const messageText = `Hello from Node.js (Official SDK)! Time: ${new Date().toLocaleTimeString()}`; // --- Validate Configuration --- if (!infobipConfig.domain || !infobipConfig.apiKey) { console.error('ERROR: INFOBIP_BASE_URL and INFOBIP_API_KEY must be set in the .env file.'); process.exit(1); } if (!recipientPhoneNumber) { console.error('ERROR: YOUR_VERIFIED_PHONE_NUMBER must be set in the .env file for testing.'); process.exit(1); } // --- Send SMS --- console.log(`Attempting to send SMS to: ${recipientPhoneNumber} using the SDK`); // Call the SDK function sendSmsWithSdk(infobipConfig, recipientPhoneNumber, messageText) .then(result => { console.log('SDK Send Result:', JSON.stringify(result, null, 2)); if(result.success) { console.log(`SDK: SMS submitted successfully! Message ID: ${result.messageId}, Status: ${result.status}`); } else { console.error(`SDK: SMS sending failed. Error: ${result.errorMessage}`); } }) .catch(error => { console.error('Unexpected error in index.js:', error); });
-
Run the Test:
node index.js
Again, check your console output and your phone.
Choosing an Approach
- Manual (
axios
): Good for learning exactly how the API works, offers maximum control over the request, fewer dependencies if you already useaxios
. However, it requires more boilerplate code and manual maintenance if the API changes. - Official SDK (
@infobip-api/sdk
): Recommended for most production scenarios. It's easier to use, reduces code volume, handles authentication and request structure internally, and is maintained by Infobip, ensuring compatibility with API updates.
For robustness and ease of maintenance, using the official SDK is generally the preferred approach.
3. Building a Simple API Layer (Optional)
Exposing the SMS functionality via a simple web API is a common requirement. We'll use Express to create an endpoint.
-
Install Express:
npm install express
(If using yarn:
yarn add express
) -
Create
server.js
: Create a file namedserver.js
. We'll use the SDK implementation here for brevity.// server.js require('dotenv').config(); const express = require('express'); const { sendSmsWithSdk, initializeInfobipClient } = require('./sendSdk'); // Use SDK const app = express(); const port = process.env.PORT || 3000; // Use environment variable or default // --- Middleware --- app.use(express.json()); // Enable parsing JSON request bodies // --- Configuration --- const infobipConfig = { domain: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, }; // --- Validate Configuration on Startup --- if (!infobipConfig.domain || !infobipConfig.apiKey) { console.error('FATAL ERROR: INFOBIP_BASE_URL and INFOBIP_API_KEY must be set in the environment variables.'); process.exit(1); } // --- Initialize Infobip Client --- // Initialize client once when server starts try { initializeInfobipClient(infobipConfig); console.log(""Infobip client initialized successfully.""); } catch (error) { console.error(""FATAL ERROR: Failed to initialize Infobip client:"", error.message); process.exit(1); } // --- API Routes --- app.get('/', (req, res) => { res.send('SMS Service is running. Use POST /send to send an SMS.'); }); app.post('/send', async (req, res) => { const { to, message } = req.body; // Get recipient number and message from request body // --- Basic Input Validation --- if (!to || !message) { // Use backticks for field names return res.status(400).json({ success: false, error: 'Missing required fields: `to` (phone number) and `message`.' }); } // Add more robust validation for phone number format if needed if (typeof to !== 'string' || typeof message !== 'string' || message.trim() === '') { return res.status(400).json({ success: false, error: 'Invalid input format. `to` and `message` must be non-empty strings.' }); } console.log(`Received request to send SMS to: ${to}`); try { // Assuming infobipConfig is correctly populated from environment variables const result = await sendSmsWithSdk(infobipConfig, to, message); if (result.success) { console.log(`API: SMS submitted successfully for ${to}. Message ID: ${result.messageId}`); // Return only relevant success info to the client res.status(200).json({ success: true, messageId: result.messageId, status: result.status, description: result.description }); } else { console.error(`API: Failed to send SMS to ${to}. Error: ${result.errorMessage}`, result.errorDetails || ''); // Return a generic server error or specific error if safe res.status(result.statusCode || 500).json({ success: false, error: result.errorMessage || 'Failed to send SMS due to an internal error.', // Optionally include errorDetails in non-production environments for debugging details: process.env.NODE_ENV !== 'production' ? result.errorDetails : undefined }); } } catch (error) { // Catch unexpected errors during the API call processing console.error(`API: Unexpected error processing /send request for ${to}:`, error); res.status(500).json({ success: false, error: 'An unexpected server error occurred.' }); } }); // --- Start Server --- app.listen(port, () => { console.log(`SMS API server listening on port ${port}`); console.log(`Infobip Base URL configured: ${infobipConfig.domain}`); });
-
Run the Server:
node server.js
The server will start, typically on port 3000.
-
Test the API Endpoint: You can use
curl
or a tool like Postman to send a POST request.Using
curl
: ReplaceYOUR_PHONE_NUMBER
with the recipient's number (must be verified for free trial).curl -X POST http://localhost:3000/send \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""YOUR_PHONE_NUMBER"", ""message"": ""Hello from the Express API!"" }'
Expected Response (Success):
{ ""success"": true, ""messageId"": ""SOME_MESSAGE_ID_FROM_INFOBIP"", ""status"": ""PENDING_ACCEPTED"", ""description"": ""Message accepted and pending processing."" }
Expected Response (Failure - e.g., missing field):
{ ""success"": false, ""error"": ""Missing required fields: `to` (phone number) and `message`."" }
4. Integrating with Third-Party Services (Infobip)
We've already covered the core integration:
-
Configuration: API Key (
INFOBIP_API_KEY
) and Base URL (INFOBIP_BASE_URL
) are stored securely in the.env
file and loaded usingdotenv
. -
Authentication: Handled either via the
Authorization: App YOUR_API_KEY
header (manual approach) or automatically by the SDK when configured withAuthType.ApiKey
. -
Dashboard Navigation: As detailed in the ""Getting Your Infobip API Credentials"" section.
-
Environment Variables:
INFOBIP_API_KEY
: Your secret key for API authentication. Obtain from Infobip portal API Keys section. Format: String.INFOBIP_BASE_URL
: Your account-specific API domain. Obtain from Infobip portal dashboard. Format: String (e.g.,xxxxx.api.infobip.com
).YOUR_VERIFIED_PHONE_NUMBER
: (Optional, for testing) The phone number linked to your Infobip trial account. Format: String (e.g.,+15551234567
).PORT
: (Optional, for server) The port number for the Express server to listen on. Format: Number.
-
Fallback Mechanisms: For simple SMS sending, a basic fallback might involve retrying the request (see Error Handling). For critical systems, you might consider alternative providers or channels, but that's beyond this basic scope.
5. Error Handling, Logging, and Retry Mechanisms
Robust applications need solid error handling.
-
Error Handling Strategy:
- Both
sendManual.js
andsendSdk.js
includetry...catch
blocks. - Specific Infobip errors (usually returned in the response body on non-2xx status codes) are parsed and returned in a consistent
{ success: false, errorMessage: '...', errorDetails: '...', statusCode: ... }
format. - Network errors or request setup issues are also caught.
- The API layer (
server.js
) catches errors during request processing and returns appropriate HTTP status codes (400 for bad input, 500 for server errors, or the status code from Infobip if available).
- Both
-
Logging:
- We are using basic
console.log
andconsole.error
. - For production, replace these with a structured logging library like
pino
orwinston
. This enables:- Different log levels (debug, info, warn, error).
- Outputting logs in JSON format for easier parsing by log management systems.
- Writing logs to files or external services.
- Conceptual Example (using Pino):
Note: This requires installing
pino
(npm install pino
) and initializing the logger.npm install pino pino-pretty # pino-pretty for development logging
// --- Conceptual Pino Integration --- // At the top of server.js (replace console with logger) const pino = require('pino'); // Initialize Pino Logger const logger = pino({ level: process.env.LOG_LEVEL || 'info', // Use pino-pretty for human-readable logs in development, JSON in production transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined, }); // Example Usage (replace console.* calls): // logger.info(`SMS API server listening on port ${port}`); // logger.error({ err: error, to }, `API: Unexpected error processing /send request.`); // --- End Conceptual Pino Integration --- // --- Example usage within server.js --- // Replace console.log(...) with logger.info(...) // Replace console.error(...) with logger.error(...) // Inside /send route error handling: // logger.error({ err: result.errorDetails, to }, `API: Failed to send SMS. Error: ${result.errorMessage}`); // ... // logger.error({ err: error, to }, `API: Unexpected error processing /send request.`); // Start server (using logger) app.listen(port, () => { logger.info(`SMS API server listening on port ${port}`); logger.info(`Infobip Base URL configured: ${infobipConfig.domain}`); });
- We are using basic
-
Retry Mechanisms:
- Sending an SMS is often idempotent (sending the same message twice might be acceptable or undesirable depending on context).
- For transient network errors or specific temporary Infobip issues (e.g., 5xx status codes), you might implement retries.
- Use libraries like
async-retry
or implement a manual loop with exponential backoff (wait longer between each retry). - Caution: Be careful not to retry on permanent errors (like invalid API keys or invalid phone numbers - 4xx errors) or you'll waste resources.
- Conceptual Example (using
async-retry
): Note: This requires installingasync-retry
(npm install async-retry
) and assumes alogger
instance is available.npm install async-retry
// --- Conceptual Async-Retry Integration (within sendSdk.js) --- const retry = require('async-retry'); // Assumes a 'logger' instance (like Pino from above) is available in this scope // Inside sendSmsWithSdk function, wrap the API call: // try { // ... setup before call ... const sendOperation = async (bail, attemptNumber) => { // bail(error) is used to stop retrying on permanent errors const client = initializeInfobipClient(config); // Ensure client is ready try { logger.info(`Attempt ${attemptNumber}: Sending SMS via SDK to ${destinationNumber}`); const infobipResponse = await client.channels.sms.send(sendSmsPayload); return infobipResponse; // Success! Return the response } catch (error) { // Decide if the error is permanent (e.g., 4xx client errors) if (error.response && error.response.status >= 400 && error.response.status < 500) { logger.error(`Permanent error detected (Status ${error.response.status}). Bailing out.`_ error); bail(error); // Don't retry 4xx errors } // Log other errors and allow retry throw error; // Important: re-throw the error so retry() catches it } }; // Retry logic const response = await retry(sendOperation_ { retries: 3_ // Number of retries factor: 2_ // Exponential backoff factor minTimeout: 1000_ // Initial delay ms maxTimeout: 5000_ // Max delay ms onRetry: (error_ attemptNumber) => { logger.warn({ err: error }, `Attempt ${attemptNumber} failed. Retrying...`); } }); // Parse success response (same as before) const responseData = response.data; // ... rest of success parsing ... if (!responseData || !responseData.messages || responseData.messages.length === 0) { return { success: true, data: responseData, warning: ""Successful response but no message details found."" }; } const singleMessageResponse = responseData.messages[0]; return { success: true, messageId: singleMessageResponse.messageId, status: singleMessageResponse.status.name, groupName: singleMessageResponse.status.groupName, description: singleMessageResponse.status.description, bulkId: responseData.bulkId }; } catch (error) { // This catch block now handles errors after all retries failed logger.error({ err: error }, 'Error sending SMS with SDK after retries'); // Parse SDK error structure (same as before) let errorMessage = error.message; let errorDetails = null; let statusCode = error.response ? error.response.status : null; if (error.response && error.response.data && error.response.data.requestError && error.response.data.requestError.serviceException) { errorMessage = error.response.data.requestError.serviceException.text || 'Unknown Infobip SDK service exception'; errorDetails = error.response.data; } else if (error.response && error.response.data) { errorMessage = `Infobip SDK Error (Status ${statusCode})`; errorDetails = error.response.data; } return { success: false, errorMessage: errorMessage, errorDetails: errorDetails, statusCode: statusCode }; } // }; // End of sendSmsWithSdk function // module.exports = { sendSmsWithSdk, initializeInfobipClient }; // --- End Conceptual Async-Retry Integration ---