code examples

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

How to Send Bulk SMS with Node.js, Express, and Infobip API - Complete Guide

Learn how to build a production-ready bulk SMS broadcast system using Node.js, Express.js, and Infobip API. Step-by-step tutorial with code examples, error handling, and database integration.

How to Send Bulk SMS with Node.js, Express, and Infobip API

Learn how to build a production-ready bulk SMS broadcast system using Node.js and Express.js with the Infobip API. This comprehensive tutorial covers everything from initial project setup to deploying a scalable SMS messaging service with error handling, database integration, and security best practices.

This guide provides a step-by-step walkthrough for implementing bulk SMS messaging functionality. By the end, you'll have a fully functional Express API endpoint capable of sending SMS messages to multiple recipients simultaneously using Infobip's Node.js SDK.

What You'll Build: Node.js Bulk SMS Service Overview

This guide provides a comprehensive, step-by-step walkthrough for building a production-ready Node.js application using the Express framework to send bulk broadcast SMS messages via the Infobip API. We will cover project setup, core implementation, API design, error handling, security, deployment, and verification.

By the end of this guide, you will have a functional Express API endpoint capable of receiving a list of phone numbers and a message, then efficiently dispatching that message to all recipients using Infobip's robust communication platform.

Project Overview and Goals

What We're Building:

We are creating a backend service built with Node.js and Express. This service will expose a single API endpoint (/api/broadcast) that accepts a POST request containing:

  1. A list of recipient phone numbers.
  2. The text message content to be sent.

The service will then use the Infobip Node.js SDK to send the specified message to all phone numbers in the list via SMS.

Problem Solved:

This addresses the common need for applications to send notifications, alerts, marketing messages, or other communications to multiple users simultaneously via SMS, leveraging a reliable third-party provider like Infobip for delivery.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications. Chosen for its asynchronous, event-driven nature, suitable for I/O-heavy tasks like API interactions.
  • Express.js: A minimal and flexible Node.js web application framework. Chosen for its simplicity, widespread adoption, and ease of building RESTful APIs.
  • Infobip API & Node.js SDK (@infobip-api/sdk): Infobip provides communication APIs (SMS, WhatsApp, etc.). We use their official Node.js SDK for cleaner integration compared to manual HTTP requests.
  • dotenv: A module to load environment variables from a .env file into process.env, essential for managing sensitive credentials securely during local development.
  • (Optional) Prisma & SQLite: For demonstrating database interaction (logging results), though not strictly required for the core broadcast function.
  • (Optional) express-rate-limit: Middleware for basic rate limiting to protect the API endpoint.

System Architecture:

mermaid
graph LR
    A[Client (e.g., Web/Mobile App)] --> B{Node.js Express API<br>(POST /api/broadcast)<br>- Validate Request<br>- Use Infobip SDK<br>- (Optional) Log to DB<br>- Return Response};
    B --> C[Infobip API<br>(SMS Sending)];
    B -.-> D[(Optional) Database<br>(e.g., SQLite/Postgres)];

    style D fill:#eee,stroke:#ccc,stroke-dasharray: 5 5

Prerequisites:

  • Node.js and npm/yarn: Node.js v22 (Jod, Active LTS) or v20 (Iron, Maintenance LTS) recommended for production use. The @infobip-api/sdk requires Node.js 14 minimum. Check your version with node -v.
  • Infobip Account: A registered account on Infobip.
  • Infobip API Credentials: API Key and Base URL from your Infobip account dashboard. Note: Trial accounts can typically only send SMS to the registered phone number.
  • Basic Terminal/Command Line Knowledge: Familiarity with navigating directories and running commands.
  • Text Editor or IDE: Like VS Code, WebStorm, etc.

1. Node.js Project Setup and Dependencies

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

  1. Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.

    bash
    mkdir infobip-bulk-sms-express
    cd infobip-bulk-sms-express
  2. Initialize Node.js Project: This creates a package.json file to manage project details and dependencies.

    bash
    npm init -y
    # or
    yarn init -y

    (The -y flag accepts default settings; you can omit it to customize project details).

  3. Install Dependencies: We need Express for the web server, the Infobip SDK for SMS sending, and dotenv for environment variable management.

    bash
    npm install express @infobip-api/sdk dotenv
    # or
    yarn add express @infobip-api/sdk dotenv
  4. Install Development Dependencies (Optional but Recommended): nodemon automatically restarts the server on file changes, speeding up development.

    bash
    npm install --save-dev nodemon
    # or
    yarn add --dev nodemon
  5. Create Project Structure: Organize the code logically.

    bash
    mkdir src
    mkdir src/routes
    mkdir src/controllers
    mkdir src/services
    mkdir src/config
    # If adding DB:
    # mkdir src/db
    # mkdir prisma # (if using Prisma)
    touch src/server.js
    touch src/routes/broadcastRoutes.js
    touch src/controllers/broadcastController.js
    touch src/services/infobipService.js
    touch src/config/infobipClient.js
    touch .env
    touch .gitignore
  6. Configure .gitignore: Prevent committing sensitive files and unnecessary directories.

    text
    # .gitignore
    
    # Dependencies
    node_modules/
    
    # Environment variables (IMPORTANT!)
    .env*
    
    # Logs
    logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    
    # Build output
    dist/
    build/
    
    # OS generated files
    .DS_Store
    Thumbs.db
    
    # Database files (example for SQLite)
    *.db
    *.db-journal
  7. Set up .env File: Store sensitive credentials here for local development only. Never commit this file to version control. See Deployment section for production handling.

    dotenv
    # .env
    
    # Infobip Credentials (Replace with your actual values from Infobip Dashboard)
    INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY_HERE
    INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL_HERE
    
    # Application Settings
    PORT=3000
    
    # Database URL (if using Prisma with SQLite example)
    # DATABASE_URL="file:./dev.db"
    • Replace placeholders with your actual Infobip API Key and Base URL found in your Infobip dashboard. The Base URL looks something like xxxxx.api.infobip.com.
  8. Add start and dev Scripts to package.json: These scripts make running the application easier. Open package.json and ensure the scripts section looks like this:

    json
    // package.json (partial)
    "scripts": {
      "start": "node src/server.js",
      "dev": "nodemon src/server.js",
      "test": "echo \"Error: no test specified\" && exit 1"
    },

2. Implementing Core SMS Functionality with Infobip SDK

We'll encapsulate the Infobip interaction logic within a dedicated service module.

  1. Configure Infobip Client: Create a reusable Infobip client instance.

    javascript
    // src/config/infobipClient.js
    import { Infobip, AuthType } from '@infobip-api/sdk';
    import dotenv from 'dotenv';
    
    // Load environment variables (primarily for local development)
    dotenv.config();
    
    const apiKey = process.env.INFOBIP_API_KEY;
    const baseUrl = process.env.INFOBIP_BASE_URL;
    
    if (!apiKey || !baseUrl) {
      console.error('Error: INFOBIP_API_KEY and INFOBIP_BASE_URL must be set in environment variables.');
      // In production, these should be injected, not just from .env
      process.exit(1); // Exit if credentials are missing
    }
    
    const infobipClient = new Infobip({
      baseUrl: baseUrl,
      apiKey: apiKey,
      authType: AuthType.ApiKey, // Specify API Key authentication
    });
    
    export default infobipClient;
    • Why: We centralize the client creation. dotenv.config() loads variables from .env (useful locally). We add checks to ensure critical variables are present. AuthType.ApiKey explicitly tells the SDK how to authenticate.
  2. Create the Infobip Service: This service will contain the function to send bulk SMS messages.

    javascript
    // src/services/infobipService.js
    import infobipClient from '../config/infobipClient.js';
    
    /**
     * Sends a single SMS message to multiple recipients using the Infobip API.
     * @param {string[]} recipients - An array of phone numbers in international format (e.g., '447123456789').
     * @param {string} messageText - The text content of the SMS message.
     * @param {string} [sender='InfoSMS'] - The sender ID (alphanumeric). Check Infobip regulations for your country.
     * @returns {Promise<object>} - The response object from the Infobip API containing bulkId and message statuses.
     * @throws {Error} - Throws an error if the API call fails.
     */
    const sendBulkSms = async (recipients, messageText, sender = 'InfoSMS') => {
      if (!recipients || recipients.length === 0) {
        throw new Error('Recipients array cannot be empty.');
      }
      if (!messageText) {
        throw new Error('Message text cannot be empty.');
      }
    
      // Map recipients array to the format required by Infobip API
      const destinations = recipients.map(number => ({ to: number }));
    
      try {
        console.log(`Attempting to send SMS via Infobip to ${recipients.length} recipients...`);
    
        const response = await infobipClient.channels.sms.send({
          messages: [
            {
              from: sender, // Sender ID (e.g., your brand name)
              destinations: destinations,
              text: messageText,
            },
            // Note: You could add more message objects here to send *different* messages
            // in the same API request if needed, but for a broadcast, one message
            // object with multiple destinations is typical.
          ],
          // You can add other options like 'bulkId', 'notifyUrl' for delivery reports, etc.
          // bulkId: 'YOUR_CUSTOM_BULK_ID', // Optional: Assign a custom ID to the batch
          // notifyUrl: 'YOUR_WEBHOOK_URL', // Optional: URL for delivery reports
        });
    
        // Log the API response data (consider logging only essential fields in production)
        console.log('Infobip API Response Data:', JSON.stringify(response.data, null, 2));
        // INFO: In production, consider logging only response.data.bulkId and perhaps summaries
        // of message statuses to reduce log volume and avoid exposing potentially sensitive details
        // depending on the full API response structure. Essential fields might include:
        // console.log(`Infobip request accepted. Bulk ID: ${response.data.bulkId}, Messages: ${response.data.messages?.length}`);
    
        // The response.data contains details like bulkId and individual message statuses
        // Example structure:
        // {
        //   ""bulkId"": ""some-unique-bulk-id"",
        //   ""messages"": [
        //     { ""to"": ""447123456789"", ""status"": { ""groupId"": 1, ""groupName"": ""PENDING"", ... }, ""messageId"": ""..."" },
        //     { ""to"": ""15551234568"", ""status"": { ""groupId"": 1, ""groupName"": ""PENDING"", ... }, ""messageId"": ""..."" }
        //   ]
        // }
        return response.data; // Return the relevant data part of the response
    
      } catch (error) {
        // Log detailed error information from Infobip if available
        console.error('Error sending SMS via Infobip:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
        // Rethrow a more specific error or handle it as needed
        const errorMessage = error.response?.data?.requestError?.serviceException?.text || error.message || 'Unknown Infobip API error';
        throw new Error(`Infobip API error: ${errorMessage}`);
      }
    };
    
    export { sendBulkSms };
    • Why: This encapsulates the logic. We validate inputs (recipients, messageText). We map the simple array of numbers to the destinations array format expected by the SDK ([{ to: 'number1' }, { to: 'number2' }]). The core logic is the infobipClient.channels.sms.send call. We structure the payload with one messages object containing multiple destinations for efficient broadcast. Error handling logs detailed information from the API response if available. A comment advises on more concise production logging.

3. Building the Express API Endpoint for Bulk SMS

Now, let's create the Express endpoint that will use our infobipService.

  1. Create the Controller: Handles incoming requests, validates input, calls the service, and sends the response.

    javascript
    // src/controllers/broadcastController.js
    import { sendBulkSms } from '../services/infobipService.js';
    
    const handleBroadcastRequest = async (req, res) => {
      const { recipients, message, sender } = req.body; // recipients: string[], message: string, sender?: string
    
      // --- Basic Input Validation ---
      if (!recipients || !Array.isArray(recipients) || recipients.length === 0) {
        return res.status(400).json({
          success: false,
          message: 'Invalid input: "recipients" must be a non-empty array of phone numbers.',
        });
      }
      if (!message || typeof message !== 'string' || message.trim() === '') {
        return res.status(400).json({
          success: false,
          message: 'Invalid input: "message" must be a non-empty string.',
        });
      }
      // Basic phone number format check (very lenient - doesn't validate country codes etc.)
      // Consider using a library like 'libphonenumber-js' for robust validation (see Section 7)
      const invalidNumbers = recipients.filter(num => typeof num !== 'string' || !/^\+?\d{7,15}$/.test(num.replace(/\s/g, '')));
      if (invalidNumbers.length > 0) {
         return res.status(400).json({
           success: false,
           message: `Invalid phone number format detected for: ${invalidNumbers.join(', ')}. Use international format (e.g., +15551234567).`,
           invalidRecipients: invalidNumbers // Optionally list invalid numbers
         });
      }
      // --- End Validation ---
    
      try {
        console.log(`Processing broadcast request for ${recipients.length} recipients, sender: ${sender || 'default (InfoSMS)'}`);
        // Call the service function to interact with Infobip
        const result = await sendBulkSms(recipients, message, sender); // sender is optional
    
        // Successfully submitted to Infobip (doesn't guarantee delivery yet)
        res.status(200).json({
          success: true,
          message: `SMS broadcast request accepted by Infobip for ${recipients.length} recipients.`,
          infobipResponse: result, // Include bulkId and individual message statuses from Infobip
        });
    
      } catch (error) {
        console.error('Broadcast request processing failed:', error.message);
        // Determine appropriate status code based on error type if possible
        // For now, use 500 for any service-level or unexpected error
        res.status(500).json({
          success: false,
          message: 'Failed to process broadcast SMS request.',
          error: error.message, // Provide the specific error message from the service/Infobip
        });
      }
    };
    
    export { handleBroadcastRequest };
    • Why: Separates request handling logic from the core service logic. Implements essential input validation to prevent invalid data from reaching the service/API. Provides clear success and error responses in JSON format. Passes control to the infobipService.
  2. Create the Route: Defines the API endpoint path and connects it to the controller.

    javascript
    // src/routes/broadcastRoutes.js
    import express from 'express';
    import { handleBroadcastRequest } from '../controllers/broadcastController.js';
    
    const router = express.Router();
    
    // Define the POST endpoint for sending bulk SMS
    // POST /api/broadcast
    router.post('/broadcast', handleBroadcastRequest);
    
    export default router;
    • Why: Maps the HTTP method (POST) and path (/api/broadcast) to the specific controller function. Keeps routing definitions clean and separate.
  3. Set up the Main Server File: Integrates all parts: loads environment variables, configures Express middleware, mounts the router, and starts the server.

    javascript
    // src/server.js
    import express from 'express';
    import dotenv from 'dotenv';
    import broadcastRoutes from './routes/broadcastRoutes.js';
    // Optional: import rateLimit from 'express-rate-limit';
    // Optional: import helmet from 'helmet';
    
    // Load environment variables from .env file (primarily for local development)
    dotenv.config();
    
    const app = express();
    const PORT = process.env.PORT || 3000; // Default to 3000 if PORT not set
    
    // --- Security Middleware (Recommended) ---
    // app.use(helmet()); // Helps set various security-related HTTP headers
    
    // --- Core Middleware ---
    // 1. Enable JSON body parsing for POST/PUT/PATCH requests
    app.use(express.json({ limit: '1mb' })); // Adjust limit as needed
    
    // 2. Optional: Basic Rate Limiting (adjust limits as needed)
    /*
    const apiLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
      legacyHeaders: false, // Disable the `X-RateLimit-*` headers
      message: 'Too many requests from this IP, please try again after 15 minutes',
    });
    app.use('/api', apiLimiter); // Apply rate limiting specifically to API routes
    */
    
    // 3. Basic Logging Middleware (Example - Consider using a dedicated logger like Winston/Pino)
    app.use((req, res, next) => {
      console.log(`${new Date().toISOString()} - ${req.method} ${req.originalUrl} [${req.ip}]`);
      next(); // Pass control to the next middleware/route handler
    });
    // --- End Middleware ---
    
    
    // --- Routes ---
    // Mount the broadcast routes under the /api path
    app.use('/api', broadcastRoutes);
    
    // Basic health check endpoint
    app.get('/health', (req, res) => {
      res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
    });
    // --- End Routes ---
    
    
    // --- Global Error Handler (Catch-all) ---
    // Catches errors passed via next(err) or thrown in async handlers if not caught locally
    app.use((err, req, res, next) => {
      console.error('Unhandled Error:', err.stack || err);
      res.status(err.status || 500).json({ // Use error status if available, otherwise 500
        success: false,
        message: err.message || 'An unexpected internal server error occurred.',
        // Avoid leaking stack traces in production environment
        error: process.env.NODE_ENV === 'development' ? err.stack : undefined,
      });
    });
    // --- End Global Error Handler ---
    
    
    // --- Start Server ---
    app.listen(PORT, () => {
      console.log(`Server running on http://localhost:${PORT}`);
      console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
      // Verify Infobip URL is loaded (useful for debugging setup)
      console.log(`Configured Infobip Base URL: ${process.env.INFOBIP_BASE_URL}`);
      // CRITICAL: Never log the API Key here or anywhere else!
    });
    • Why: This is the application's entry point. express.json() is crucial for parsing the request body. Rate limiting and Helmet (commented out) are vital security measures. Basic logging helps track requests. Mounting the routes under /api namespaces the API. A health check is standard practice. A global error handler provides a fallback for unexpected issues.

4. Configuring Infobip API Integration

This section reinforces the Infobip-specific configuration steps.

  1. Obtaining Credentials:

    • Log in to your Infobip Customer Portal.
    • Navigate to the Homepage or look for sections like API Keys Management or Developer Tools (UI may vary). You might find your Base URL on the homepage under API access details.
    • Generate or find your API Key. Treat this like a password – keep it secret and secure.
    • Note your specific Base URL (e.g., xyz123.api.infobip.com). This is unique to your account or region.
  2. Secure Storage (.env for Local Dev):

    • As done in Step 1.7, place your INFOBIP_API_KEY and INFOBIP_BASE_URL in the .env file at the project root for local development only.
    • Crucially: Ensure .env (and variants like .env.local) is listed in your .gitignore file to prevent accidental commits. Do not deploy .env files to production. Use platform environment variables instead (see Section 12).
  3. Loading Credentials:

    • The dotenv.config() call at the top of src/config/infobipClient.js (and potentially src/server.js if needed early) loads these variables into process.env when running locally. In deployed environments, process.env will be populated by the platform.
  4. SDK Initialization (src/config/infobipClient.js):

    • The Infobip class is instantiated with the baseUrl, apiKey, and authType: AuthType.ApiKey. This configures the shared client instance used by infobipService.js.
  5. Environment Variables Explained:

    • INFOBIP_API_KEY: (String) Your secret key for authenticating requests with the Infobip API. Format: Typically a long alphanumeric string. Obtain from Infobip dashboard. Treat as highly sensitive.
    • INFOBIP_BASE_URL: (String) The specific domain assigned to your Infobip account for API requests. Format: subdomain.api.infobip.com. Obtain from Infobip dashboard.
    • PORT: (Number) The network port your Express server will listen on. Format: Integer (e.g., 3000, 8080). Set according to your environment needs or platform requirements.
    • NODE_ENV: (String) Typically set to production in deployed environments, development locally. Used by Express and other libraries to optimize behavior (e.g., caching, error details).

5. Error Handling and Logging Best Practices

Production systems need robust error handling and logging.

  1. Consistent Error Strategy:

    • Validation Errors (Controller): Return 400 Bad Request with clear JSON messages indicating the specific input issue (as shown in broadcastController.js).
    • Service Errors (Infobip API): The infobipService.js catches errors from the SDK. It logs the detailed error (often including Infobip's specific error message) and throws a new, more general error. The controller catches this and returns 500 Internal Server Error (or potentially other 4xx/5xx codes if more specific error types were identified and mapped).
    • Unhandled Errors (Server): The global error handler in server.js catches anything missed (e.g., errors in middleware, unhandled promise rejections if not using modern Node/Express error handling), logs it, and returns a generic 500 error message to avoid leaking implementation details.
  2. Logging:

    • Basic Console Logging: We use console.log for request tracing and info, and console.error for errors. Suitable for development, but limited in production.
    • Structured Logging (Recommended for Production): Use libraries like Winston or Pino. They provide:
      • Log Levels: (e.g., error, warn, info, debug) to control verbosity and filter logs.
      • JSON Formatting: Makes logs machine-readable for parsing by log aggregation tools (like ELK stack, Datadog, Splunk, Loki).
      • Transports: Output logs to files, standard output/error, databases, or external logging services.
      • Context: Easily add request IDs, user info (if applicable), etc., to logs for better tracing.
    • Example (Conceptual with Pino):
      javascript
      // Example: Replace console.log/error with a logger instance
      import pino from 'pino';
      const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
      // In middleware/controllers:
      logger.info({ requestId: req.id }, 'Broadcast request received...');
      logger.error({ err: error, requestId: req.id }, 'Infobip API error');
  3. Retry Mechanisms:

    • Infobip Internal: Infobip likely handles some transient network issues internally when submitting SMS.
    • Application-Level (Use with Caution): For critical broadcasts, you might implement retries in your service for specific, potentially temporary Infobip errors (e.g., 429 Too Many Requests, 5xx server errors).
    • Strategy: Use libraries like async-retry or p-retry. Implement exponential backoff (wait progressively longer between attempts: 1s, 2s, 4s...) with jitter (randomness) to avoid thundering herd issues. Limit the number of retries.
    • Caveat: Retrying API calls that initiate actions (like sending SMS) requires careful consideration of idempotency. Infobip's API might support idempotency keys, or you need to ensure your logic doesn't cause duplicate sends if a request succeeds but the response is lost. For simple broadcast submission, relying on the initial 2xx acceptance and monitoring via Delivery Reports (Section 10) might be safer and simpler than complex client-side retry logic.
  4. Testing Error Scenarios:

    • Send requests with missing/invalid recipients or message.
    • Send requests with incorrectly formatted phone numbers.
    • Temporarily use an invalid INFOBIP_API_KEY in environment variables to trigger authentication errors.
    • (If possible) Send to a known blocklisted or invalid number format that Infobip might reject immediately.
    • Simulate network errors by temporarily blocking access to the Infobip Base URL.
    • Use tools like Postman or curl (see Section 13) to manually trigger these conditions.

6. Database Integration with Prisma ORM (Optional)

While not essential for the core broadcast function, logging results to a database provides auditability and tracking. We'll use Prisma ORM with SQLite for this example.

  1. Install Prisma:

    bash
    npm install --save-dev prisma
    npm install @prisma/client
    # or
    yarn add --dev prisma
    yarn add @prisma/client
  2. Initialize Prisma:

    bash
    npx prisma init --datasource-provider sqlite
    • Creates prisma/schema.prisma and updates .env with DATABASE_URL=""file:./dev.db"". Ensure dev.db is in .gitignore.
  3. Define Schema (prisma/schema.prisma): Model the broadcast jobs and their individual message results.

    prisma
    // prisma/schema.prisma
    
    generator client {
      provider = ""prisma-client-js""
    }
    
    datasource db {
      provider = ""sqlite"" // Using SQLite for simplicity
      url      = env(""DATABASE_URL"") // Loads from .env
    }
    
    // Model for a high-level broadcast job request
    model BroadcastJob {
      id             String    @id @default(cuid()) // Unique job ID (e.g., CUID)
      createdAt      DateTime  @default(now())
      updatedAt      DateTime  @updatedAt
      message        String    // The message text sent
      sender         String?   // The sender ID used (e.g., 'InfoSMS', phone number)
      status         String    // Overall status: PENDING, ACCEPTED, FAILED_SUBMISSION
      infobipBulkId  String?   @unique // The Bulk ID from Infobip, if accepted
      recipientCount Int       // How many recipients were targeted initially
      errorMessage   String?   // Error message if the job failed submission to Infobip
    
      // Relation to individual message results (one-to-many)
      // Tracks the status of each message within the job
      results        MessageResult[]
    
      @@index([createdAt])
      @@index([status])
    }
    
    // Model for individual message results within a job
    model MessageResult {
      id              String    @id @default(cuid())
      jobId           String    // Foreign key linking back to BroadcastJob
      job             BroadcastJob @relation(fields: [jobId], references: [id], onDelete: Cascade) // Link back to the job
    
      recipient       String    // The recipient phone number
      submittedAt     DateTime  @default(now())
      infobipMessageId String?   @unique // Infobip message ID for this specific message
      initialStatus   String    // Initial status from Infobip (e.g., ""PENDING_ACCEPTED"", ""REJECTED_DESTINATION"")
      initialStatusGroup String? // e.g., ""PENDING"", ""REJECTED"", ""DELIVERED"" (group name)
      initialErrorCode Int?      // Potential error code from Infobip status object
    
      // Fields to potentially update via Delivery Reports webhook
      finalStatus     String?   // Final delivery status (e.g., ""DELIVERED_TO_HANDSET"", ""FAILED_UNDELIVERABLE"")
      finalizedAt     DateTime? // Timestamp when final status was received
    
      @@index([jobId])
      @@index([recipient])
      @@index([infobipMessageId])
      @@index([finalStatus])
    }
    • ERD: BroadcastJob (1) -> (*) MessageResult. Added indexes for common query fields. Added onDelete: Cascade so deleting a job removes its results.
  4. Apply Migrations: Create the database file (dev.db) and apply the schema changes.

    bash
    npx prisma migrate dev --name init-broadcast-schema
    • Generates SQL migration files in prisma/migrations and updates the database.
  5. Create Prisma Client Instance: A reusable client instance.

    javascript
    // src/db/prismaClient.js (Create this file)
    import { PrismaClient } from '@prisma/client';
    
    const prisma = new PrismaClient({
      // Optional: Configure logging for development
      // log: ['query', 'info', 'warn', 'error'],
    });
    
    export default prisma;
  6. Update Controller to Log Data: Modify handleBroadcastRequest to interact with the database.

    javascript
    // src/controllers/broadcastController.js (Modified Snippet)
    import { sendBulkSms } from '../services/infobipService.js';
    import prisma from '../db/prismaClient.js'; // Import prisma client
    
    const handleBroadcastRequest = async (req, res) => {
      const { recipients, message, sender } = req.body;
    
      // --- Input Validation (Keep as before) ---
      if (!recipients || !Array.isArray(recipients) || recipients.length === 0 /* ... etc */) {
        // ... return 400 error
      }
      // ... other validations ...
    
      let jobRecord; // Variable to hold the created job database record
      try {
        // --- 1. Create Initial Job Record in DB ---
        jobRecord = await prisma.broadcastJob.create({
          data: {
            message: message,
            sender: sender || 'InfoSMS', // Use default sender if not provided
            status: 'PENDING', // Initial status before calling Infobip
            recipientCount: recipients.length,
          },
        });
        console.log(`Created initial BroadcastJob record ID: ${jobRecord.id}`);
    
        // --- 2. Call Infobip Service ---
        console.log(`Processing broadcast request JobID ${jobRecord.id} for ${recipients.length} recipients...`);
        const infobipResult = await sendBulkSms(recipients, message, sender);
    
        // --- 3. Update Job Record with Infobip Response (Success) ---
        await prisma.broadcastJob.update({
          where: { id: jobRecord.id },
          data: {
            status: 'ACCEPTED', // Update status: accepted by Infobip
            infobipBulkId: infobipResult.bulkId,
            // Create related MessageResult records based on Infobip's response
            results: {
              create: infobipResult.messages.map(msg => ({
                recipient: msg.to,
                infobipMessageId: msg.messageId,
                initialStatus: msg.status.name,       // e.g., PENDING_ACCEPTED
                initialStatusGroup: msg.status.groupName, // e.g., PENDING
                initialErrorCode: msg.status.id,      // Numeric status/error code
              })),
            },
            updatedAt: new Date(), // Explicitly set update timestamp
          },
        });
        console.log(`Successfully submitted JobID ${jobRecord.id} to Infobip. Bulk ID: ${infobipResult.bulkId}`);
    
        // --- 4. Send Success Response to Client ---
        res.status(200).json({
          success: true,
          message: `SMS broadcast request accepted by Infobip for ${recipients.length} recipients.`,
          jobId: jobRecord.id, // Include our internal job ID
          infobipResponse: infobipResult,
        });
    
      } catch (error) {
        console.error(`Broadcast request processing failed for JobID ${jobRecord?.id || 'N/A'}:`, error.message);
    
        // --- 5. Update Job Record on Failure (if job was created) ---
        if (jobRecord) {
          try {
            await prisma.broadcastJob.update({
              where: { id: jobRecord.id },
              data: {
                status: 'FAILED_SUBMISSION',
                errorMessage: error.message, // Log the error message
                updatedAt: new Date(),
              },
            });
            console.log(`Updated JobID ${jobRecord.id} status to FAILED_SUBMISSION.`);
          } catch (dbError) {
            console.error(`Failed to update job record ${jobRecord.id} after Infobip error:`, dbError);
            // Log this secondary error, but proceed with sending the original error response
          }
        }
    
        // --- 6. Send Error Response to Client ---
        res.status(500).json({
          success: false,
          message: 'Failed to process broadcast SMS request.',
          jobId: jobRecord?.id, // Include job ID if available
          error: error.message,
        });
      }
    };
    
    export { handleBroadcastRequest };
    • Why: Adds database logging around the Infobip call. Creates a BroadcastJob record before calling Infobip, updates it with the bulkId and individual message statuses on success, or updates it with an error status on failure. This provides a persistent record of each attempt and its outcome. Includes basic error handling for the database update itself.

Frequently Asked Questions

How to send bulk SMS with Node.js and Express?

Use the Infobip API and Node.js SDK along with Express.js to create an API endpoint that accepts recipient phone numbers and a message, sending it as a bulk SMS broadcast via Infobip's platform. This setup handles the backend logic for sending notifications, alerts, and other mass communications.

What is the Infobip API used for in this Node.js project?

The Infobip API and its official Node.js SDK enable efficient sending of SMS messages, WhatsApp messages, and other communications. It integrates smoothly with a Node.js and Express server, streamlining the messaging logic.

Why use Express.js for bulk SMS in Node.js?

Express.js simplifies building a RESTful API endpoint to handle incoming broadcast requests. Its minimal design and widespread adoption make it an ideal framework for managing requests and responses in Node.js SMS applications.

How to set up Infobip API key in Node.js?

Obtain your API key and base URL from the Infobip dashboard. For local development, store these in a .env file and load them using dotenv. Never commit the .env file. In production, use platform-specific environment variable settings.

What Node.js packages are needed for Infobip SMS?

Install express, @infobip-api/sdk, and dotenv for core functionality. Nodemon is recommended for development to restart the server automatically on code changes. Optionally, use Prisma and SQLite for a database, and express-rate-limit for rate limiting.

How to structure a Node.js Express Infobip project?

Create folders for routes, controllers, services, and config. Implement controllers to manage requests, services to handle Infobip interactions, and routers to define endpoints. Centralize Infobip configuration, error handling, and API initialization logic in separate files.

What is the purpose of the /api/broadcast endpoint?

The /api/broadcast endpoint accepts POST requests containing an array of recipient phone numbers and the message text. The controller then utilizes the Infobip Service to send the message to all provided numbers via SMS using the Infobip API.

How to handle errors when sending bulk SMS with Infobip?

Implement validation in the controller to catch invalid input. The Infobip Service should handle API errors and provide specific messages for detailed logging. Use a global error handler in Express for unhandled errors and avoid leaking sensitive data in production.

How to handle rate limiting with Infobip SMS API?

Use the express-rate-limit middleware in your Express app to protect the /api/broadcast endpoint from excessive requests. Configure the limits based on your Infobip account limits and expected traffic patterns. Return informative error messages to clients exceeding the rate limit.

How to create a basic health check endpoint in Express.js?

Add a GET route, typically at /health, that returns a 200 OK status with a JSON payload like { status: 'UP' }. This allows monitoring systems to quickly check the server's availability. You can also include a timestamp.

How to log Infobip API responses effectively in Node.js?

Log essential data (e.g., bulkId) for monitoring. Consider using dedicated logging libraries like Winston or Pino for structured logging, log levels, and various output options (files, external services). Avoid logging sensitive details like the full API response body or API key.

Why use Prisma and a database with Infobip SMS integration?

Prisma and a database like SQLite (or others like Postgres) offer a persistent record of each broadcast request, including recipient details, message content, timestamps, and Infobip response data (bulkId, individual message statuses). This is essential for auditing and tracking message deliveries.

How to implement retry mechanisms for Infobip SMS?

Use libraries like async-retry or p-retry with exponential backoff and jitter for transient errors like 429 (Too Many Requests). Be cautious with retries for actions like SMS sending; consider idempotency or rely on Infobip's delivery reports for status updates instead of client-side retries.

Can I test error scenarios with the Infobip API?

Test with invalid input, bad phone numbers, or a temporarily wrong API key to see error handling. If possible, use tools like Postman to simulate network issues or trigger Infobip-specific errors to evaluate the robustness of your error logging and response handling.

When should I implement custom sender IDs for Infobip SMS?

Customize the "from" parameter in the Infobip API request when you want to display an alphanumeric sender ID (your brand name) instead of a phone number. Check Infobip's regulations for your country, as these IDs are subject to specific restrictions and require prior approval in many regions.