code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / Sinch

How to Send Bulk SMS with Node.js and Sinch API (2025 Guide)

Learn how to send bulk SMS messages using Node.js, Express, and Sinch Batch API. Step-by-step tutorial with code examples for authentication, error handling, and production deployment.

Build a Node.js Express Bulk SMS Broadcaster with Sinch

Learn how to send bulk SMS messages programmatically using Node.js, Express, and the Sinch Batch SMS API. This comprehensive tutorial shows you how to build a production-ready SMS broadcasting application that can send messages to up to 1,000 recipients per batch request.

Whether you need to send transactional notifications, marketing campaigns, OTP codes, or appointment reminders at scale, this guide covers everything from authentication and error handling to retry mechanisms and deployment best practices.

Technologies you'll use:

  • Node.js: A JavaScript runtime for building server-side applications.
  • Express: A minimal and flexible Node.js web application framework.
  • Sinch SMS API: A third-party service for sending and receiving SMS messages globally.
  • dotenv: A module to load environment variables from a .env file.
  • node-fetch (v2): A module to make HTTP requests (use v2 for CommonJS compatibility).
  • (Optional) Prisma: A modern ORM for database access (for managing recipient lists).
  • (Optional) PostgreSQL/SQLite: A relational database.

Prerequisites:

  • Node.js and npm (or yarn) installed.
  • A Sinch account with API credentials (Service Plan ID, API Token) and a provisioned virtual number.
  • Basic understanding of Node.js, Express, and REST APIs.
  • (Optional) Docker installed for containerization.
  • (Optional) A PostgreSQL database instance if using Prisma for recipient management.

Bulk SMS System Architecture Overview

Here's a high-level overview of the system you'll build:

text
+-------------+       +-------------------+       +-----------------+       +--------------+
| User/Client | ----> |   Node.js/Express | ----> |  Sinch Service  | ----> | Sinch SMS API| ----> SMS
| (e.g. curl) |       |      API Layer    |       |  (Your Wrapper) |       |              |
+-------------+       +-------------------+       +-----------------+       +--------------+
      |                       |
      | Optional              | Optional
      |                       |
      +-----------------------+--------------------> +-----------------+
                              |                      | Database (e.g., |
                              |                      |   PostgreSQL)   |
                              +---------------------> +-----------------+
                                   (Recipient Lists)

By the end of this guide, you'll have a deployable Node.js application with a secure API endpoint for initiating bulk SMS broadcasts via Sinch.


1. Set Up Your Node.js Project for Bulk SMS

Start by creating your project directory and initializing a Node.js project.

  1. Create project directory: Open your terminal and create a new directory for the project.

    bash
    mkdir sinch-bulk-sms-broadcaster
    cd sinch-bulk-sms-broadcaster
  2. Initialize Node.js project: Initialize the project using npm. The -y flag accepts default settings.

    bash
    npm init -y
  3. Install dependencies: Install Express for the web server, dotenv to manage environment variables, and node-fetch (specifically version 2 for easy CommonJS require usage) to make requests to the Sinch API.

    bash
    npm install express dotenv node-fetch@2
    • Why node-fetch@2? Version 3+ uses ES Modules, requiring "type": "module" in package.json and import syntax. Using v2 simplifies compatibility with the standard CommonJS require syntax.
  4. Install development dependencies (optional but recommended): Install nodemon to automatically restart the server during development.

    bash
    npm install --save-dev nodemon
  5. Create project structure: Organize the project for clarity and maintainability.

    bash
    mkdir src
    mkdir src/routes
    mkdir src/controllers
    mkdir src/services
    mkdir src/middleware
    mkdir src/config
    mkdir src/utils
    touch src/app.js
    touch src/server.js
    touch .env
    touch .gitignore
    • src/: Contains all source code.
    • routes/: Defines API endpoints.
    • controllers/: Handles incoming requests and orchestrates responses.
    • services/: Encapsulates business logic, like interacting with Sinch.
    • middleware/: Holds Express middleware functions (e.g., authentication).
    • config/: Stores configuration files (though we'll primarily use .env).
    • utils/: Contains helper functions.
    • app.js: Configures the Express application instance.
    • server.js: Starts the HTTP server.
    • .env: Stores sensitive credentials (API keys, etc.). Never commit this file.
    • .gitignore: Specifies files/directories Git should ignore.
  6. Configure .gitignore: Add node_modules and .env to prevent committing them to version control.

    text
    # .gitignore
    
    node_modules/
    .env
    *.log
  7. Configure .env: Create placeholder environment variables. You'll get the actual values later.

    dotenv
    # .env
    
    # Server Configuration
    PORT=3000
    
    # Sinch API Credentials
    SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID
    SINCH_API_TOKEN=YOUR_API_TOKEN
    SINCH_VIRTUAL_NUMBER=YOUR_SINCH_NUMBER # Your purchased or assigned Sinch number in E.164 format (e.g., +12025550181)
    
    # Application Security
    INTERNAL_API_KEY=generate-a-strong-secure-random-string # Used for basic internal API protection
    • Why .env? Storing configuration like API keys separately from code is crucial for security and deployment flexibility. dotenv loads these into process.env at runtime.
  8. Add run scripts to package.json: Add scripts for starting the server easily.

    json
    {
      "name": "sinch-bulk-sms-broadcaster",
      "version": "1.0.0",
      "description": "Bulk SMS broadcaster using Node.js, Express, and Sinch",
      "main": "src/server.js",
      "scripts": {
        "start": "node src/server.js",
        "dev": "nodemon src/server.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [
        "sms",
        "bulk",
        "sinch",
        "node",
        "express"
      ],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "dotenv": "^16.x",
        "express": "^4.x",
        "node-fetch": "^2.6.7"
      },
      "devDependencies": {
        "nodemon": "^2.x"
      }
    }
  9. Create basic Express app (src/app.js): Set up the Express application instance and load environment variables.

    javascript
    // src/app.js
    const express = require('express');
    const dotenv = require('dotenv');
    const messagingRoutes = require('./routes/messagingRoutes');
    const { errorHandler } = require('./middleware/errorHandler');
    const { healthCheck } = require('./controllers/healthController');
    
    dotenv.config();
    
    const app = express();
    
    app.use(express.json());
    
    app.get('/health', (req, res) => res.status(200).json({ status: 'OK' }));
    
    app.use('/api/v1/messaging', messagingRoutes);
    
    app.use((req, res, next) => {
        res.status(404).json({ message: 'Not Found' });
    });
    
    app.use(errorHandler);
    
    module.exports = app;
  10. Create server entry point (src/server.js): Import the app and start the HTTP server.

    javascript
    // src/server.js
    const app = require('./app');
    
    const PORT = process.env.PORT || 3000;
    
    app.listen(PORT, () => {
        console.log(`Server running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`);
    });

Now run npm run dev in your terminal. The server should start, though you haven't defined all routes and handlers yet.


2. Implement Sinch Batch SMS API Integration

Create a dedicated service to handle all interactions with the Sinch API. This keeps your API controller clean and makes the Sinch logic reusable and testable.

  1. Create Sinch service file: Create the file src/services/sinchService.js.

  2. Implement sendBulkSms function: This function takes an array of recipient phone numbers and the message body, then constructs and sends the request to the Sinch Batch SMS API.

    javascript
    // src/services/sinchService.js
    const fetch = require('node-fetch');
    
    const SINCH_API_BASE_URL = 'https://us.sms.api.sinch.com/xms/v1';
    
    /**
     * Sends a bulk SMS message using the Sinch Batch API.
     *
     * The Sinch Batch API allows sending a single message to multiple recipients (up to 1000 per batch).
     * Batches are queued and sent at the rate limit in first-in-first-out order.
     * See: https://developers.sinch.com/docs/sms/api-reference/sms/tag/Batches
     *
     * @param {string[]} recipients - An array of phone numbers in E.164 format (e.g., ['+12025550181', '+12025550182']).
     *                                 Maximum 1000 recipients per batch per Sinch API specification.
     * @param {string} messageBody - The text content of the SMS message (0-2000 characters).
     * @param {string} [clientReference=null] - Optional unique identifier for the batch.
     * @returns {Promise<object>} - The response object from the Sinch API containing batch_id and other details.
     * @throws {Error} - Throws an error if the API request fails or returns an error status.
     */
    async function sendBulkSms(recipients, messageBody, clientReference = null) {
        const servicePlanId = process.env.SINCH_SERVICE_PLAN_ID;
        const apiToken = process.env.SINCH_API_TOKEN;
        const sinchNumber = process.env.SINCH_VIRTUAL_NUMBER;
    
        if (!servicePlanId || !apiToken || !sinchNumber) {
            console.error('Error: Sinch API credentials or virtual number not configured in .env');
            throw new Error('Sinch service not configured.');
        }
    
        if (!recipients || recipients.length === 0) {
            throw new Error('Recipient list cannot be empty.');
        }
        if (!messageBody) {
            throw new Error('Message body cannot be empty.');
        }
    
        if (recipients.length > 1000) {
            throw new Error('Maximum 1000 recipients per batch allowed by Sinch API.');
        }
    
        const invalidNumbers = recipients.filter(num => !/^\+[1-9]\d{1,14}$/.test(num));
        if (invalidNumbers.length > 0) {
            throw new Error(`Invalid E.164 phone number format detected: ${invalidNumbers.join(', ')}`);
        }
    
        if (messageBody.length > 2000) {
            throw new Error('Message body exceeds maximum length of 2000 characters.');
        }
    
        const endpoint = `${SINCH_API_BASE_URL}/${servicePlanId}/batches`;
    
        const payload = {
            to: recipients,
            from: sinchNumber,
            body: messageBody,
            type: 'mt_text',
            ...(clientReference && { client_reference: clientReference })
        };
    
        console.log(`Sending bulk SMS to ${recipients.length} recipients via Sinch...`);
    
        try {
            const response = await fetch(endpoint, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiToken}`
                },
                body: JSON.stringify(payload)
            });
    
            const responseBody = await response.json();
    
            if (!response.ok) {
                console.error(`Sinch API Error (${response.status}):`, responseBody);
                throw new Error(`Sinch API request failed with status ${response.status}: ${responseBody.error?.message || response.statusText}`);
            }
    
            console.log('Sinch API Response:', responseBody);
            return responseBody;
    
        } catch (error) {
            console.error('Error sending request to Sinch:', error);
            if (error.message.startsWith('Sinch API request failed')) {
                throw error;
            }
            throw new Error('Failed to communicate with Sinch API.');
        }
    }
    
    module.exports = {
        sendBulkSms
    };
    • Why this structure?
      • It fetches credentials securely from process.env.
      • It performs comprehensive input validation (non-empty recipients/message, E.164 format, batch size limits, message length).
      • It constructs the exact payload required by the Sinch batches endpoint per official API documentation.
      • It uses node-fetch to make the POST request with the correct headers (Content-Type, Authorization).
      • It checks the response status (response.ok) and parses the JSON body.
      • It throws informative errors for failed requests or configuration issues.
      • It logs relevant information for debugging.

3. Build a Secure Express API Endpoint for SMS Broadcasting

Expose the bulk sending functionality through a secure Express API endpoint.

  1. Create authentication middleware (src/middleware/authMiddleware.js): Implement simple API key authentication to protect the endpoint.

    javascript
    // src/middleware/authMiddleware.js
    
    function authenticateApiKey(req, res, next) {
        const apiKey = req.headers['x-api-key'];
        const expectedApiKey = process.env.INTERNAL_API_KEY;
    
        if (!expectedApiKey) {
            console.error('INTERNAL_API_KEY not set in environment variables. API is unprotected!');
        }
    
        if (expectedApiKey && apiKey === expectedApiKey) {
            return next();
        } else if (!expectedApiKey) {
            console.warn('INTERNAL_API_KEY not set, allowing request. THIS IS INSECURE FOR PRODUCTION.');
            return next();
        } else {
            console.warn('Unauthorized API access attempt denied.');
            return res.status(401).json({ message: 'Unauthorized: Invalid or missing API Key' });
        }
    }
    
    module.exports = {
        authenticateApiKey
    };
    • Why API Key? While not as robust as OAuth, a simple API key in the header provides a basic layer of security suitable for internal services or trusted clients. Ensure INTERNAL_API_KEY is a strong, random string.
  2. Create messaging controller (src/controllers/messagingController.js): This controller handles request validation and calls the sinchService.

    javascript
    // src/controllers/messagingController.js
    const sinchService = require('../services/sinchService');
    
    async function handleBulkSmsRequest(req, res, next) {
        const { recipients, message, clientReference } = req.body;
    
        if (!recipients || !Array.isArray(recipients) || recipients.length === 0) {
            return res.status(400).json({ message: 'Bad Request: "recipients" must be a non-empty array of phone numbers.' });
        }
        if (!message || typeof message !== 'string' || message.trim() === '') {
            return res.status(400).json({ message: 'Bad Request: "message" must be a non-empty string.' });
        }
    
        const MAX_RECIPIENTS_PER_BATCH = 1000;
        if (recipients.length > MAX_RECIPIENTS_PER_BATCH) {
            return res.status(400).json({
                message: `Bad Request: Maximum ${MAX_RECIPIENTS_PER_BATCH} recipients per batch allowed by Sinch API.`
            });
        }
    
        try {
            console.log(`Received bulk SMS request for ${recipients.length} recipients.`);
            const sinchResponse = await sinchService.sendBulkSms(recipients, message.trim(), clientReference);
    
            res.status(202).json({
                message: `Bulk SMS batch accepted for processing.`,
                batchId: sinchResponse.id,
                details: sinchResponse
            });
    
        } catch (error) {
            next(error);
        }
    }
    
    module.exports = {
        handleBulkSmsRequest
    };
    • Why next(error)? Instead of handling all error responses here, pass errors to the centralized errorHandler middleware (created later) for consistent error formatting.
    • Why 202 Accepted? The Sinch API accepts the batch request, but actual delivery happens asynchronously per the Batches API documentation. 202 reflects that the request was accepted for processing.
  3. Create messaging routes (src/routes/messagingRoutes.js): Define the /broadcast endpoint and apply the authentication middleware.

    javascript
    // src/routes/messagingRoutes.js
    const express = require('express');
    const messagingController = require('../controllers/messagingController');
    const { authenticateApiKey } = require('../middleware/authMiddleware');
    
    const router = express.Router();
    
    router.post(
        '/broadcast',
        authenticateApiKey,
        messagingController.handleBulkSmsRequest
    );
    
    module.exports = router;
  4. Update src/app.js (Import routes): Ensure messagingRoutes is imported and used in src/app.js (already done in Step 1.9).

  5. Test the API endpoint: Restart your server (npm run dev). Test using curl or Postman. Replace placeholders with your actual values.

    bash
    curl -X POST http://localhost:3000/api/v1/messaging/broadcast \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_INTERNAL_API_KEY" \
    -d '{
      "recipients": ["YOUR_RECIPIENT_1", "YOUR_RECIPIENT_2"],
      "message": "Hello from the Sinch Bulk Broadcaster!",
      "clientReference": "test-broadcast-001"
    }'

    Expected Success Response (JSON):

    json
    {
      "message": "Bulk SMS batch accepted for processing.",
      "batchId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
      "details": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "to": ["YOUR_RECIPIENT_1", "YOUR_RECIPIENT_2"],
        "from": "YOUR_SINCH_NUMBER",
        "canceled": false,
        "body": "Hello from the Sinch Bulk Broadcaster!",
        "type": "mt_text",
        "client_reference": "test-broadcast-001"
      }
    }

    Expected Error Response (e.g., Missing/Invalid API Key - JSON):

    json
    {
      "message": "Unauthorized: Invalid or missing API Key"
    }

    Expected Error Response (e.g., Bad Request - JSON):

    json
    {
        "message": "Bad Request: \"recipients\" must be a non-empty array of phone numbers."
    }

4. Configure Sinch API Credentials and Authentication

Configure Sinch credentials securely and handle them properly.

  1. Obtain Sinch credentials:

    • Log in to your Sinch Customer Dashboard.
    • Navigate to APIs in the left-hand menu.
    • Under SMS, find your active API configuration or create a new one.
    • Note down the Service plan ID and API token. Treat the API token like a password—keep it confidential.
    • Navigate to NumbersYour virtual numbers.
    • Note down the phone number you want to use as the sender (from address). Ensure it's in E.164 format (e.g., +12025550181).
  2. Configure environment variables (.env): Open your .env file and replace the placeholders with your actual Sinch credentials and a secure internal API key.

    dotenv
    # .env
    
    # Server Configuration
    PORT=3000
    
    # Sinch API Credentials
    SINCH_SERVICE_PLAN_ID=YOUR_ACTUAL_SERVICE_PLAN_ID
    SINCH_API_TOKEN=YOUR_ACTUAL_API_TOKEN
    SINCH_VIRTUAL_NUMBER=+12345678900
    
    # Application Security
    INTERNAL_API_KEY=dJ8sK9pL3dRqZ7vN1xY5bA...
    • SINCH_SERVICE_PLAN_ID: Identifies your specific service plan with Sinch. Required in the API endpoint URL.
    • SINCH_API_TOKEN: Authenticates your requests to the Sinch API using Bearer token authentication. Sent in the Authorization: Bearer header.
    • SINCH_VIRTUAL_NUMBER: The sender ID (phone number) that will appear on the recipient's device. Must be a number associated with your Sinch account in E.164 format.
    • INTERNAL_API_KEY: Used by your own API middleware (authenticateApiKey) to protect the endpoint.
  3. Secure handling:

    • Never commit .env to Git. Ensure .env is listed in your .gitignore file.
    • Use environment variables provided by your deployment platform (Heroku Config Vars, AWS Secrets Manager, etc.) in production instead of a .env file.
    • Rotate your Sinch API token periodically for enhanced security.
  4. Rate limits and queuing:

    • Each Sinch service plan has a rate limit that sets the maximum messages per second. A batch with 10 recipients counts as 10 messages for rate limiting.
    • Batches are queued and sent at the rate limit in first-in-first-out (FIFO) order. New batches are accepted immediately but may be delayed if earlier batches are in the queue.
    • For high-volume scenarios, implement application-level queueing with Redis or RabbitMQ to buffer requests.
  5. Fallback mechanisms (conceptual): While Sinch is generally reliable, consider these for extreme high availability (more advanced):

    • Retry logic: Implement retry logic within sinchService.js for transient network errors or specific Sinch 5xx errors (see Section 5).
    • Circuit breaker: Use a pattern/library (like opossum) to temporarily stop sending requests to Sinch if a high error rate is detected, preventing cascading failures.
    • Alternative provider: For critical systems, you might have a secondary SMS provider configured, switching over if Sinch experiences a prolonged outage. This adds significant complexity. For most use cases, robust retry logic is sufficient.

5. Implement Error Handling and Retry Logic for Bulk SMS

Build robust error handling and logging for diagnosing issues in production.

  1. Centralized error handler (src/middleware/errorHandler.js): Create middleware to catch errors passed via next(error) and format a consistent JSON response.

    javascript
    // src/middleware/errorHandler.js
    
    function errorHandler(err, req, res, next) {
        console.error('--- Unhandled Error ---');
        console.error('Timestamp:', new Date().toISOString());
        console.error('Request Path:', req.path);
        console.error('Request Method:', req.method);
        console.error('Error Stack:', err.stack || err);
        console.error('--- End Unhandled Error ---');
    
        let statusCode = err.statusCode || 500;
        let message = err.message || 'Internal Server Error';
    
        if (err.message.startsWith('Sinch API request failed')) {
            statusCode = 502;
            message = 'Failed to communicate with SMS provider.';
        } else if (err.message === 'Sinch service not configured.') {
            statusCode = 503;
            message = 'SMS service is temporarily unavailable due to configuration issues.';
        } else if (err.message.includes('Invalid E.164 phone number format')) {
            statusCode = 400;
            message = err.message;
        } else if (err.message === 'Recipient list cannot be empty.' || err.message === 'Message body cannot be empty.') {
            statusCode = 400;
            message = err.message;
        } else if (err.message.includes('Maximum 1000 recipients')) {
            statusCode = 400;
            message = err.message;
        }
    
        res.status(statusCode).json({
            message: message,
            ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
        });
    }
    
    module.exports = {
        errorHandler
    };
    • Why centralized? Ensures all errors are logged consistently and return a standardized JSON response format to the client, hiding potentially sensitive stack traces in production.
  2. Update src/app.js (Import error handler): Ensure the errorHandler is imported and registered last in the middleware chain in src/app.js (already done in Step 1.9).

  3. Logging:

    • You're currently using console.log and console.error. This is acceptable for simple applications or development.
    • Production logging: For production, use a dedicated logging library like winston or pino. These offer:
      • Log levels: (debug, info, warn, error) to control verbosity.
      • Structured logging: Output logs in JSON format for easier parsing by log analysis tools (e.g., Datadog, Splunk, ELK stack).
      • Transports: Send logs to files, databases, or external services.
  4. Retry mechanisms (basic implementation in sinchService.js): Add simple retry logic for potential network issues or transient Sinch errors.

    javascript
    // src/services/sinchService.js
    const fetch = require('node-fetch');
    const SINCH_API_BASE_URL = 'https://us.sms.api.sinch.com/xms/v1';
    
    const MAX_RETRIES = 3;
    const INITIAL_RETRY_DELAY_MS = 500;
    
    const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
    
    async function sendBulkSms(recipients, messageBody, clientReference = null) {
        const servicePlanId = process.env.SINCH_SERVICE_PLAN_ID;
        const apiToken = process.env.SINCH_API_TOKEN;
        const sinchNumber = process.env.SINCH_VIRTUAL_NUMBER;
    
        if (!servicePlanId || !apiToken || !sinchNumber) {
            console.error('Error: Sinch API credentials or virtual number not configured in .env');
            throw new Error('Sinch service not configured.');
        }
        if (!recipients || recipients.length === 0) {
            throw new Error('Recipient list cannot be empty.');
        }
        if (!messageBody) {
            throw new Error('Message body cannot be empty.');
        }
        if (recipients.length > 1000) {
            throw new Error('Maximum 1000 recipients per batch allowed by Sinch API.');
        }
        const invalidNumbers = recipients.filter(num => !/^\+[1-9]\d{1,14}$/.test(num));
        if (invalidNumbers.length > 0) {
            throw new Error(`Invalid E.164 phone number format detected: ${invalidNumbers.join(', ')}`);
        }
        if (messageBody.length > 2000) {
            throw new Error('Message body exceeds maximum length of 2000 characters.');
        }
    
        const endpoint = `${SINCH_API_BASE_URL}/${servicePlanId}/batches`;
        const payload = {
            to: recipients,
            from: sinchNumber,
            body: messageBody,
            type: 'mt_text',
            ...(clientReference && { client_reference: clientReference })
        };
    
        console.log(`Sending bulk SMS to ${recipients.length} recipients via Sinch...`);
    
        let attempts = 0;
        while (attempts < MAX_RETRIES) {
            attempts++;
            try {
                console.log(`Sinch API request attempt ${attempts}/${MAX_RETRIES}...`);
                const response = await fetch(endpoint, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${apiToken}`
                    },
                    body: JSON.stringify(payload),
                    timeout: 10000
                });
    
                if (response.status >= 400 && response.status < 500) {
                    const responseBody = await response.json().catch(() => ({}));
                    console.error(`Sinch API Client Error (${response.status}) on attempt ${attempts}:`, responseBody);
                    throw new Error(`Sinch API request failed with status ${response.status}: ${responseBody.error?.message || response.statusText} (Non-Retryable Client Error)`);
                }
    
                if (!response.ok) {
                    const responseBody = await response.json().catch(() => ({}));
                    console.warn(`Sinch API Server Error (${response.status}) on attempt ${attempts}:`, responseBody);
                    throw new Error(`Sinch API server error ${response.status}`);
                }
    
                const responseBody = await response.json();
                console.log('Sinch API Response:', responseBody);
                return responseBody;
    
            } catch (error) {
                console.warn(`Attempt ${attempts} failed: ${error.message}`);
    
                const isNonRetryable = error.message.includes('Non-Retryable Client Error') ||
                                       error.message === 'Sinch service not configured.' ||
                                       error.message.includes('Invalid E.164') ||
                                       error.message.includes('cannot be empty') ||
                                       error.message.includes('Maximum 1000 recipients') ||
                                       error.message.includes('exceeds maximum length');
    
                if (attempts >= MAX_RETRIES || isNonRetryable) {
                    console.error(`Giving up after ${attempts} attempts or due to non-retryable error.`);
                    throw isNonRetryable ? error : new Error(`Failed to send SMS via Sinch after ${attempts} attempts.`);
                }
    
                const jitter = Math.random() * INITIAL_RETRY_DELAY_MS * 0.5;
                const delayTime = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempts - 1) + jitter;
                console.log(`Retrying in approximately ${Math.round(delayTime)}ms...`);
                await delay(delayTime);
            }
        }
        throw new Error('Sinch request failed unexpectedly after retry loop.');
    }
    
    module.exports = {
        sendBulkSms
    };
    • Why retry? Network glitches or temporary Sinch issues can occur. Retrying increases the chance of successful delivery without manual intervention.
    • Why exponential backoff with jitter? Waiting longer between retries (INITIAL_RETRY_DELAY_MS * Math.pow(2, attempts - 1)) prevents overwhelming the Sinch API during widespread issues. Adding jitter (a small random variation) helps prevent multiple instances of your service from retrying simultaneously.
    • Why differentiate errors? Only retry on server errors (5xx) or network timeouts/errors per best practices. Client errors (4xx like invalid number format, bad credentials) or configuration/input errors identified before the request are not retryable, so you fail fast.

Frequently Asked Questions (FAQ)

How do I send bulk SMS with Node.js using Sinch API?

Use the Sinch Batch SMS API with Node.js and Express. Install node-fetch, express, and dotenv, configure your Sinch Service Plan ID and API Token, create a service function that constructs a batch request with an array of E.164 phone numbers (up to 1000 per batch), and send the POST request to https://us.sms.api.sinch.com/xms/v1/{service_plan_id}/batches with Bearer token authentication.

What is the Sinch Batch API for bulk SMS?

The Sinch Batch API allows you to send a single message to multiple recipients simultaneously. You submit one API request with an array of phone numbers (1-1000) in the to field, and Sinch distributes the message to all recipients. The API returns a batch_id for tracking, supports delivery reports, and handles rate limiting automatically. Batches are queued and sent at the rate limit in first-in-first-out order. This is more efficient than sending individual requests per recipient.

How do I authenticate requests to the Sinch SMS API?

Use Bearer token authentication with your Sinch API Token. Include the header Authorization: Bearer YOUR_API_TOKEN in all requests. Obtain your API Token from the Sinch Customer Dashboard under APIs → SMS. Store the token securely in environment variables using .env and never commit it to version control. The token authenticates your service plan access.

What phone number format does Sinch require?

Sinch requires E.164 international phone number format: +[country code][subscriber number] without spaces or special characters. The format allows 1-15 digits total. Examples: +12025550181 (US), +447700900123 (UK). Validate numbers with the regex /^\+[1-9]\d{1,14}$/ before sending. Invalid formats will cause 400 Bad Request errors from the Sinch API. All recipient numbers must be in this format.

How do I handle errors and retries in bulk SMS sending?

Implement exponential backoff with jitter for retry logic per Sinch rate limiting guidance. Retry only on server errors (5xx) or network timeouts, not on client errors (4xx). Start with 500ms delay, double each retry (500ms, 1000ms, 2000ms), add random jitter to prevent thundering herd, limit to 3-5 attempts, and log all failures. For 4xx errors (invalid numbers, bad credentials), fail fast without retries as these indicate non-retryable issues.

What is the maximum number of recipients per Sinch batch request?

According to the Sinch Batches API documentation, the to field accepts 1-1000 phone numbers per batch request. For the rate limit calculation, a batch with 10 recipients counts as 10 messages. For larger lists, split into multiple batches with appropriate pacing between requests. Each service plan has its own rate limit (messages per second).

How do I secure my bulk SMS API endpoint?

Implement API key authentication in the x-api-key header, use HTTPS in production, validate all input (phone numbers in E.164 format, message content), sanitize request data to prevent injection attacks, rate limit requests to prevent abuse, rotate API keys periodically, use environment variables for credentials, and never expose Sinch tokens in client-side code or logs. Store the Sinch API Token securely per authentication best practices.

How do I track delivery status for bulk SMS messages?

Enable delivery reports in your Sinch batch request by adding delivery_report: 'summary', 'full', or 'per_recipient' to the payload. Sinch sends callbacks to your webhook endpoint with status updates (delivered, failed, etc.). Store the batch_id returned from the API, implement a webhook endpoint to receive delivery receipts, and update your database with final delivery status. Configure webhooks in the Sinch Dashboard or per-batch via callback_url.

What's the difference between node-fetch v2 and v3 for Sinch integration?

node-fetch v2 uses CommonJS (require()) and works seamlessly with standard Express setups without additional configuration. node-fetch v3+ uses ES Modules requiring import syntax and "type": "module" in package.json, which changes your entire project structure. For CommonJS Express projects (as shown in this guide), use node-fetch@2 for simpler integration. If using ES Modules throughout your project, v3+ works fine with async/await import statements.

How do I implement rate limiting for bulk SMS sending?

Use the p-limit or bottleneck npm package to control concurrent requests at the application level. Respect Sinch rate limits specified in your service plan (messages per second). Each service plan gets its own message queue served in FIFO order. Batches are queued and sent at the rate limit automatically by Sinch, but implement application-level throttling for high-volume scenarios. Monitor 429 (Too Many Requests) responses (if applicable) and implement backoff. For large sends, split into multiple batches and queue them gradually over time.


Conclusion: Your Node.js Bulk SMS Solution is Ready

You've built a production-ready bulk SMS broadcasting system using Node.js, Express, and the Sinch Batch SMS API. Your application handles secure API authentication with Bearer tokens, validates E.164 phone number formats, implements exponential backoff retry logic for transient failures, provides comprehensive error handling with centralized middleware, and exposes a clean REST API endpoint protected with API key authentication.

The Sinch Batch API enables you to efficiently send messages to up to 1000 recipients per batch with a single request, while your Express application provides a robust wrapper with input validation, error handling, and retry mechanisms. With proper environment variable management using .env, structured error responses, and configurable retry logic, your bulk SMS broadcaster is ready for production deployment.

As you scale your messaging operations, consider implementing message queueing with Redis or RabbitMQ for high-volume scenarios, adding Prisma with PostgreSQL for persistent recipient list management and delivery tracking, integrating monitoring tools like Datadog or Sentry for real-time error tracking, implementing webhook endpoints for Sinch delivery reports, and adding application-level rate limiting to work within your service plan's rate limits. Your Node.js bulk SMS broadcaster is now ready to power notifications, alerts, and marketing campaigns at scale.