code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Developer Guide: Handling Infobip SMS Delivery Status Webhooks with Node.js and Express

A comprehensive guide on building a Node.js/Express application to send SMS via Infobip and process delivery status webhooks (DLRs).

Developer Guide: Handling Infobip SMS Delivery Status Webhooks with Node.js and Express

This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Infobip API and handle real-time delivery status updates through webhooks (callbacks).

By the end of this tutorial, you will have a functional application capable of:

  1. Sending SMS messages programmatically using Infobip.
  2. Receiving delivery reports (DLRs) from Infobip to track message status (e.g., delivered, failed, rejected).
  3. Processing these DLRs within your Express application.

This enables developers to build more robust messaging workflows, providing visibility into message delivery success and enabling actions based on status updates — such as retrying failed messages or logging delivery confirmations.

Target Audience: Developers familiar with Node.js, Express, and basic API concepts.

System Architecture

The system involves the following components:

  1. User/Client: Initiates the request to send an SMS (e.g., via a web form or another service).
  2. Node.js/Express Application:
    • Exposes an API endpoint to receive SMS sending requests.
    • Calls the Infobip API to send the SMS, including a notifyUrl for callbacks.
    • Exposes a webhook endpoint (/infobip-dlr) to receive delivery reports from Infobip.
    • Processes incoming delivery reports.
  3. Infobip Platform:
    • Receives the SMS send request from the Node.js application.
    • Attempts to deliver the SMS to the recipient's handset via mobile networks.
    • Sends a POST request containing the delivery status to the notifyUrl provided by the application.
mermaid
sequenceDiagram
    participant User/Client
    participant Node.js/Express App
    participant Infobip API
    participant Mobile Network/Handset

    User/Client->>+Node.js/Express App: POST /send-sms (to, message)
    Node.js/Express App->>+Infobip API: Send SMS Request (to, message, notifyUrl='/infobip-dlr')
    Infobip API-->>-Node.js/Express App: Acknowledge Request (messageId)
    Node.js/Express App-->>-User/Client: Success/Failure Sending Initiated
    Infobip API->>+Mobile Network/Handset: Deliver SMS
    Mobile Network/Handset-->>-Infobip API: Delivery Status Update
    Infobip API->>+Node.js/Express App: POST /infobip-dlr (Delivery Report Payload)
    Node.js/Express App-->>-Infobip API: 200 OK (Acknowledge Receipt)
    Note over Node.js/Express App: Process DLR (Log, Update DB, etc.)

Prerequisites

  • Node.js and npm (or yarn): Ensure you have a recent version installed. (Node.js Downloads)
  • Infobip Account: You need an active Infobip account. A free trial account works, but remember it typically restricts sending SMS only to the phone number used during registration. (Infobip Signup)
  • Infobip API Key and Base URL: Obtain these from your Infobip account dashboard.
  • Publicly Accessible URL: For Infobip to send delivery reports back to your application, your callback endpoint must be reachable from the public internet. During development, tools like ngrok are essential.
  • Basic understanding: Familiarity with JavaScript, Node.js, Express, REST APIs, and environment variables.

1. Setting up the Project

Let's initialize our Node.js project and install the necessary dependencies.

1.1 Create Project Directory

Open your terminal or command prompt and create a new directory for the project.

bash
mkdir infobip-dlr-app
cd infobip-dlr-app

1.2 Initialize Node.js Project

Initialize the project using npm (or yarn). This creates a package.json file.

bash
npm init -y
  • Why -y? This flag accepts the default settings during initialization, speeding up the process. You can omit it to customize project details.

1.3 Install Dependencies

We need express for our web server, axios to make HTTP requests to the Infobip API, and dotenv to manage environment variables securely.

bash
npm install express axios dotenv
  • express: A minimal and flexible Node.js web application framework.
  • axios: A popular promise-based HTTP client for making requests to external APIs like Infobip's.
  • dotenv: Loads environment variables from a .env file into process.env, keeping sensitive information like API keys out of your source code.

1.4 Create Project Structure

Create the basic files and folders:

bash
# For Linux/macOS
touch index.js .env .gitignore infobipService.js

# For Windows (Command Prompt)
echo. > index.js
echo. > .env
echo. > .gitignore
echo. > infobipService.js

# For Windows (PowerShell)
New-Item index.js -ItemType File
New-Item .env -ItemType File
New-Item .gitignore -ItemType File
New-Item infobipService.js -ItemType File

Your initial structure should look like this:

infobip-dlr-app/ ├── node_modules/ ├── .env ├── .gitignore ├── index.js ├── infobipService.js └── package.json

1.5 Configure .gitignore

Add node_modules and .env to your .gitignore file. This prevents committing dependencies and sensitive credentials to version control.

text
# .gitignore
node_modules
.env
  • Why ignore .env? It contains sensitive API keys and credentials. Never commit this file to public or shared repositories. Team members should create their own .env files based on a template or secure sharing mechanism.

1.6 Set up Environment Variables (.env)

Open the .env file and add placeholders for your Infobip credentials and application settings. You will replace these placeholders later.

dotenv
# .env

# Infobip Credentials
INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY_HERE
INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL_DOMAIN_HERE # e.g., xyz.api.infobip.com

# Application Settings
PORT=3000
APP_BASE_URL=http://localhost:3000 # Replace with your public URL (e.g., ngrok URL) during testing
  • INFOBIP_API_KEY: Your secret key for authenticating API requests.
  • INFOBIP_BASE_URL: The specific domain assigned to your Infobip account for API access.
  • PORT: The port your Express application will listen on.
  • APP_BASE_URL: The base URL where your application is accessible from the internet. Infobip needs this to send callbacks. For local development, this will be your ngrok tunnel URL.

2. Implementing Core Functionality: Sending SMS

Let's create a service to interact with the Infobip API for sending SMS messages.

2.1 Create Infobip Service File

This step was included in the project structure setup (1.4). You should have an infobipService.js file.

2.2 Implement sendSms Function

Add the following code to infobipService.js. This code adapts the logic from the Infobip developer blog post, adding the crucial notifyUrl parameter.

javascript
// infobipService.js
const axios = require('axios');

// Load environment variables (ensure dotenv is configured in index.js first)
const apiKey = process.env.INFOBIP_API_KEY;
const baseUrlDomain = process.env.INFOBIP_BASE_URL;
const appBaseUrl = process.env.APP_BASE_URL; // Base URL for callbacks

/**
 * Constructs the full API endpoint URL for sending SMS.
 * @param {string} domain - The Infobip base URL domain.
 * @returns {string} The full API URL.
 */
const buildUrl = (domain) => {
    if (!domain) {
        throw new Error('INFOBIP_BASE_URL is not defined in environment variables.');
    }
    // Using the recommended Advanced SMS endpoint
    return `https://${domain}/sms/2/text/advanced`;
};

/**
 * Constructs the necessary HTTP headers for Infobip API authentication.
 * @param {string} key - The Infobip API Key.
 * @returns {object} Headers object.
 */
const buildHeaders = (key) => {
    if (!key) {
        throw new Error('INFOBIP_API_KEY is not defined in environment variables.');
    }
    return {
        'Authorization': `App ${key}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json', // Explicitly accept JSON responses
    };
};

/**
 * Constructs the request body for the Infobip Send SMS API.
 * Includes the destination number, message text, and the callback URL.
 * @param {string} destinationNumber - The recipient's phone number in international format (e.g., 447... ).
 * @param {string} message - The text content of the SMS.
 * @param {string} callbackUrl - The publicly accessible URL for delivery reports.
 * @returns {object} Request body object.
 */
const buildRequestBody = (destinationNumber, message, callbackUrl) => {
    const destinationObject = {
        to: destinationNumber,
        // messageId: 'OPTIONAL-CUSTOM-MESSAGE-ID' // Optional: Add a custom ID if needed
    };

    const messageObject = {
        destinations: [destinationObject],
        text: message,
        // from: 'YourSenderID', // Optional: Specify a custom sender ID if configured/allowed
        notifyUrl: callbackUrl, // ** Crucial for receiving delivery reports **
        notifyContentType: 'application/json', // Specify format for DLRs
        // See Infobip docs for more options: validityPeriod, sendAt, etc.
    };

    return {
        messages: [messageObject],
        // bulkId: 'OPTIONAL-CUSTOM-BULK-ID' // Optional: Group messages
    };
};

/**
 * Sends an SMS message using the Infobip API.
 * @param {string} destinationNumber - Recipient's phone number (international format).
 * @param {string} message - SMS text content.
 * @returns {Promise<object>} A promise that resolves with the Infobip API response or rejects with an error.
 */
const sendSms = async (destinationNumber, message) => {
    if (!destinationNumber || !message) {
        throw new Error('Destination number and message text are required.');
    }
    if (!appBaseUrl) {
        throw new Error('APP_BASE_URL is not defined. Needed for callback URL.');
    }

    const apiUrl = buildUrl(baseUrlDomain);
    const headers = buildHeaders(apiKey);
    // Construct the full callback URL
    const callbackUrl = `${appBaseUrl}/infobip-dlr`;
    const requestBody = buildRequestBody(destinationNumber, message, callbackUrl);

    console.log(`Sending SMS to ${destinationNumber} via ${apiUrl}`);
    console.log(`Callback URL set to: ${callbackUrl}`);

    try {
        const response = await axios.post(apiUrl, requestBody, { headers });
        console.log('Infobip API Response:', JSON.stringify(response.data, null, 2));
        // The response here confirms Infobip accepted the request, not final delivery.
        // It includes a messageId useful for tracking.
        return response.data;
    } catch (error) {
        console.error('Error sending SMS via Infobip:');
        if (error.response) {
            // The request was made and the server responded with a status code
            // that falls out of the range of 2xx
            console.error('Status:', error.response.status);
            console.error('Headers:', JSON.stringify(error.response.headers, null, 2));
            console.error('Data:', JSON.stringify(error.response.data, null, 2));
            // Rethrow a more specific error or handle based on status code
            throw new Error(`Infobip API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
        } else if (error.request) {
            // The request was made but no response was received
            console.error('Request Error:', error.request);
            throw new Error('Error sending SMS: No response received from Infobip.');
        } else {
            // Something happened in setting up the request that triggered an Error
            console.error('Axios Error:', error.message);
            throw new Error(`Error sending SMS: ${error.message}`);
        }
    }
};

module.exports = {
    sendSms,
};
  • Why async/await? It simplifies working with promises returned by axios, making the code cleaner and easier to read than using .then() and .catch() chains directly for complex flows.
  • Why notifyUrl and notifyContentType? These parameters instruct Infobip where and how (JSON format) to send the delivery report once the final status of the message is known. This is the core mechanism for enabling callbacks.
  • Why APP_BASE_URL? The callback URL must be absolute and publicly accessible. We construct it using the base URL defined in our environment variables.

3. Building the API Layer and Handling Callbacks

Now, let's set up our Express server, create an endpoint to trigger SMS sending, and critically, an endpoint to receive the delivery reports from Infobip.

3.1 Basic Express Server Setup

Modify your index.js file:

javascript
// index.js

// Load environment variables from .env file
require('dotenv').config();

const express = require('express');
const infobipService = require('./infobipService'); // Import our service

const app = express();
const port = process.env.PORT || 3000; // Use port from .env or default to 3000

// Middleware to parse JSON request bodies
// Crucial for both our API endpoint and receiving Infobip's JSON callbacks
app.use(express.json());

// Simple root route for health check or basic info
app.get('/', (req, res) => {
    res.send('Infobip DLR Handler App is running!');
});

// --- API Endpoint to Send SMS ---
app.post('/send-sms', async (req, res) => {
    const { to, text } = req.body; // Expect 'to' (number) and 'text' (message) in JSON body

    // Basic Input Validation
    if (!to || typeof to !== 'string' || to.trim() === '') {
        return res.status(400).json({ error: 'Missing or invalid "to" phone number in request body.' });
    }
    if (!text || typeof text !== 'string' || text.trim() === '') {
        return res.status(400).json({ error: 'Missing or invalid "text" message in request body.' });
    }

    try {
        console.log(`Received request to send SMS to: ${to}`);
        const result = await infobipService.sendSms(to.trim(), text.trim());
        // Send back the initial response from Infobip (acknowledgment, messageId)
        res.status(202).json({ // 202 Accepted: Request taken, processing underway
            message: 'SMS sending initiated successfully.',
            infobipResponse: result,
        });
    } catch (error) {
        console.error('Error in /send-sms endpoint:', error.message);
        // Avoid sending detailed internal errors back to the client in production
        res.status(500).json({ error: 'Failed to initiate SMS sending.' });
    }
});

// --- Webhook Endpoint to Receive Infobip Delivery Reports ---
app.post('/infobip-dlr', (req, res) => {
    console.log('--- Received Infobip Delivery Report ---');
    console.log('Timestamp:', new Date().toISOString());
    console.log('Headers:', JSON.stringify(req.headers, null, 2)); // Log headers for debugging if needed
    console.log('Body:', JSON.stringify(req.body, null, 2)); // Log the full DLR payload

    // --- Processing Logic ---
    // The actual payload structure can vary slightly based on Infobip product/settings.
    // Inspect the logged 'Body' from a real callback to confirm structure.
    // Common structure includes a 'results' array.
    if (req.body && Array.isArray(req.body.results)) {
        req.body.results.forEach(report => {
            const messageId = report.messageId;
            const status = report.status ? report.status.name : 'UNKNOWN';
            const groupName = report.status ? report.status.groupName : 'UNKNOWN';
            const description = report.status ? report.status.description : 'No description';
            const recipient = report.to;
            const errorCode = report.error ? report.error.id : null;
            const errorName = report.error ? report.error.name : null;
            const errorDescription = report.error ? report.error.description : null;

            console.log(`\n--- Processing DLR for Message ID: ${messageId} ---`);
            console.log(`  To: ${recipient}`);
            console.log(`  Status Group: ${groupName}`); // e.g., PENDING, UNDELIVERABLE, DELIVERED, EXPIRED, REJECTED
            console.log(`  Status: ${status}`);         // e.g., PENDING_ACCEPTED, DELIVERED_TO_HANDSET, UNDELIVERABLE_NOT_DELIVERED
            console.log(`  Description: ${description}`);

            if (errorCode) {
                console.error(`  Error Code: ${errorCode} (${errorName})`);
                console.error(`  Error Description: ${errorDescription}`);
            }

            // TODO: Add your business logic here:
            // 1. Find the original message record in your database using messageId.
            // 2. Update the message status based on 'groupName' or 'status'.
            // 3. If status is 'UNDELIVERABLE' or 'REJECTED', trigger alerts or retry logic.
            // 4. Log detailed information for auditing.
        });
    } else {
        console.warn('Received DLR in unexpected format:', JSON.stringify(req.body));
    }

    // ** Crucial: Respond to Infobip quickly with a 2xx status code **
    // Failing to respond or responding with an error might cause Infobip to retry sending the DLR.
    res.sendStatus(200); // Send 'OK' back to Infobip to acknowledge receipt.
});

// Start the server
app.listen(port, () => {
    console.log(`Server listening on port ${port}`);
    console.log(`Ensure INFOBIP_API_KEY and INFOBIP_BASE_URL are set.`);
    console.log(`Callback endpoint configured internally as /infobip-dlr`);
    console.log(`Ensure APP_BASE_URL is set correctly in .env for callbacks`);
    if (process.env.NODE_ENV !== 'production') {
        console.warn(`--- Development Mode ---`);
        console.warn(`Use a tool like ngrok to expose port ${port} publicly.`);
        console.warn(`Update APP_BASE_URL in .env with your ngrok URL (e.g., https://xxxxx.ngrok.io)`);
        console.warn(`Example ngrok command: ngrok http ${port}`);
    }
});
  • Why require('dotenv').config() first? This line must execute before any code that accesses process.env variables (like our infobipService.js) to ensure those variables are loaded from the .env file.
  • Why express.json() middleware? Infobip sends delivery reports as JSON payloads in the POST request body. This middleware parses that JSON and makes it available as req.body. It's also needed for our /send-sms endpoint to parse the incoming JSON request.
  • Why /send-sms endpoint? This provides a clean interface for other parts of your system (or external clients) to request an SMS without needing direct access to the Infobip service logic.
  • Why /infobip-dlr endpoint? This is the publicly accessible listener that Infobip will call. Its sole purpose is to receive, acknowledge, and process incoming delivery status updates.
  • Why res.sendStatus(200) in /infobip-dlr? You must respond to Infobip's webhook request with a success status (2xx) quickly. This acknowledges receipt. If you respond with an error (4xx, 5xx) or time out, Infobip will likely retry sending the report according to their retry schedule, potentially leading to duplicate processing if not handled carefully. Do complex processing asynchronously if needed.
  • Why parse req.body.results? Based on Infobip documentation and common webhook patterns, delivery reports often come batched within a results array in the JSON payload. Always inspect the actual payload you receive during testing to confirm the structure.

4. Integrating with Infobip: Configuration

Let's get the necessary credentials from Infobip and configure our .env file.

4.1 Obtain Infobip API Key and Base URL

  1. Log in to your Infobip Portal.
  2. Navigate to the API Keys management section. This is typically found under your user profile, account settings, or a dedicated ""Developers"" section. The exact location might change, explore the portal if needed.
    • Example Path (might vary): Click your username/icon -> Account Settings -> API Keys. Or look for Developers -> API Keys.
  3. Create a new API Key if you don't have one already. Give it a descriptive name (e.g., Node DLR App Key).
  4. Copy the API Key immediately and store it securely. You often only see the full key upon creation.
  5. Find your Base URL (API Domain). This is usually displayed prominently in the API section or on the main dashboard after logging in. It looks like xxxxxx.api.infobip.com.
    • Example Location (might vary): Often shown on the API Keys page or a general API information page.

4.2 Update .env File

Open your .env file and replace the placeholders with the actual values you obtained:

dotenv
# .env

# Infobip Credentials
INFOBIP_API_KEY=YOUR_ACTUAL_INFOBIP_API_KEY
INFOBIP_BASE_URL=YOUR_ACTUAL_INFOBIP_BASE_URL_DOMAIN # e.g., abc12.api.infobip.com

# Application Settings
PORT=3000
APP_BASE_URL=http://localhost:3000 # <-- IMPORTANT: Update this when using ngrok

4.3 Set Up ngrok for Local Development

Infobip needs to reach your /infobip-dlr endpoint over the public internet. ngrok creates a secure tunnel from a public URL to your local machine.

  1. Download and install ngrok.

  2. Authenticate ngrok if you have an account (optional but recommended for longer sessions).

  3. In your terminal (in a separate window from your running Node app), start ngrok, pointing it to the port your Express app is running on (defined by PORT in .env, default 3000).

    bash
    ngrok http 3000
  4. ngrok will display output including lines like:

    Forwarding http://<random-string>.ngrok.io -> http://localhost:3000 Forwarding https://<random-string>.ngrok.io -> http://localhost:3000
  5. Copy the https Forwarding URL (e.g., https://<random-string>.ngrok.io). This is your public URL.

  6. Update APP_BASE_URL in your .env file with this https ngrok URL.

    dotenv
    # .env (Example Update)
    
    # ... other variables ...
    APP_BASE_URL=https://b7a9-123-45-67-89.ngrok.io # Use YOUR ngrok HTTPS URL
  7. Restart your Node.js application (node index.js or using nodemon) after updating .env so it picks up the new APP_BASE_URL.

  • Why ngrok? It makes your locally running server accessible from the internet without complex network configuration, essential for receiving webhooks during development.
  • Why HTTPS ngrok URL? Always use HTTPS for security, especially when dealing with potentially sensitive callback data.

5. Error Handling and Logging

Our current code includes basic console logging and error handling. Let's refine it slightly.

  • Infobip Service (infobipService.js): The sendSms function already includes a try...catch block that logs detailed errors from axios, distinguishing between response errors, request errors, and setup errors. It re-throws a generic error to the caller.
  • API Endpoint (/send-sms in index.js): The endpoint wraps the service call in try...catch. It logs the error server-side and returns a generic 500 error to the client to avoid leaking internal details. Basic 400 Bad Request errors are returned for invalid input.
  • Callback Endpoint (/infobip-dlr in index.js): This endpoint logs the entire incoming payload. It includes a console.warn for unexpected formats and logs specific details for recognized formats. Crucially, it always responds with 200 OK to prevent Infobip retries, even if internal processing fails. Robust error handling here might involve:
    • Wrapping the processing logic in a try...catch.
    • Logging any processing errors internally (e.g., database update failure).
    • Still sending res.sendStatus(200) back to Infobip.
    • Implementing a separate mechanism (like a dead-letter queue or retry queue) to handle DLRs that failed processing.

Further Improvements (Beyond this Guide):

  • Structured Logging: Use a library like winston or pino for structured JSON logging, making logs easier to parse and analyze, especially in production.
  • Error Tracking Services: Integrate with services like Sentry or Datadog to capture, aggregate, and alert on application errors.
  • Retry Mechanisms: For sending SMS, if the initial request to Infobip fails due to network issues or temporary Infobip problems (e.g., 5xx errors), implement a retry strategy with exponential backoff using libraries like axios-retry or async-retry. Do not retry based on DLRs indicating final failure (like UNDELIVERABLE).

6. Database Schema and Data Layer (Conceptual)

While this guide doesn't implement a database, here's how you would typically integrate one:

6.1 Conceptual Schema

You'd likely need a table to store information about outgoing messages:

sql
CREATE TABLE outgoing_messages (
    id SERIAL PRIMARY KEY, -- Or UUID
    infobip_message_id VARCHAR(255) UNIQUE, -- Store the ID from Infobip's initial response
    recipient_number VARCHAR(20) NOT NULL,
    message_text TEXT NOT NULL,
    status VARCHAR(50) DEFAULT 'PENDING_INFOBIP', -- Initial status before Infobip confirms acceptance
    infobip_status_group VARCHAR(50), -- e.g., PENDING, DELIVERED, UNDELIVERABLE
    infobip_status_name VARCHAR(100), -- Specific status e.g., PENDING_ACCEPTED
    infobip_status_description TEXT,
    error_code INTEGER,
    error_name VARCHAR(100),
    error_description TEXT,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    last_updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    callback_received_at TIMESTAMPTZ -- Timestamp when the DLR was processed
);

-- Index for faster lookups when processing callbacks
CREATE INDEX idx_infobip_message_id ON outgoing_messages(infobip_message_id);

6.2 Data Layer Integration

  1. On Sending (/send-sms):
    • Before calling infobipService.sendSms, insert a new record into outgoing_messages with status PENDING_INFOBIP.
    • After a successful call to infobipService.sendSms, update the record using the messageId from the Infobip response, setting the infobip_message_id field and potentially updating status to PENDING_ACCEPTED (or similar based on the initial response).
  2. On Receiving Callback (/infobip-dlr):
    • Inside the loop processing req.body.results:
      • Extract the messageId from the report.
      • Find the corresponding record in outgoing_messages using infobip_message_id.
      • If found, update the infobip_status_group, infobip_status_name, infobip_status_description, error fields, callback_received_at, and last_updated_at based on the DLR content.
      • Handle cases where the messageId is not found (log an error/warning).

Tools: Use an ORM like Sequelize (for SQL databases) or Mongoose (for MongoDB) to manage database interactions more easily. Use migration tools like sequelize-cli or knex migrations to manage schema changes.


7. Security Features

Security is paramount, especially when handling API keys and potentially sensitive message data.

  • Environment Variables: We are already using .env to keep INFOBIP_API_KEY out of the code and .gitignore to prevent committing it. This is crucial.
  • Input Validation (/send-sms): Basic validation is implemented to check for the presence and type of to and text. Enhance this based on expected phone number formats or message length constraints. Use libraries like joi or express-validator for more robust validation schemas.
  • Webhook Security (/infobip-dlr):
    • HTTPS: Using ngrok with HTTPS and deploying to a server with HTTPS configured is essential to encrypt data in transit.
    • Secret Validation (Optional/Advanced): Check if Infobip supports sending a secret signature in a header (e.g., X-Infobip-Signature) along with the webhook. If so, you would:
      1. Configure a secret string in your Infobip webhook settings (if available).
      2. Store the same secret in your application's environment variables.
      3. On receiving a webhook, calculate the expected signature based on the request body and your secret (using HMAC-SHA1 or similar, as specified by Infobip).
      4. Compare the calculated signature with the one provided in the header. Reject requests if they don't match. Note: Simple signature verification doesn't seem to be a standard, easily configurable feature for basic Infobip SMS DLRs via notifyUrl as of common knowledge, but check their latest documentation.
    • IP Whitelisting (If feasible): If Infobip publishes a list of IP addresses from which they send webhooks, you could configure your firewall or application middleware to only accept requests to /infobip-dlr from those specific IPs. This can be difficult to maintain if IPs change.
  • Rate Limiting: Protect your /send-sms endpoint from abuse by implementing rate limiting using middleware like express-rate-limit.
  • Helmet: Use the helmet middleware for Express to set various security-related HTTP headers (e.g., Content-Security-Policy, X-Content-Type-Options). npm install helmet and app.use(helmet());.

8. Handling Special Cases

  • Phone Number Formatting: The Infobip API generally expects numbers in international format (e.g., 447123456789 for the UK, 14155552671 for the US). Ensure your input validation or normalization logic enforces this format before sending to the API.
  • Character Encoding & Concatenation: Standard SMS messages have length limits (160 GSM-7 characters, 70 UCS-2 characters for non-standard alphabets). Longer messages are often automatically split (concatenated) by carriers and Infobip. Be aware that concatenated messages consume more credits. Infobip's API handles much of this, but be mindful of text length if cost is critical.
  • DLR Latency: Delivery reports are not always instantaneous. There can be delays depending on carrier networks. Your application should handle the possibility that a DLR arrives seconds, minutes, or sometimes even longer after the message was sent. The PENDING status indicates the message is still in transit.
  • Duplicate DLRs: While Infobip aims to deliver reports reliably, network issues or slow responses from your endpoint (timeouts before you send 200 OK) could lead to Infobip retrying the webhook delivery. Design your processing logic (/infobip-dlr) to be idempotent — meaning processing the same DLR multiple times has no adverse effects. Using the infobip_message_id as a unique key in your database helps achieve this (e.g., update status only if the new status is different or more final than the existing one).
  • Time Zones: Use TIMESTAMPTZ (Timestamp with Time Zone) in your database schemas (created_at, last_updated_at, callback_received_at) to store timestamps unambiguously. Log timestamps using toISOString() for clarity.

9. Performance Optimizations (Conceptual)

For high-volume applications:

  • Asynchronous Processing: If DLR processing involves complex logic or database updates, consider moving it out of the main /infobip-dlr request handler. Immediately send the 200 OK response, and then push the DLR payload onto a message queue (like RabbitMQ, Redis Streams, or AWS SQS) for background workers to process. This prevents holding up Infobip's request and avoids timeouts.
  • Database Indexing: As shown in the conceptual schema, index columns frequently used in WHERE clauses (especially infobip_message_id).
  • Connection Pooling: Ensure your database client (e.g., Sequelize, node-postgres) uses connection pooling to reuse database connections efficiently.
  • Caching: If you frequently query message statuses, consider caching recent statuses (e.g., in Redis) to reduce database load, especially for non-final states. Invalidate the cache when a final DLR (Delivered, Undeliverable, Rejected) is received.
  • Load Testing: Use tools like k6, artillery, or JMeter to simulate traffic to your /send-sms endpoint and (if possible) your /infobip-dlr endpoint to identify bottlenecks under load.

10. Monitoring, Observability, and Analytics (Conceptual)

  • Health Checks: Keep the simple app.get('/') endpoint or create a dedicated /health endpoint that checks basic connectivity (e.g., can reach the database). Monitoring services can ping this endpoint.

Frequently Asked Questions

How to handle Infobip SMS delivery reports?

Handle Infobip SMS delivery reports by setting up a webhook endpoint in your Node.js/Express application. This endpoint, specified by the `notifyUrl` parameter in your Infobip API request, receives real-time delivery updates in JSON format. Your application should then process this data, updating internal systems and triggering actions based on the delivery status (e.g., delivered, failed).

What is a notify URL for Infobip SMS?

The notify URL is a crucial parameter in the Infobip SMS API. It's the publicly accessible URL of your application's webhook endpoint, where Infobip sends real-time delivery reports (DLRs). This URL must be reachable by Infobip for your application to receive status updates.

Why does Infobip need a callback URL?

Infobip needs a callback URL (the `notifyUrl`) to send your application asynchronous updates about the delivery status of your SMS messages. This enables your system to react to successful deliveries, failures, or other status changes without constantly polling the Infobip API.

When should I use ngrok with Infobip?

Use ngrok during local development with Infobip to create a publicly accessible URL for your webhook endpoint. Since Infobip needs to reach your local server for DLR callbacks, ngrok provides a tunnel from a public URL to your localhost, making testing and development easier.

Can I retry failed Infobip SMS messages?

You can implement retry mechanisms for failed Infobip SMS messages, but only for initial failures from the Infobip API (like network or 5xx errors), not for final statuses like 'UNDELIVERABLE'. Use exponential backoff to avoid overloading the system, but don't retry messages marked as permanently undeliverable by the carrier or Infobip.

What is the Infobip SMS notify content type?

The Infobip SMS notify content type is 'application/json'. This parameter specifies that delivery reports sent to your webhook endpoint will be in JSON format, making parsing and processing the data within your application more structured and efficient.

How to set up Infobip DLR with Node.js?

Set up Infobip DLR with Node.js by creating an Express.js server with two key endpoints: `/send-sms` to initiate message sending and `/infobip-dlr` to receive callbacks. The `infobipService.js` file contains the logic for sending SMS and handling API interactions using Axios.

What is the purpose of express.json() middleware?

The `express.json()` middleware in Express.js is essential for handling Infobip DLR callbacks. It parses incoming JSON payloads in POST requests, making the data accessible via `req.body`. This allows you to easily process delivery reports and update your application's internal state.

Why use dotenv with Infobip API integration?

Dotenv helps manage environment variables securely when integrating with the Infobip API. It loads credentials like your API key from a `.env` file, keeping sensitive information out of your source code and version control.

How to send SMS with Infobip API and Node.js?

To send SMS with the Infobip API and Node.js, use the `sendSms` function from `infobipService.js`. This function requires the recipient's phone number in international format and the message text. It handles constructing the API request, including headers, and making the request using Axios.

How to test Infobip webhooks locally?

To test Infobip webhooks locally, expose your development server using ngrok and update the `notifyUrl` in your Infobip API requests to point to the generated ngrok HTTPS URL. This allows Infobip to reach your local server with DLR callbacks during development.

How to structure a database for Infobip DLRs?

Structure a database for Infobip DLRs with a table for outgoing messages. Include fields for message details, Infobip message ID, recipient number, status, error codes, and timestamps. Index the `infobip_message_id` column for efficient lookup during callback processing.

What are common status groups in Infobip DLR?

Common status groups in Infobip DLRs include 'PENDING', 'UNDELIVERABLE', 'DELIVERED', 'EXPIRED', and 'REJECTED'. These groups provide a broad categorization of the message delivery status. More detailed status names within each group provide further information.

What are best practices for logging Infobip DLR?

Best practices for logging Infobip DLRs include logging the full incoming request body to understand its structure and content. Log specific fields from each report, including messageId, status, recipient, and errors. Use structured logging and external logging services for improved analysis and alerting.

How to implement security for Infobip webhook endpoint?

Implement security for your Infobip webhook endpoint by using HTTPS for secure communication. Optional security measures could include signature validation (if supported by Infobip) to verify the sender, and IP whitelisting if feasible. Basic input validation is also important for security and preventing unexpected input.