Bulk SMS with Twilio using Node.js, TypeScript, and Express

Bulk SMS with Twilio using Node.js, TypeScript, and Express

Project Setup

Step 1: Initialize Project

mkdir twilio-bulk-sms
cd twilio-bulk-sms
npm init -y
npm install express twilio dotenv csv-parser multer cors
npm install -D typescript @types/node @types/express @types/cors @types/multer ts-node-dev

Step 2: Create TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Step 3: Create Environment File

# .env
TWILIO_ACCOUNT_SID=your_account_sid_here
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_PHONE_NUMBER=+12345678901
PORT=3000

Main Application Code

Step 4: Create the Main Server

// src/index.ts
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import multer from 'multer';
import fs from 'fs';
import csv from 'csv-parser';
import { Twilio } from 'twilio';

dotenv.config();

const app = express();
const upload = multer({ dest: 'uploads/' });
const port = process.env.PORT || 3000;

// Validate environment variables
if (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN || !process.env.TWILIO_PHONE_NUMBER) {
  console.error('Missing Twilio environment variables. Please check your .env file.');
  process.exit(1);
}

// Initialize Twilio client
const twilioClient = new Twilio(
  process.env.TWILIO_ACCOUNT_SID,
  process.env.TWILIO_AUTH_TOKEN
);

app.use(cors());
app.use(express.json());

// Interface for CSV row
interface Contact {
  phone_number: string;
  name?: string;
  [key: string]: string | undefined;
}

// Helper function to validate phone number
function isValidPhoneNumber(phoneNumber: string): boolean {
  const phoneRegex = /^\+[1-9]\d{1,14}$/;
  return phoneRegex.test(phoneNumber);
}

// Helper function to send single SMS
async function sendSMS(to: string, message: string): Promise<any> {
  try {
    const result = await twilioClient.messages.create({
      body: message,
      from: process.env.TWILIO_PHONE_NUMBER!,
      to: to
    });
    return { success: true, sid: result.sid, to, status: result.status };
  } catch (error: any) {
    return { 
      success: false, 
      to, 
      error: error.message || 'Unknown error' 
    };
  }
}

// Endpoint 1: Send SMS to a single number
app.post('/api/send-single', async (req, res) => {
  try {
    const { phone_number, message } = req.body;

    if (!phone_number || !message) {
      return res.status(400).json({ 
        error: 'phone_number and message are required' 
      });
    }

    if (!isValidPhoneNumber(phone_number)) {
      return res.status(400).json({ 
        error: 'Invalid phone number format. Use E.164 format (e.g., +12345678901)' 
      });
    }

    const result = await sendSMS(phone_number, message);

    if (result.success) {
      res.json({
        success: true,
        message: 'SMS sent successfully',
        data: result
      });
    } else {
      res.status(500).json({
        success: false,
        error: result.error
      });
    }
  } catch (error: any) {
    res.status(500).json({ 
      error: 'Failed to send SMS', 
      details: error.message 
    });
  }
});

// Endpoint 2: Send bulk SMS from CSV
app.post('/api/send-bulk', upload.single('file'), async (req, res) => {
  try {
    const { message, delay = 500 } = req.body;

    if (!req.file) {
      return res.status(400).json({ error: 'CSV file is required' });
    }

    if (!message) {
      return res.status(400).json({ error: 'Message is required' });
    }

    const filePath = req.file.path;
    const contacts: Contact[] = [];
    const results: any[] = [];

    // Read and parse CSV file
    await new Promise((resolve, reject) => {
      fs.createReadStream(filePath)
        .pipe(csv())
        .on('data', (row) => {
          if (row.phone_number) {
            contacts.push(row);
          }
        })
        .on('end', resolve)
        .on('error', reject);
    });

    if (contacts.length === 0) {
      fs.unlinkSync(filePath);
      return res.status(400).json({ error: 'No valid phone numbers found in CSV' });
    }

    // Process each contact with delay
    for (let i = 0; i < contacts.length; i++) {
      const contact = contacts[i];

      // Validate phone number
      if (!isValidPhoneNumber(contact.phone_number)) {
        results.push({
          success: false,
          to: contact.phone_number,
          error: 'Invalid phone number format'
        });
        continue;
      }

      // Personalize message
      let personalizedMessage = message;
      if (contact.name) {
        personalizedMessage = personalizedMessage.replace(/{name}/g, contact.name);
      }

      // Send SMS
      const result = await sendSMS(contact.phone_number, personalizedMessage);
      results.push(result);

      // Add delay between messages (except for last one)
      if (i < contacts.length - 1 && delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }

    // Clean up uploaded file
    fs.unlinkSync(filePath);

    // Calculate summary
    const successCount = results.filter(r => r.success).length;
    const failureCount = results.filter(r => !r.success).length;

    res.json({
      success: true,
      summary: {
        total: contacts.length,
        sent: successCount,
        failed: failureCount
      },
      results: results
    });

  } catch (error: any) {
    if (req.file) {
      fs.unlinkSync(req.file.path);
    }
    res.status(500).json({ 
      error: 'Failed to process bulk SMS', 
      details: error.message 
    });
  }
});

// Endpoint 3: Check service health
app.get('/api/health', (req, res) => {
  res.json({ 
    status: 'running', 
    service: 'Twilio Bulk SMS API',
    timestamp: new Date().toISOString()
  });
});

// Start server
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
  console.log('Available endpoints:');
  console.log(`  POST http://localhost:${port}/api/send-single`);
  console.log(`  POST http://localhost:${port}/api/send-bulk`);
  console.log(`  GET  http://localhost:${port}/api/health`);
});

Step 5: Create a Simple HTML Test Page

<!-- test.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Twilio SMS Tester</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1, h2 {
            color: #333;
        }
        .endpoint {
            background: #f8f9fa;
            padding: 15px;
            margin: 15px 0;
            border-left: 4px solid #007bff;
            border-radius: 4px;
        }
        code {
            background: #e9ecef;
            padding: 2px 6px;
            border-radius: 3px;
        }
        pre {
            background: #2d2d2d;
            color: #fff;
            padding: 15px;
            border-radius: 5px;
            overflow-x: auto;
        }
        .test-section {
            margin: 30px 0;
            padding: 20px;
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        input, textarea {
            width: 100%;
            padding: 10px;
            margin: 10px 0;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        button {
            background: #007bff;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background: #0056b3;
        }
        .response {
            margin-top: 15px;
            padding: 15px;
            background: #f8f9fa;
            border-radius: 4px;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Twilio Bulk SMS API Tester</h1>

        <div class="endpoint">
            <h2>API Endpoints</h2>
            <p><strong>Health Check:</strong> <code>GET http://localhost:3000/api/health</code></p>
            <p><strong>Single SMS:</strong> <code>POST http://localhost:3000/api/send-single</code></p>
            <p><strong>Bulk SMS:</strong> <code>POST http://localhost:3000/api/send-bulk</code></p>
        </div>

        <div class="test-section">
            <h2>Test Single SMS</h2>
            <input type="text" id="phoneNumber" placeholder="Phone number (e.g., +12345678901)" value="+15005550006">
            <textarea id="message" rows="4" placeholder="Enter your message">Hello, this is a test message!</textarea>
            <button onclick="sendSingleSMS()">Send Single SMS</button>
            <div id="singleResponse" class="response"></div>
        </div>

        <div class="test-section">
            <h2>Test Bulk SMS</h2>
            <p>Create a CSV file with this format:</p>
            <pre>phone_number,name
+15005550006,John Doe
+15005550007,Jane Smith</pre>
            <input type="file" id="csvFile" accept=".csv">
            <textarea id="bulkMessage" rows="4" placeholder="Enter message template">Hello {name}, this is a bulk test message!</textarea>
            <input type="number" id="delay" placeholder="Delay between messages (ms)" value="500">
            <button onclick="sendBulkSMS()">Send Bulk SMS</button>
            <div id="bulkResponse" class="response"></div>
        </div>

        <div class="test-section">
            <h2>Sample API Requests</h2>
            <h3>Single SMS (cURL)</h3>
            <pre>curl -X POST http://localhost:3000/api/send-single \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15005550006",
    "message": "Hello from Twilio!"
  }'</pre>

            <h3>Bulk SMS (cURL)</h3>
            <pre>curl -X POST http://localhost:3000/api/send-bulk \
  -F "[email protected]" \
  -F "message=Hello {name}, this is a test message" \
  -F "delay=500"</pre>
        </div>
    </div>

    <script>
        function sendSingleSMS() {
            const phone = document.getElementById('phoneNumber').value;
            const message = document.getElementById('message').value;
            const responseDiv = document.getElementById('singleResponse');

            responseDiv.innerHTML = 'Sending...';

            fetch('http://localhost:3000/api/send-single', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    phone_number: phone,
                    message: message
                })
            })
            .then(response => response.json())
            .then(data => {
                responseDiv.innerHTML = JSON.stringify(data, null, 2);
            })
            .catch(error => {
                responseDiv.innerHTML = 'Error: ' + error.message;
            });
        }

        function sendBulkSMS() {
            const fileInput = document.getElementById('csvFile');
            const message = document.getElementById('bulkMessage').value;
            const delay = document.getElementById('delay').value;
            const responseDiv = document.getElementById('bulkResponse');

            if (!fileInput.files || fileInput.files.length === 0) {
                alert('Please select a CSV file');
                return;
            }

            responseDiv.innerHTML = 'Processing...';

            const formData = new FormData();
            formData.append('file', fileInput.files[0]);
            formData.append('message', message);
            formData.append('delay', delay);

            fetch('http://localhost:3000/api/send-bulk', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                responseDiv.innerHTML = JSON.stringify(data, null, 2);
            })
            .catch(error => {
                responseDiv.innerHTML = 'Error: ' + error.message;
            });
        }
    </script>
</body>
</html>

Step 6: Create Sample CSV File

# contacts.csv
phone_number,name
+15005550006,John Doe
+15005550007,Jane Smith
+15005550008,Bob Wilson

Step 7: Create Package.json Scripts

Update your package.json file:

{
  "name": "twilio-bulk-sms",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "dev": "ts-node-dev --respawn src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "echo \"Open test.html in browser\" && exit 0"
  },
  "dependencies": {
    "express": "^4.18.2",
    "twilio": "^4.19.0",
    "dotenv": "^16.3.1",
    "csv-parser": "^3.0.0",
    "multer": "^1.4.5-lts.1",
    "cors": "^2.8.5"
  },
  "devDependencies": {
    "typescript": "^5.2.2",
    "@types/node": "^20.8.0",
    "@types/express": "^4.17.20",
    "@types/cors": "^2.8.13",
    "@types/multer": "^1.4.7",
    "ts-node-dev": "^2.0.0"
  }
}

Running the Application

Step 8: Start the Server

# Development mode with auto-reload
npm run dev

# Or build and run production
npm run build
npm start

You should see:

Server running on http://localhost:3000
Available endpoints:
  POST http://localhost:3000/api/send-single
  POST http://localhost:3000/api/send-bulk
  GET  http://localhost:3000/api/health

Testing with Postman

Endpoint 1: Health Check

  • Method: GET
  • URL: http://localhost:3000/api/health
  • Response: JSON status of the service

Endpoint 2: Send Single SMS

  • Method: POST
  • URL: http://localhost:3000/api/send-single
  • Headers: Content-Type: application/json
  • Body (raw JSON):
{
  "phone_number": "+15005550006",
  "message": "Hello, this is a test message!"
}

Endpoint 3: Send Bulk SMS

  • Method: POST
  • URL: http://localhost:3000/api/send-bulk
  • Body (form-data):
  • Key: file, Value: [Select your CSV file]
  • Key: message, Value: Hello {name}, this is a test!
  • Key: delay, Value: 500 (optional, default 500ms)

Testing in Browser

  1. Open test.html in your browser
  2. Fill in the form fields
  3. Click the buttons to test the endpoints
  4. View responses in the result boxes

Sample API Responses

Successful Single SMS Response:

{
  "success": true,
  "message": "SMS sent successfully",
  "data": {
    "success": true,
    "sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "to": "+15005550006",
    "status": "queued"
  }
}

Bulk SMS Response:

{
  "success": true,
  "summary": {
    "total": 3,
    "sent": 2,
    "failed": 1
  },
  "results": [
    {
      "success": true,
      "sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "to": "+15005550006",
      "status": "queued"
    },
    {
      "success": true,
      "sid": "SMyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
      "to": "+15005550007",
      "status": "queued"
    },
    {
      "success": false,
      "to": "+15005550008",
      "error": "The number +15005550008 is unverified."
    }
  ]
}

Error Handling

The API handles common errors:

  1. Invalid phone number format
  2. Missing required fields
  3. File upload errors
  4. Twilio authentication errors
  5. Network timeouts

Important Notes

  1. Use Twilio Test Numbers: For testing without charges, use:
  • +15005550006 (always succeeds)
  • +15005550007 (always succeeds)
  • +15005550008 (always fails)
  1. Phone Number Format: Must be in E.164 format (e.g., +12345678901)
  2. Rate Limiting: The delay parameter helps avoid Twilio rate limits
  3. Security:
  • Never commit .env file to version control
  • Use environment variables for credentials
  • Validate all inputs

This solution provides a minimal, functional API for sending bulk SMS with Twilio. The code is simple, well-structured, and includes everything needed to get started quickly.

Posts Carousel

Leave a Comment

Your email address will not be published. Required fields are marked with *

Latest Posts

Most Commented

Featured Videos