This guide provides a step-by-step walkthrough for building a simple Node.js application using the Express framework to send SMS messages via the Infobip API. We'll cover project setup, core implementation, API key management, basic error handling, and testing.
By the end of this tutorial, you will have a functional Express API endpoint that accepts a phone number and message text, then uses the Infobip API to dispatch the SMS. This serves as a foundational building block for integrating SMS functionality into larger applications for notifications, alerts, or user communication.
Project Overview and Goals
- Goal: Create a backend service that can send SMS messages programmatically.
- Problem Solved: Automates the process of sending transactional SMS, eliminating manual sending and enabling integration with other systems.
- Technologies Used:
- Node.js: A JavaScript runtime for building server-side applications. Chosen for its asynchronous nature and large ecosystem (npm).
- Express: A minimal and flexible Node.js web application framework. Chosen for its simplicity in setting up API routes.
- Axios: A promise-based HTTP client for Node.js. Chosen for making requests to the Infobip API reliably.
- dotenv: A module to load environment variables from a
.env
file. Chosen for securely managing API keys and configuration. - Infobip API: The third-party service used for dispatching SMS messages.
- Architecture:
+-----------------+ +-----------------+ +-----------------+ | 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 a single API endpoint (
POST /api/v1/sms/send
) that triggers an SMS message via Infobip. - Prerequisites:
- Node.js and npm (or yarn) installed. (We recommend using Node Version Manager (
nvm
) to manage Node.js versions). - An active Infobip account. You can create one on the Infobip website.
- Basic understanding of JavaScript, Node.js, REST APIs, and command-line usage.
- Important Note for Free Trials: Infobip free trial accounts can typically only send SMS messages to the phone number verified during signup. Testing with other numbers will likely fail until the account is funded.
- Node.js and npm (or yarn) installed. (We recommend using Node Version Manager (
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for your project. Navigate into it.
mkdir infobip-sms-sender cd infobip-sms-sender
-
Initialize Node.js Project: Run
npm init
to create apackage.json
file. You can accept the defaults by adding the-y
flag.npm init -y
This creates a
package.json
file which tracks your project's metadata and dependencies. -
Install Dependencies: We need
express
for the web server,axios
to make HTTP requests, anddotenv
to handle environment variables.npm install express axios dotenv
This command downloads and installs the packages, adding them to your
node_modules
directory and listing them as dependencies inpackage.json
. -
Set up Project Structure: Create the following directory structure within
infobip-sms-sender
:infobip-sms-sender/ ├── node_modules/ ├── src/ │ ├── controllers/ │ │ └── smsController.js │ ├── routes/ │ │ └── smsRoutes.js │ └── services/ │ └── infobipService.js ├── .env ├── .gitignore ├── app.js └── package.json
src/
: Contains our application source code.controllers/
: Handles incoming requests and orchestrates responses.routes/
: Defines API endpoints and maps them to controllers.services/
: Contains business logic, like interacting with the Infobip API..env
: Stores environment variables (API keys, configuration). Never commit this file to version control..gitignore
: Specifies intentionally untracked files that Git should ignore (likenode_modules
and.env
).app.js
: The main entry point for our Express application.
-
Configure
.gitignore
: Create a file named.gitignore
in the project root and add the following lines to prevent sensitive information and unnecessary files from being committed to Git:# Dependencies node_modules/ # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pids *.pid *.seed *.pid.lock # Optional Editor directories .vscode/ .idea/
-
Configure Environment Variables (
.env
): Create a file named.env
in the project root. You'll need your Infobip API Key and Base URL.- How to find your Infobip API Key and Base URL:
- Log in to your Infobip account.
- Navigate to the Homepage or Dashboard.
- Your API Key and Base URL should be prominently displayed. The Base URL typically looks something like
youruniqueid.api.infobip.com
. - Copy these values.
Add the following lines to your
.env
file, replacing the placeholder values with your actual credentials:# .env # Infobip Credentials INFOBIP_API_KEY=your_infobip_api_key_here INFOBIP_BASE_URL=youruniqueid.api.infobip.com # Server Configuration PORT=3000
INFOBIP_API_KEY
: Your secret API key for authenticating with Infobip.INFOBIP_BASE_URL
: The specific domain provided by Infobip for accessing their API.PORT
: The port number on which your Express server will listen.
Why
.env
? Storing configuration like API keys directly in code is insecure and makes deployment difficult. Environment variables allow you to configure the application differently for development, testing, and production without changing the code.dotenv
loads these variables intoprocess.env
for easy access within your Node.js application. - How to find your Infobip API Key and Base URL:
2. Implementing Core Functionality (Infobip Service)
Now, let's create the service responsible for interacting with the Infobip API. This logic is adapted from the Infobip developer blog post.
-
Create
src/services/infobipService.js
: This file will contain the functions to build the request and call 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', // Good practice to accept JSON responses }; }; // Helper to construct the request body according to Infobip specs const buildRequestBody = (destinationNumber, message) => { // Basic validation if (!destinationNumber || !message) { throw new Error('Destination number and message text are mandatory'); } // Ensure destination number is in international format (e.g., 447... or 1555...) // Basic check - production apps might need more robust validation if (!/^\d+$/.test(destinationNumber)) { console.warn(`Destination number ${destinationNumber} might not be in the correct international format.`); } const destinationObject = { to: destinationNumber, }; const messageObject = { destinations: [destinationObject], text: message, // You can add 'from' here if you have a specific sender ID configured // 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 - The recipient's phone number in international format. * @param {string} message - The text content of the SMS. * @returns {Promise<object>} - A promise that resolves with the parsed success response or rejects with parsed error info. */ const sendSms = async (destinationNumber, message) => { const apiKey = process.env.INFOBIP_API_KEY; const domain = process.env.INFOBIP_BASE_URL; // Validate essential configuration and parameters 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); // Re-throw parsed error for controller handling } }; // Parses the successful response from Infobip const parseSuccessResponse = (axiosResponse) => { const responseBody = axiosResponse.data; // Handle cases where messages array might be empty or structure differs slightly const singleMessageResponse = responseBody.messages && responseBody.messages[0]; if (!singleMessageResponse) { console.warn('Infobip success response structure might be different than expected.', responseBody); return { success: true, data: responseBody }; // Return raw data if structure is unusual } 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 { // Fallback if the error structure is different errorInfo.errorMessage = `Infobip API Error: Status ${errorInfo.statusCode}`; errorInfo.details = responseBody; } } else if (axiosError.request) { // The request was made but no response was received (network error, DNS issue) errorInfo.errorMessage = 'Network Error: No response received from Infobip API.'; } else { // Something happened in setting up the request that triggered an Error errorInfo.errorMessage = `Request Setup Error: ${axiosError.message}`; } errorInfo.rawError = axiosError.message; // Include raw Axios error message return errorInfo; }; module.exports = { sendSms, };
- Why
async/await
? It makes handling asynchronous operations (like theaxios.post
call) much cleaner and easier to read than traditional Promise.then()
and.catch()
chains. - Why separate helpers? Breaking down the logic into smaller, focused functions (
buildUrl
,buildHeaders
,buildRequestBody
,parseSuccessResponse
,parseFailedResponse
) improves code readability, maintainability, and testability. - Configuration from
process.env
: The service directly accesses the API key and base URL from environment variables loaded bydotenv
, ensuring credentials aren't hardcoded.
- Why
3. Building the API Layer (Routes and Controller)
Now, let's define the Express route and controller that will receive incoming requests and use our infobipService
.
-
Create
src/controllers/smsController.js
: This controller handles the request logic.// src/controllers/smsController.js const infobipService = require('../services/infobipService'); const handleSendSms = async (req, res) => { // --- 1. Basic Input Validation --- const { to, text } = req.body; // Extract phone number and message text from request 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)' }); } // Add more robust validation if needed (e.g., phone number format, text length) // Example: Basic check for digits-only in 'to' (allowing common characters first) if (typeof to !== 'string' || !/^\d+$/.test(to.replace(/[\s+()-]/g, ''))) { // Allow '+' prefix, digits, spaces, hyphens, parentheses, remove them for digit check console.warn(`Received potentially invalid phone number format: ${to}. Ensure it uses international format (e.g., 15551234567).`); // Production Recommendation: Implement stricter validation & uncomment below to reject. // return res.status(400).json({ success: false, message: 'Invalid format for field \'to\'. Use international format without leading \'+\' (e.g., 15551234567).' }); } try { // --- 2. Call the Infobip Service --- console.log(`Controller received request to send SMS to: ${to}`); const result = await infobipService.sendSms(to, text); // --- 3. Handle Successful Response --- console.log('SMS processed successfully by Infobip service:', result); // Send back the parsed success response from the service return res.status(200).json(result); } catch (error) { // --- 4. Handle Errors from the Service --- console.error('Error in handleSendSms controller:', error); // The error object is already parsed by infobipService.parseFailedResponse // Determine appropriate status code based on parsed error const statusCode = error.statusCode || 500; // Default to 500 if no specific code return res.status(statusCode).json({ success: false, message: error.errorMessage || 'Failed to send SMS due to an internal error.', details: error.details || error.rawError, // Provide details if available }); } }; module.exports = { handleSendSms, };
- Input Validation: It performs basic checks to ensure the
to
andtext
fields are present in the request body. Production applications should implement more robust validation (e.g., using libraries likejoi
orexpress-validator
). The example includes a basic regex check for theto
field format and logs a warning for potential issues; for production, you should implement more robust validation and likely uncomment the provided code (or use a library) to reject formats known to be invalid according to Infobip requirements (international format without leading '+'). - Service Call: It calls the
sendSms
function from ourinfobipService
. - Response Handling: It sends a JSON response back to the client – either the success details from Infobip or a structured error message.
- Input Validation: It performs basic checks to ensure the
-
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); // You could add other SMS-related routes here later (e.g., get status, receive replies) module.exports = router;
- It imports the
smsController
. - It creates an Express router instance.
- It defines a
POST
route at/send
(which will be prefixed later) and maps it to thehandleSendSms
controller function.
- It imports the
-
Set up the Main Application File (
app.js
): This file initializes Express, loads environment variables, sets up middleware, and mounts the routes.// app.js const express = require('express'); const dotenv = require('dotenv'); const smsRoutes = require('./src/routes/smsRoutes'); // Import the router // Load environment variables from .env file dotenv.config(); // Create the Express application const app = express(); // Middleware to parse JSON request bodies app.use(express.json()); // Middleware for basic logging (optional but helpful) app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); // Pass control to the next middleware/route handler }); // Mount the SMS routes under the /api/v1/sms prefix app.use('/api/v1/sms', smsRoutes); // Basic health check endpoint (good practice) app.get('/health', (req, res) => { res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); // Simple 404 handler for undefined routes app.use((req, res, next) => { res.status(404).json({ success: false, message: 'Resource not found' }); }); // Basic error handler middleware (catches errors not handled in routes) app.use((err, req, res, next) => { console.error('Unhandled Error:', err); res.status(500).json({ success: false, message: 'An unexpected internal server error occurred.' }); }); // Get the port from environment variables or default to 3000 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}`); // Avoid logging the API key itself for security 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 variables from.env
intoprocess.env
. Crucially, this should be called before accessingprocess.env
variables (like ininfobipService.js
).express.json()
: Middleware to parse incoming requests with JSON payloads (available underreq.body
).- Route Mounting: The
smsRoutes
are mounted under the/api/v1/sms
path. This means the final endpoint URL will behttp://localhost:3000/api/v1/sms/send
. Versioning API paths (/v1/
) is a good practice. - Health Check: A simple
/health
endpoint is included, useful for monitoring. - Server Start: Starts the Express server listening on the configured port.
4. Integrating with Infobip (Configuration Recap)
The core integration happens within src/services/infobipService.js
using the credentials stored in .env
.
- Configuration Files:
.env
: StoresINFOBIP_API_KEY
andINFOBIP_BASE_URL
.
- Secure Handling:
- The API key is read via
process.env.INFOBIP_API_KEY
. .env
is included in.gitignore
to prevent accidental commits.
- The API key is read via
- Dashboard Steps:
- Log in to Infobip Portal.
- Locate API Key and Base URL on the main dashboard/homepage.
- Copy these values into your project's
.env
file.
- Environment Variables Explained:
INFOBIP_API_KEY
: (String, Required) Your unique secret key provided by Infobip. Used in theAuthorization: App <key>
header for authentication. Obtain from Infobip dashboard.INFOBIP_BASE_URL
: (String, Required) The specific domain assigned to your Infobip account (e.g.,xyz123.api.infobip.com
). Used to construct the target API endpoint URL. Obtain from Infobip dashboard.PORT
: (Number, Optional, Default: 3000) The port your local Node.js server will run on.
(Note: For this basic example, no specific dashboard configurations beyond obtaining the API Key/Base URL are typically required for simple SMS sending. Fallback mechanisms are not implemented here but could involve trying alternative providers or queuing messages if Infobip is down).
5. Implementing Error Handling and Logging
Error handling is built into the service and controller:
- Strategy:
- The
infobipService
usestry...catch
around theaxios
call. - It calls
parseFailedResponse
to create a standardized error object, whether it's an API error (4xx/5xx) or a network error. - The service throws this parsed error object.
- The
smsController
usestry...catch
to catch errors thrown by the service. - The controller inspects the caught error object (specifically
statusCode
anderrorMessage
) to send an appropriate HTTP status code and JSON response to the client. - Basic
console.log
andconsole.error
are used for logging requests and errors.
- The
- Logging:
- Requests are logged in
app.js
. - Service calls and outcomes are logged in
infobipService.js
. - Errors are logged in both the service and the controller catch blocks.
- (Production Enhancement: Use a dedicated logging library like
Winston
orPino
for structured logging, different log levels (debug, info, warn, error), and routing logs to files or external services.)
- Requests are logged in
- Retry Mechanisms:
- Retries are not implemented in this basic example.
- (Production Enhancement: For transient network errors or specific Infobip rate limit responses (e.g.,
429 Too Many Requests
), you could implement retries with exponential backoff using libraries likeaxios-retry
or manually within the service layer.)
- Testing Errors:
- Invalid API Key: Change
INFOBIP_API_KEY
in.env
to an incorrect value and send a request. Expect a401 Unauthorized
error response. - Invalid Base URL: Change
INFOBIP_BASE_URL
to a non-existent domain. Expect a network error (likelyENOTFOUND
or timeout). - Missing Fields: Send a request without
to
ortext
in the body. Expect a400 Bad Request
response from the controller's validation. - Invalid Phone Number (Format): While Infobip might still accept some variations, sending a clearly invalid
to
value (e.g.,'abcde'
) might result in an error from Infobip (often a400 Bad Request
indicating an invalid destination address).
- Invalid API Key: Change
6. Database Schema and Data Layer
This specific example focuses solely on sending an SMS and does not require or include a database.
- Why no database? The service is stateless; it receives a request, calls the external API, and returns the result. It doesn't need to store message history, user details, or application state.
- When would you need a database?
- To store a history of sent messages, including their status (e.g.,
messageId
,status
,timestamp
). - To manage user profiles if the SMS are sent to registered users.
- To queue messages for later sending or retries.
- To store templates for messages.
- To store a history of sent messages, including their status (e.g.,
- (If a database were needed, you might use PostgreSQL, MySQL, or MongoDB with an ORM/ODM like
Prisma
,Sequelize
, orMongoose
. You would define schemas, handle migrations, and create data access functions within the service layer or a dedicated data layer.)
7. Adding Security Features
Basic security is considered, but production systems require more.
- API Key Security: Handled via
.env
and.gitignore
. Crucial. - Input Validation: Basic checks for required fields (
to
,text
) are insmsController.js
. The controller also includes a basic warning for potentially invalid phone number formats.- (Production Enhancement: Use libraries like
joi
orexpress-validator
for stricter schema validation, type checking, format validation (e.g., regex for phone numbers), and length limits. Ensure phone numbers strictly adhere to the expected international format before sending to Infobip.) - (Sanitization: While less critical for simple SMS text, libraries like
DOMPurify
(for HTML) or custom logic might be needed if user input could be rendered elsewhere.)
- (Production Enhancement: Use libraries like
- Rate Limiting: Not implemented. Essential for public-facing APIs to prevent abuse.
- (Production Enhancement: Use
express-rate-limit
middleware to limit requests per IP address or user.)
- (Production Enhancement: Use
- Common Vulnerabilities:
- Injection: Less relevant for this specific API call, but always validate and sanitize input if it's used in database queries or commands.
- Authentication/Authorization: This endpoint is currently unauthenticated (anyone who can reach it can use it). For internal use, network restrictions might suffice. For external use, implement API key validation,
JWT
tokens, or other authentication methods.
- HTTPS: Ensure your Node.js application is deployed behind a reverse proxy (like
Nginx
orCaddy
) configured forHTTPS
, or use platform services (like Heroku, Render) that handle TLS termination. Do not handleTLS
directly in Node.js unless necessary. - Dependency Security: Regularly audit dependencies for known vulnerabilities (
npm audit
).
8. Handling Special Cases
- Phone Number Formatting: The Infobip API generally expects numbers in international format (e.g.,
15551234567
for US,447911123456
for UK). The code includes a basic check/warning but doesn't enforce strict formatting or reject invalid formats by default. Production apps often need pre-processing or libraries (like Google'slibphonenumber
) to validate and format numbers correctly before sending. - Message Encoding & Length: Standard SMS messages have length limits (160 characters for
GSM-7
encoding, fewer forUCS-2
used for non-Latin characters). Infobip handles concatenation for longer messages, but billing might be per segment. Be mindful of message length. This example doesn't explicitly handle encoding or splitting. - Sender ID: The
from
field in the Infobip payload allows specifying a sender ID (alphanumeric string or phone number). This often requires pre-registration and approval with Infobip, especially for alphanumeric IDs. The example code comments out thefrom
field. - Free Trial Limitations: Infobip free trial accounts can usually only send SMS to the phone number registered during signup. Sending to other numbers will fail until the account is upgraded/funded. This is noted in the Prerequisites section.
- Internationalization: Not applicable for this basic sending function itself, but if message content needed translation based on the destination number's country code, that logic would reside in the calling application or potentially before the
smsController
.
9. Implementing Performance Optimizations
For this simple, single API call per request, performance bottlenecks are unlikely within the Node.js application itself. The main latency will be the network round-trip to the Infobip API.
- Connection Pooling:
axios
(and Node.js's underlying HTTP agent) handles connection pooling/reuse automatically, which is the primary optimization here. - Caching: Caching is generally not applicable for unique transactional SMS messages.
- Asynchronous Nature: Node.js's non-blocking I/O ensures the server remains responsive while waiting for Infobip's response.
- (Production Enhancement: If sending very high volumes, monitor event loop lag, CPU, and memory usage. Consider horizontal scaling (running multiple instances of the app behind a load balancer). Analyze Infobip API response times.)
10. Adding Monitoring, Observability, and Analytics
Basic logging is included. Production systems need more robust monitoring.
- Health Checks: The
/health
endpoint provides a basic check. - Logging:
console.log
/console.error
provide minimal visibility.- (Production Enhancement: Integrate structured logging (
Winston
/Pino
) sending logs to services likeDatadog
,Logz.io
, orELK
stack for aggregation and analysis.)
- (Production Enhancement: Integrate structured logging (
- Metrics: Track request rates, error rates, and latency (especially for the Infobip API call).
- (Production Enhancement: Use libraries like
prom-client
for Prometheus metrics or integrate withAPM
(Application Performance Monitoring) tools likeDatadog APM
,New Relic
, orDynatrace
. These tools automatically instrument Express and Axios calls.)
- (Production Enhancement: Use libraries like
- Error Tracking: Capture and aggregate unhandled exceptions and significant errors.
- (Production Enhancement: Use services like
Sentry
orBugsnag
to capture errors in real-time with stack traces and context.)
- (Production Enhancement: Use services like
- Dashboards: Visualize key metrics (requests/sec, error rate, Infobip latency, SMS success/failure rates derived from Infobip responses or webhooks). Tools like
Grafana
(withPrometheus
),Datadog
, orNew Relic
are commonly used. - Alerting: Configure alerts based on thresholds (e.g., error rate > 5%, Infobip API latency > 2s, health check failing).
11. Troubleshooting and Caveats
- Common Errors:
401 Unauthorized
(from Infobip): IncorrectINFOBIP_API_KEY
. Double-check the key in.env
and the Infobip dashboard.Network Error
/ENOTFOUND
/ECONNREFUSED
/ Timeout: IncorrectINFOBIP_BASE_URL
, DNS issues, firewall blocking outgoing connections, or Infobip API temporary unavailability. Verify the Base URL and network connectivity from the server.400 Bad Request
(from Infobip): Often indicates invalid input. Check:to
number format (should be international).- Missing required fields in the JSON payload structure sent by
axios
. - Invalid
from
sender ID (if used). - Message content issues (e.g., unsupported characters if encoding isn't handled).
- Refer to the
details
field in the error response for specific Infobip error codes/messages (e.g.,Invalid destination address
).
TypeError: Cannot read property '...' of undefined
(in Node.js): Often due to missing environment variables (dotenv.config()
not called early enough, variables missing in.env
) or unexpected response structures from Infobip. Add logging to trace variable values.