code examples

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

How to Send SMS with Node.js Using Vonage Messages API: Complete Express Tutorial

Learn how to send SMS programmatically with Node.js and Express using the Vonage Messages API. Step-by-step guide covering setup, security, error handling, and production deployment.

Expected: {"success":false,"message":"Missing "to" phone number or "text" message in request body."} (Status 400)

Build a production-ready Node.js and Express application that sends SMS messages programmatically using the Vonage Messages API. This comprehensive tutorial covers project setup, API integration, security best practices, error handling, and deployment strategies.

By the end, you'll have a REST API endpoint that accepts a phone number and message text, then sends an SMS via Vonage. Use this foundation to integrate SMS functionality into larger applications for notifications, two-factor authentication (2FA), OTP verification, or customer alerts.

Time to complete: 45–60 minutes

Skill level: Intermediate (requires basic JavaScript and Node.js knowledge)

Project Overview and Goals

What you're building:

A Node.js web server using Express that exposes a single API endpoint (POST /send-sms). When this endpoint receives a request with a destination phone number and message text, it uses the Vonage Messages API to send an SMS.

Problem solved:

Programmatically send SMS messages from your web application without managing direct carrier integrations.

Technologies:

  • Node.js: JavaScript runtime for server-side applications. Offers strong performance, a large ecosystem (npm), and asynchronous I/O suitable for API calls
  • Express: Minimal and flexible Node.js web framework for setting up routes and handling HTTP requests
  • Vonage Messages API: Communication platform enabling SMS, MMS, WhatsApp, and other messaging channels with robust global infrastructure
  • @vonage/server-sdk: Official Vonage Node.js library
  • dotenv: Module for loading environment variables from .env files to securely manage API credentials

System architecture:

text
+-------------+       +------------------------+       +-----------------+       +--------------+
|   Client    |------>|  Node.js/Express API   |------>|   Vonage API    |------>| Mobile Phone |
| (e.g. curl, |       |  (Your Application)    |       | (Messages API)  |       |  (Recipient) |
|  Postman)   |       |  POST /send-sms        |       +-----------------+       +--------------+
+-------------+       +------------------------+
                        |
                        | Uses @vonage/server-sdk
                        | Reads credentials from .env

Prerequisites:

  • Node.js 14+ and npm (or yarn): Download from nodejs.org
  • Vonage API Account: Sign up at Vonage API Dashboard. New accounts receive free credits
  • Vonage Application: Create a Vonage application with Messages capabilities enabled
  • Private Key: Associated with your Vonage application
  • Vonage Virtual Number: Purchase or rent a phone number through your Vonage account capable of sending SMS. Expect costs of $1–$5/month for number rental plus per-message fees (~$0.01–$0.05 per SMS)
  • Basic JavaScript/Node.js knowledge: Familiarity with fundamental concepts
  • Terminal access: For running commands

1. Set Up Your Node.js SMS Project

Create the project directory, initialize Node.js, and install dependencies.

  1. Create project directory:

    Open your terminal and create a new directory for your project.

    bash
    mkdir vonage-sms-sender
    cd vonage-sms-sender
  2. Initialize Node.js project:

    Create a package.json file with default settings.

    bash
    npm init -y
  3. Install dependencies:

    Install express for the web server, @vonage/server-sdk for Vonage API interaction, and dotenv for environment variable management.

    bash
    npm install express @vonage/server-sdk dotenv
    • express: Framework for building the API
    • @vonage/server-sdk: Simplifies Vonage Messages API calls
    • dotenv: Loads environment variables from .env files for secure configuration
  4. Create project files:

    Create the main application file, environment variables file, and Git ignore rules.

    macOS/Linux:

    bash
    touch index.js .env .gitignore

    Windows (Command Prompt):

    cmd
    type nul > index.js
    type nul > .env
    type nul > .gitignore

    Windows (PowerShell):

    powershell
    New-Item index.js, .env, .gitignore
    • index.js: Contains your Express server and API logic
    • .env: Stores sensitive credentials like API keys. Never commit this file to version control.
    • .gitignore: Specifies files Git should ignore (like .env and node_modules)
  5. Configure .gitignore:

    Open .gitignore and add these lines to prevent committing sensitive information:

    Code
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Vonage private key file
    private.key
    
    # Logs
    logs
    *.log
    
    # Runtime data
    pids
    *.pid
    *.seed
    *.pid.lock
    
    # Optional IDE directories/files
    .idea
    .vscode
    *.suo
    *.ntvs*
    *.njsproj
    *.sln
    *.sw?
  6. Verify project structure:

    Your project directory should look like this:

    text
    vonage-sms-sender/
    ├── .env
    ├── .gitignore
    ├── index.js
    ├── node_modules/
    ├── package-lock.json
    └── package.json

2. Configure Vonage API Credentials

Obtain credentials from Vonage and configure your application to use them securely via environment variables.

  1. Log in to Vonage Dashboard:

    Access your Vonage API Dashboard.

  2. Create a Vonage Application:

    • Navigate to "Applications" in the left-hand menu
    • Click "Create a new application"
    • Name your application (e.g., "Node SMS Sender Guide")
    • Click "Generate public and private key". Important: A file named private.key downloads immediately. Save this file in the root of your project directory (vonage-sms-sender/). You cannot download it again. Vonage stores the public key
    • Scroll to the "Capabilities" section
    • Toggle on "Messages". You'll see fields for "Inbound URL" and "Status URL". For sending SMS, these aren't strictly required immediately, but Vonage requires them. Enter placeholder URLs like https://example.com/webhooks/inbound and https://example.com/webhooks/status. Update these with real endpoints later if you plan to receive messages or delivery receipts
    • Click "Generate new application"
    • On the application details page, copy the Application ID – you'll need it for your .env file
  3. Link your Vonage number:

    • Navigate to "Numbers" > "Your numbers" in the dashboard
    • If you don't have a number, go to "Buy numbers", find one with SMS capability in your desired country, and purchase it. Expect costs of $1–$5/month for number rental
    • In "Your numbers", find the number you want to use for sending SMS
    • Click the gear icon or "Manage" button next to the number
    • Under "Messaging Settings", select the Vonage Application you created ("Node SMS Sender Guide") from the dropdown menu
    • Click "Save". Copy this phone number (including country code) – this is your sender ID (VONAGE_FROM_NUMBER)

    Choosing the right number type:

    • Long code (standard number): Most common for person-to-person messaging. Suitable for low-volume SMS
    • Short code: 5–6 digit numbers for high-volume messaging. Requires approval and higher costs
    • Toll-free: Free for recipients. Good for customer support. May have restrictions in some countries
  4. Get API Key and Secret (optional but recommended):

    While you primarily use Application ID and Private Key for Messages API authentication, your main account API Key and Secret are on the dashboard landing page ("API settings"). Store these for future use with other Vonage APIs.

  5. Configure environment variables (.env):

    Open the .env file and add these variables, replacing placeholders with your actual credentials:

    Code
    # Vonage Credentials
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID_HERE
    VONAGE_PRIVATE_KEY_PATH=./private.key
    VONAGE_FROM_NUMBER=YOUR_VONAGE_NUMBER_HERE
    
    # Optional: Vonage Account API Key/Secret (for other APIs)
    # VONAGE_API_KEY=YOUR_API_KEY_HERE
    # VONAGE_API_SECRET=YOUR_API_SECRET_HERE
    
    # Server Configuration
    PORT=3000
    • VONAGE_APPLICATION_ID: The Application ID from your Vonage application
    • VONAGE_PRIVATE_KEY_PATH: Relative path from index.js to your private.key file. ./private.key assumes it's in the same directory as index.js
    • VONAGE_FROM_NUMBER: Your Vonage virtual number in E.164 format (e.g., +14155550100)
    • PORT: Port number your Express server listens on

    Security: .env is in .gitignore, preventing accidental commits of secret credentials. Ensure private.key is also in .gitignore.

    Recommended: Create a .env.example file with placeholder values to help other developers set up their own .env file:

    Code
    VONAGE_APPLICATION_ID=your_application_id
    VONAGE_PRIVATE_KEY_PATH=./private.key
    VONAGE_FROM_NUMBER=+1234567890
    PORT=3000

    If you lose your private key: You cannot download it again. Generate a new key pair in the Vonage Dashboard under your application settings, then download and replace the old private.key file.

3. Implement the SMS Sending API Endpoint

Write the Node.js/Express code to create the server and SMS sending endpoint.

  1. Set up Express server (index.js):

    Open index.js and add the initial setup:

    javascript
    // index.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    const { Vonage } = require('@vonage/server-sdk');
    
    // --- Vonage Client Initialization ---
    // Ensure necessary environment variables are loaded
    if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) {
        console.error('Error: VONAGE_APPLICATION_ID or VONAGE_PRIVATE_KEY_PATH not set in .env');
        process.exit(1); // Exit if essential config is missing
    }
    
    const vonage = new Vonage({
        applicationId: process.env.VONAGE_APPLICATION_ID,
        privateKey: process.env.VONAGE_PRIVATE_KEY_PATH
    });
    
    // --- Express App Setup ---
    const app = express();
    const port = process.env.PORT || 3000; // Use port from .env or default to 3000
    
    // Middleware to parse JSON request bodies
    app.use(express.json());
    // Middleware to parse URL-encoded request bodies
    app.use(express.urlencoded({ extended: true }));
    
    // --- API Endpoints ---
    // (Add /send-sms endpoint here)
    
    // --- Basic Health Check Endpoint ---
    app.get('/health', (req, res) => {
        res.status(200).send('OK');
    });
    
    // --- Start Server ---
    app.listen(port, () => {
        console.log(`Server listening on http://localhost:${port}`);
    });
    • require('dotenv').config();: Loads variables from .env into process.env. Must be called at the top
    • Imports express and Vonage from the SDK
    • Initializes the Vonage client using Application ID and private key path from environment variables. Includes a check to ensure these critical variables are set
    • Sets up Express app (app)
    • Uses express.json() and express.urlencoded() middleware to parse incoming request bodies
    • Includes a /health endpoint for monitoring
    • Starts the server with app.listen
  2. Add CORS support (if needed for frontend integration):

    Install the cors package:

    bash
    npm install cors

    Update index.js:

    javascript
    // index.js (near the top)
    const cors = require('cors');
    // ...
    const app = express();
    app.use(cors()); // Enable CORS for all routes
    // ... (rest of middleware)
  3. Implement the /send-sms endpoint:

    Add this route handler within the // --- API Endpoints --- section of index.js:

    javascript
    // index.js (inside API Endpoints section)
    
    app.post('/send-sms', async (req, res) => {
        // Extract and validate input
        const { to, text } = req.body;
        const from = process.env.VONAGE_FROM_NUMBER;
    
        if (!to || !text) {
            console.error('Missing "to" or "text" in request body');
            return res.status(400).json({
                success: false,
                message: 'Missing "to" phone number or "text" message in request body.'
            });
        }
    
        if (!from) {
            console.error('Error: VONAGE_FROM_NUMBER not set in .env');
            return res.status(500).json({
                success: false,
                message: 'Server configuration error: Sender number not set.'
            });
        }
    
        // Validate message length (160 chars for single SMS)
        if (text.length > 1600) { // 10 messages max as example limit
            return res.status(400).json({
                success: false,
                message: 'Message too long. Maximum 1600 characters (10 SMS segments).'
            });
        }
    
        console.log(`Attempting to send SMS from ${from} to ${to} with text: "${text}"`);
    
        try {
            const resp = await vonage.messages.send({
                message_type: "text",
                text: text,
                to: to,
                from: from,
                channel: "sms"
            });
    
            console.log('Message sent successfully:', resp);
            // Vonage Messages API returns message_uuid on success
            res.status(200).json({
                success: true,
                message_uuid: resp.message_uuid
            });
    
        } catch (error) {
            console.error('Error sending SMS via Vonage:', error);
    
            // Provide specific feedback based on error
            let errorMessage = 'Failed to send SMS.';
            let statusCode = 500;
    
            if (error.response?.data) {
                console.error('Vonage API Error Details:', error.response.data);
                // Extract specific error message from Vonage response
                errorMessage = error.response.data.title || error.response.data.detail || errorMessage;
    
                // Set appropriate status code based on error type
                if (error.response.status >= 400 && error.response.status < 500) {
                    statusCode = 400; // Client error
                }
            } else if (error.message) {
                errorMessage = error.message;
            }
    
            res.status(statusCode).json({
                success: false,
                message: errorMessage,
                errorDetails: error.response?.data
            });
        }
    });
    • Defines a POST route at /send-sms
    • Uses an async function to handle the asynchronous vonage.messages.send call with await
    • Input validation: Extracts to (recipient number) and text (message content) from the JSON request body. Checks they exist and that the message isn't too long. Returns 400 Bad Request or 500 Internal Server Error if validation fails
    • Vonage call: Calls vonage.messages.send() with:
      • message_type: Set to "text" for standard SMS
      • text: SMS message content
      • to: Recipient's phone number (should be in E.164 format, e.g., +14155550101)
      • from: Your Vonage virtual number (sender ID) from .env
      • channel: Must be "sms"
    • Success handling: Logs the response (includes message_uuid) and sends a 200 OK response with success: true and the message_uuid
    • Error handling: Logs detailed errors and sends a 400 or 500 response with success: false and an error message, potentially including Vonage API response details

    Example request/response:

    Request:

    bash
    curl -X POST http://localhost:3000/send-sms \
         -H "Content-Type: application/json" \
         -d '{"to": "+14155550101", "text": "Hello from Vonage!"}'

    Success response (200):

    json
    {
      "success": true,
      "message_uuid": "abcd1234-5678-90ef-ghij-klmnopqrstuv"
    }

    Error response (400):

    json
    {
      "success": false,
      "message": "Missing \"to\" phone number or \"text\" message in request body."
    }

4. Advanced Error Handling and Logging for Production

Refine error handling and logging for production readiness.

Logging Strategy

Current implementation uses console.log for information and console.error for failures.

Production logging: Use a dedicated logging library:

  • Winston: Structured logging with multiple transports (file, console, remote services)
  • Pino: High-performance JSON logging

Install Winston:

bash
npm install winston

Configure Winston in index.js:

javascript
// index.js (near the top, after dotenv)
const winston = require('winston');

const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' })
    ]
});

// Log to console in development
if (process.env.NODE_ENV !== 'production') {
    logger.add(new winston.transports.Console({
        format: winston.format.simple()
    }));
}

// Replace console.log and console.error with logger
// Example: logger.info('Server started'), logger.error('Error occurred', { error })

Retry Mechanisms

Vonage internal retries: Vonage handles some retries internally for transient network issues when delivering messages to carriers.

Application-level retries: Network issues can occur between your server and the Vonage API. For critical messages, implement application-level retries with exponential backoff.

Install async-retry:

bash
npm install async-retry

Add retry logic to /send-sms endpoint:

javascript
// index.js (at the top)
const retry = require('async-retry');

// ... inside app.post('/send-sms') ...
try {
    const resp = await retry(async (bail) => {
        try {
            const result = await vonage.messages.send({
                message_type: "text",
                text: text,
                to: to,
                from: from,
                channel: "sms"
            });
            return result;
        } catch (error) {
            // Don't retry client errors (4xx)
            if (error.response?.status >= 400 && error.response?.status < 500) {
                bail(error); // Stop retrying
                return;
            }
            throw error; // Retry on 5xx or network errors
        }
    }, {
        retries: 3, // Number of retries
        factor: 2, // Exponential backoff factor
        minTimeout: 1000, // Initial timeout (1 second)
        maxTimeout: 5000 // Maximum timeout (5 seconds)
    });

    logger.info('Message sent successfully', { message_uuid: resp.message_uuid });
    res.status(200).json({ success: true, message_uuid: resp.message_uuid });

} catch (error) {
    // ... existing error handling ...
}

Idempotency

Prevent duplicate messages by implementing idempotency:

  • Accept an idempotency_key in the request body
  • Store sent message UUIDs with their idempotency keys in a database or cache (Redis)
  • Before sending, check if an idempotency key was already processed. If yes, return the cached response instead of sending again

Example:

javascript
// Pseudocode - requires Redis or database
const idempotencyKey = req.body.idempotency_key;
if (idempotencyKey) {
    const cachedResult = await redis.get(`idempotency:${idempotencyKey}`);
    if (cachedResult) {
        return res.status(200).json(JSON.parse(cachedResult));
    }
}

// Send message...
const resp = await vonage.messages.send({...});

// Cache result
if (idempotencyKey) {
    await redis.setex(`idempotency:${idempotencyKey}`, 86400, JSON.stringify({
        success: true,
        message_uuid: resp.message_uuid
    }));
}

5. Security Best Practices for SMS APIs

Implement security best practices to protect your application and Vonage account.

Input Validation

Use validation libraries for robust input checking:

Install joi:

bash
npm install joi

Add validation to /send-sms endpoint:

javascript
// index.js (at the top)
const Joi = require('joi');

// Define schema
const smsSchema = Joi.object({
    to: Joi.string().pattern(/^\+[1-9]\d{1,14}$/).required(), // E.164 format
    text: Joi.string().min(1).max(1600).required()
});

// ... inside app.post('/send-sms') ...
const { error, value } = smsSchema.validate(req.body);
if (error) {
    return res.status(400).json({
        success: false,
        message: error.details[0].message
    });
}

const { to, text } = value;
// ... rest of endpoint logic ...

Rate Limiting

Protect your API from abuse with rate limiting:

Install express-rate-limit:

bash
npm install express-rate-limit

Configure rate limiting:

javascript
// index.js (near the top, after express)
const rateLimit = require('express-rate-limit');

// Create rate limiter
const smsLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per windowMs
    message: 'Too many SMS requests from this IP. Try again after 15 minutes.',
    standardHeaders: true, // Return rate limit info in RateLimit-* headers
    legacyHeaders: false, // Disable X-RateLimit-* headers
});

// Apply to SMS endpoint
app.post('/send-sms', smsLimiter, async (req, res) => {
    // ... existing endpoint logic ...
});

Authentication

Protect your endpoint with API key authentication:

Generate API keys: Use a UUID generator or secure random string generator.

Store API keys: Use environment variables or a database.

Implement middleware:

javascript
// index.js
const API_KEYS = new Set([
    process.env.API_KEY_1,
    process.env.API_KEY_2
].filter(Boolean)); // Remove undefined keys

function authenticateApiKey(req, res, next) {
    const apiKey = req.headers['x-api-key'];

    if (!apiKey || !API_KEYS.has(apiKey)) {
        return res.status(401).json({
            success: false,
            message: 'Invalid or missing API key'
        });
    }

    next();
}

// Apply to SMS endpoint
app.post('/send-sms', authenticateApiKey, smsLimiter, async (req, res) => {
    // ... existing endpoint logic ...
});

Usage:

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -H "X-API-Key: your-secret-api-key" \
     -d '{"to": "+14155550101", "text": "Hello!"}'

HTTPS/TLS

Development: Use HTTP locally.

Production: Always use HTTPS. Most hosting platforms (Heroku, Render, AWS) provide HTTPS by default. If self-hosting:

  • Obtain SSL certificates from Let's Encrypt (free)
  • Configure your reverse proxy (Nginx, Apache) or Node.js HTTPS server

Helmet

Use Helmet to set security-related HTTP headers:

Install helmet:

bash
npm install helmet

Configure helmet:

javascript
// index.js (near the top)
const helmet = require('helmet');

const app = express();
app.use(helmet()); // Apply Helmet middleware early
// ... (rest of middleware) ...

Security Checklist

  • Environment variables secured (.env in .gitignore)
  • Private key file secured (private.key in .gitignore)
  • Input validation implemented (phone numbers, message length)
  • Rate limiting configured
  • API key authentication implemented
  • HTTPS enabled in production
  • Helmet middleware applied
  • Error messages don't expose sensitive information

6. Handle Special Cases

Consider edge cases relevant to SMS messaging.

Phone Number Formatting

Vonage expects numbers in E.164 format (e.g., +14155550100). Standardize input using libphonenumber-js:

Install libphonenumber-js:

bash
npm install libphonenumber-js

Validate and format phone numbers:

javascript
// index.js (at the top)
const { parsePhoneNumber, isValidPhoneNumber } = require('libphonenumber-js');

// ... inside app.post('/send-sms') ...
const { to, text } = req.body;

// Validate and format phone number
if (!isValidPhoneNumber(to)) {
    return res.status(400).json({
        success: false,
        message: 'Invalid phone number format. Use E.164 format (e.g., +14155550100).'
    });
}

const phoneNumber = parsePhoneNumber(to);
const formattedTo = phoneNumber.format('E.164');

// Use formattedTo in vonage.messages.send()
const resp = await vonage.messages.send({
    message_type: "text",
    text: text,
    to: formattedTo, // Use formatted number
    from: from,
    channel: "sms"
});

Character Limits and Encoding

  • GSM-7 encoding: 160 characters per SMS segment
  • UCS-2 encoding (Unicode): 70 characters per SMS segment for non-Latin characters
  • Concatenated SMS: Longer messages are split into multiple segments

Vonage handles concatenation automatically. Be mindful of costs – multi-part messages consume multiple message credits.

International Sending

Sender ID requirements vary by country:

CountrySender ID TypeNotes
US/CanadaLong code or toll-freeAlphanumeric sender IDs not supported
UKLong code or alphanumericAlphanumeric sender IDs supported
IndiaSender ID registration requiredStrict regulations, pre-registration needed
UAEPre-registered sender IDs onlyAlphanumeric sender IDs require approval
ChinaNot supportedSMS to mainland China blocked

Consult Vonage's country-specific documentation before sending internationally.

Delivery Failures

Not all SMS messages get delivered. Common reasons:

  • Invalid or disconnected number
  • Phone powered off or out of coverage
  • Carrier blocking or filtering
  • Number porting in progress

Implement Status Webhooks (Delivery Receipts):

  1. Update your Vonage Application's "Status URL" to your webhook endpoint (e.g., https://yourdomain.com/webhooks/status).

  2. Create the webhook endpoint:

    javascript
    // index.js
    app.post('/webhooks/status', (req, res) => {
        const { message_uuid, status, error_text } = req.body;
    
        logger.info('Message status update', { message_uuid, status, error_text });
    
        // Store status in database or trigger alerts
        // Status values: submitted, delivered, rejected, expired, etc.
    
        res.status(200).send('OK');
    });

Test Numbers (Free Tier)

On Vonage trial accounts, you can only send SMS to verified test numbers. To add a test number:

  1. Go to Vonage Dashboard > Settings > Test Numbers
  2. Enter the phone number
  3. Verify via SMS or voice call

Sending to unverified numbers results in a "Non-Whitelisted Destination" error.

SMS Compliance and Spam Regulations

Compliance requirements:

  • Obtain consent: Recipients must opt-in to receive messages
  • Provide opt-out: Include "Reply STOP to unsubscribe" in your messages
  • Honor opt-outs: Maintain a suppression list and stop sending to opted-out numbers
  • TCPA (US): Telephone Consumer Protection Act requires prior express written consent for marketing messages
  • GDPR (EU): General Data Protection Regulation requires consent and data protection measures
  • CTIA Guidelines: Follow Cellular Telecommunications Industry Association best practices

Implementation example:

javascript
// Maintain opt-out list (use database in production)
const optOutList = new Set();

app.post('/send-sms', authenticateApiKey, smsLimiter, async (req, res) => {
    const { to, text } = req.body;

    // Check opt-out list
    if (optOutList.has(to)) {
        return res.status(400).json({
            success: false,
            message: 'Recipient has opted out of receiving messages.'
        });
    }

    // ... rest of endpoint logic ...
});

// Handle opt-out webhook (inbound SMS)
app.post('/webhooks/inbound', (req, res) => {
    const { from, text } = req.body;

    if (text.trim().toUpperCase() === 'STOP') {
        optOutList.add(from);
        logger.info('User opted out', { phone: from });

        // Send confirmation (required by TCPA)
        vonage.messages.send({
            message_type: "text",
            text: "You have been unsubscribed. Reply START to resume messages.",
            to: from,
            from: process.env.VONAGE_FROM_NUMBER,
            channel: "sms"
        });
    }

    res.status(200).send('OK');
});

7. Test Your SMS Implementation

Ensure your implementation works correctly.

Start the Server

Verify your .env file contains correct Vonage credentials.

bash
node index.js

You should see: Server listening on http://localhost:3000

Manual Testing with curl

Replace YOUR_RECIPIENT_NUMBER with a valid phone number (use a verified test number if on a trial account).

Test successful SMS send:

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -H "X-API-Key: your-secret-api-key" \
     -d '{
           "to": "YOUR_RECIPIENT_NUMBER",
           "text": "Hello from my Node.js Vonage App!"
         }'

Expected output:

Terminal (server):

text
Server listening on http://localhost:3000
Attempting to send SMS from YOUR_VONAGE_NUMBER to YOUR_RECIPIENT_NUMBER with text: "Hello from my Node.js Vonage App!"
Message sent successfully: { message_uuid: '...' }

Terminal (curl):

json
{"success":true,"message_uuid":"SOME_UUID_FROM_VONAGE"}

Recipient phone receives the SMS message.

Test Error Cases

Missing parameters:

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -H "X-API-Key: your-secret-api-key" \
     -d '{"text": "Missing recipient"}'

# Expected: {"success":false,"message":"Missing \"to\" phone number or \"text\" message in request body."} (Status 400)

Invalid phone number:

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -H "X-API-Key: your-secret-api-key" \
     -d '{
           "to": "invalid-number",
           "text": "Testing invalid recipient"
         }'

# Expected: {"success":false,"message":"Invalid phone number format..."} (Status 400)

Non-whitelisted number (trial account):

bash
curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -H "X-API-Key: your-secret-api-key" \
     -d '{
           "to": "+15555555555",
           "text": "Testing non-whitelisted number"
         }'

# Expected: {"success":false,"message":"Non-Whitelisted Destination",...} (Status 400/500)

Rate limit exceeded:

bash
# Send 101 requests rapidly
for i in {1..101}; do
    curl -X POST http://localhost:3000/send-sms \
         -H "Content-Type: application/json" \
         -H "X-API-Key: your-secret-api-key" \
         -d '{"to": "+14155550101", "text": "Test '$i'"}'
done

# Expected (on 101st request): {"success":false,"message":"Too many SMS requests..."} (Status 429)

Automated Testing

Install Jest:

bash
npm install --save-dev jest supertest

Create test file (index.test.js):

javascript
const request = require('supertest');
const express = require('express');

// Mock Vonage SDK
jest.mock('@vonage/server-sdk');
const { Vonage } = require('@vonage/server-sdk');

describe('SMS API', () => {
    let app;
    let mockSend;

    beforeEach(() => {
        // Set up mock
        mockSend = jest.fn().mockResolvedValue({ message_uuid: 'test-uuid-123' });
        Vonage.mockImplementation(() => ({
            messages: {
                send: mockSend
            }
        }));

        // Set required environment variables
        process.env.VONAGE_APPLICATION_ID = 'test-app-id';
        process.env.VONAGE_PRIVATE_KEY_PATH = './test-private.key';
        process.env.VONAGE_FROM_NUMBER = '+14155550100';
        process.env.API_KEY_1 = 'test-api-key';

        // Load app (in real scenario, export app from index.js)
        app = require('./index');
    });

    afterEach(() => {
        jest.clearAllMocks();
    });

    test('POST /send-sms - success', async () => {
        const response = await request(app)
            .post('/send-sms')
            .set('X-API-Key', 'test-api-key')
            .send({
                to: '+14155550101',
                text: 'Test message'
            });

        expect(response.status).toBe(200);
        expect(response.body.success).toBe(true);
        expect(response.body.message_uuid).toBe('test-uuid-123');
        expect(mockSend).toHaveBeenCalledWith({
            message_type: 'text',
            text: 'Test message',
            to: '+14155550101',
            from: '+14155550100',
            channel: 'sms'
        });
    });

    test('POST /send-sms - missing parameters', async () => {
        const response = await request(app)
            .post('/send-sms')
            .set('X-API-Key', 'test-api-key')
            .send({ text: 'Test message' });

        expect(response.status).toBe(400);
        expect(response.body.success).toBe(false);
        expect(response.body.message).toContain('Missing');
    });

    test('POST /send-sms - invalid API key', async () => {
        const response = await request(app)
            .post('/send-sms')
            .set('X-API-Key', 'wrong-key')
            .send({
                to: '+14155550101',
                text: 'Test message'
            });

        expect(response.status).toBe(401);
        expect(response.body.success).toBe(false);
    });

    test('POST /send-sms - Vonage API error', async () => {
        mockSend.mockRejectedValue({
            response: {
                status: 400,
                data: { title: 'Invalid number format' }
            }
        });

        const response = await request(app)
            .post('/send-sms')
            .set('X-API-Key', 'test-api-key')
            .send({
                to: 'invalid',
                text: 'Test message'
            });

        expect(response.status).toBe(400);
        expect(response.body.success).toBe(false);
        expect(response.body.message).toContain('Invalid');
    });
});

Add test script to package.json:

json
{
  "scripts": {
    "test": "jest",
    "start": "node index.js"
  }
}

Run tests:

bash
npm test

Verification Checklist

  • Project dependencies installed (npm install)
  • .env file created with correct Vonage Application ID, Private Key Path, and From Number
  • private.key file downloaded and placed at specified path
  • .env and private.key in .gitignore
  • Vonage number linked to correct Vonage Application in dashboard
  • Server starts without errors (node index.js)
  • /health endpoint returns OK (Status 200)
  • Valid request to /send-sms returns {"success":true, "message_uuid":"..."} (Status 200)
  • Recipient phone receives SMS message
  • Invalid requests return appropriate error responses (Status 400/500)
  • Server logs show relevant information
  • Rate limiting works (Status 429 after limit exceeded)
  • API key authentication works (Status 401 for invalid keys)

8. Troubleshooting Common SMS Issues

Credentials Not Found

Error: Credentials could not be found or similar authentication errors.

Solutions:

  • Verify VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH in .env are correct
  • Ensure private.key file exists at the exact path specified in VONAGE_PRIVATE_KEY_PATH relative to where you run node index.js
  • Confirm private.key file content is correct (should start with -----BEGIN PRIVATE KEY-----)
  • Ensure dotenv is loaded (require('dotenv').config(); at the top of index.js)

Non-Whitelisted Destination

Error: Non-Whitelisted Destination error.

Solutions:

  • You're using a Vonage trial account. Add recipient numbers to your verified test list in Vonage Dashboard > Settings > Test Numbers
  • Upgrade your account by adding payment details

Invalid From Number

Error: Invalid 'From' number or sender ID errors.

Solutions:

  • Ensure VONAGE_FROM_NUMBER in .env is a valid number from your Vonage account in E.164 format (e.g., +14155550100)
  • Confirm this number is linked to your Vonage Application (matching VONAGE_APPLICATION_ID)
  • Check if the number can send SMS to the destination country. Some numbers have restrictions

Messages Not Received

Issue: SMS not delivered to recipient.

Solutions:

  • Verify recipient number (to) is correct and in E.164 format
  • Check server logs for message_uuid and any Vonage errors in the catch block
  • Check Vonage Dashboard logs for message status details
  • Consider temporary carrier issues, device problems (phone off, poor signal), or blocked numbers
  • Implement Delivery Receipts (status webhooks) for better tracking

Rate Limits Exceeded

Error: Status 429 or rate limit messages.

Solutions:

  • If you implemented express-rate-limit, check the limit configuration. Default is 100 requests per 15 minutes per IP
  • Vonage has platform-wide rate limits. Check your account tier and limits in the dashboard
  • Implement exponential backoff and retry logic in your client application

SDK Version Compatibility

Issue: Unexpected behavior after updating @vonage/server-sdk.

Solutions:

  • Check the SDK changelog for breaking changes
  • Pin SDK version in package.json for stability: "@vonage/server-sdk": "3.x.x" (replace with your version)
  • Test thoroughly after major version upgrades

Vonage Rate Limits and Quotas

Vonage imposes the following limits (as of 2024):

  • Free tier: ~1,000 messages (varies by country)
  • API rate limits:
    • 100 requests/second per account
    • 1,000 requests/second aggregate across all Vonage APIs
  • SMS throughput: Depends on number type:
    • Long code: 1 message/second
    • Short code: Up to 100 messages/second
    • Toll-free: Up to 3 messages/second

Check your specific limits in the Vonage Dashboard under "Settings" > "API Settings".

FAQ

Q: Can I use this with TypeScript?

A: Yes. Install @types/express and @types/node, rename index.js to index.ts, and compile with TypeScript.

Q: How do I handle inbound SMS?

A: Create a webhook endpoint (POST /webhooks/inbound) and configure it in your Vonage Application's "Inbound URL". The endpoint receives from, to, text, and other parameters.

Q: Can I send MMS?

A: Yes. Change message_type to "image" and add image.url parameter. Requires MMS-capable Vonage number.

Q: How do I track message delivery?

A: Implement status webhooks (see "Delivery Failures" section). Vonage sends updates to your "Status URL" endpoint.

Q: What's the cost per message?

A: Varies by destination country. US SMS typically costs $0.0075–$0.0150 per segment. Check Vonage pricing.

9. Deploy Your SMS Application to Production

Deploy your Node.js application to a production environment with proper configuration management.

Choose a Hosting Platform

Platform comparison:

PlatformTypeProsConsCost
HerokuPaaSSimple deployment, add-ons ecosystemHigher cost, limited free tier~$7/month (Eco dyno)
RenderPaaSModern PaaS, free tier, auto-deployNewer platform, fewer add-onsFree tier available, ~$7/month (Starter)
AWS Elastic BeanstalkPaaSAWS ecosystem integration, scalableMore complex setupPay per usage (~$10–50/month)
AWS EC2IaaSFull control, highly scalableRequires server managementPay per usage (~$5–100/month)
DigitalOceanIaaSSimple VPS, predictable pricingManual setup required~$6–40/month
AWS LambdaServerlessPay per request, auto-scalingCold starts, API Gateway costsPay per invocation (~$0.20 per 1M requests)

Environment Variable Management

Critical: Never commit .env or private.key to version control.

Platform-specific configuration:

  • Heroku: Use Config Vars (Dashboard > Settings > Config Vars or heroku config:set)
  • Render: Environment Variables (Dashboard > Environment)
  • AWS: Parameter Store or Secrets Manager
  • Docker: Environment variables in docker-compose.yml or -e flags

For private.key file:

  • Securely copy the file to your server during deployment (use SCP, CI/CD secure file handling, or secret managers)

  • Set VONAGE_PRIVATE_KEY_PATH to the file location on the server

  • Alternative: Store the private key content as an environment variable and write it to a file at runtime:

    javascript
    // index.js
    const fs = require('fs');
    const privateKeyPath = './private.key';
    
    if (process.env.VONAGE_PRIVATE_KEY_CONTENT) {
        fs.writeFileSync(privateKeyPath, process.env.VONAGE_PRIVATE_KEY_CONTENT);
    }
    
    const vonage = new Vonage({
        applicationId: process.env.VONAGE_APPLICATION_ID,
        privateKey: privateKeyPath
    });

Deploy to Heroku

Prerequisites: Install Heroku CLI.

Steps:

  1. Log in to Heroku:

    bash
    heroku login
  2. Create Heroku app:

    bash
    heroku create your-app-name
  3. Add Procfile:

    Create Procfile in project root:

    Procfile
    web: node index.js
  4. Set config vars:

    Replace placeholders with your actual values:

    bash
    heroku config:set VONAGE_APPLICATION_ID=your_application_id
    heroku config:set VONAGE_FROM_NUMBER=+14155550100
    heroku config:set API_KEY_1=your_secret_api_key
    
    # Option 1: Set private key path (requires copying file during deployment)
    heroku config:set VONAGE_PRIVATE_KEY_PATH=./private.key
    
    # Option 2: Store private key content as environment variable
    heroku config:set VONAGE_PRIVATE_KEY_CONTENT="$(cat private.key)"
  5. Commit changes:

    bash
    git add .
    git commit -m "Add Procfile and prepare for Heroku"
  6. Deploy:

    bash
    git push heroku main
  7. Verify deployment:

    bash
    heroku logs --tail
    heroku open

    Test the /health endpoint:

    bash
    curl https://your-app-name.herokuapp.com/health
    # Expected: OK

Deploy with Docker

Create Dockerfile:

dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application files
COPY . .

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs

EXPOSE 3000

CMD ["node", "index.js"]

Create docker-compose.yml:

yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - VONAGE_APPLICATION_ID=${VONAGE_APPLICATION_ID}
      - VONAGE_FROM_NUMBER=${VONAGE_FROM_NUMBER}
      - VONAGE_PRIVATE_KEY_PATH=/app/private.key
      - API_KEY_1=${API_KEY_1}
      - PORT=3000
    volumes:
      - ./private.key:/app/private.key:ro
    restart: unless-stopped

Create .env file for Docker Compose:

Code
VONAGE_APPLICATION_ID=your_application_id
VONAGE_FROM_NUMBER=+14155550100
API_KEY_1=your_secret_api_key

Build and run:

bash
docker-compose up -d

Test:

bash
curl http://localhost:3000/health

CI/CD Pipeline

GitHub Actions example (.github/workflows/deploy.yml):

yaml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test
        env:
          VONAGE_APPLICATION_ID: ${{ secrets.VONAGE_APPLICATION_ID }}
          VONAGE_PRIVATE_KEY_CONTENT: ${{ secrets.VONAGE_PRIVATE_KEY_CONTENT }}
          VONAGE_FROM_NUMBER: ${{ secrets.VONAGE_FROM_NUMBER }}
          API_KEY_1: ${{ secrets.API_KEY_1 }}

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Deploy to Heroku
        uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: "your-app-name"
          heroku_email: "your-email@example.com"

Setup secrets in GitHub:

Go to Repository > Settings > Secrets and variables > Actions, add:

  • VONAGE_APPLICATION_ID
  • VONAGE_PRIVATE_KEY_CONTENT (paste entire private key content)
  • VONAGE_FROM_NUMBER
  • API_KEY_1
  • HEROKU_API_KEY

GitLab CI example (.gitlab-ci.yml):

yaml
stages:
  - test
  - deploy

test:
  stage: test
  image: node:18-alpine
  script:
    - npm ci
    - npm run lint
    - npm test
  variables:
    VONAGE_APPLICATION_ID: $VONAGE_APPLICATION_ID
    VONAGE_PRIVATE_KEY_CONTENT: $VONAGE_PRIVATE_KEY_CONTENT
    VONAGE_FROM_NUMBER: $VONAGE_FROM_NUMBER
    API_KEY_1: $API_KEY_1

deploy:
  stage: deploy
  image: ruby:3.1
  script:
    - gem install dpl
    - dpl --provider=heroku --app=your-app-name --api-key=$HEROKU_API_KEY
  only:
    - main

Rollback Strategy

Heroku rollback:

bash
# View recent releases
heroku releases

# Rollback to previous release
heroku rollback

# Rollback to specific version
heroku rollback v42

Docker rollback:

bash
# Tag images with version numbers
docker build -t your-app:v1.2.3 .

# Switch to previous version
docker-compose down
docker tag your-app:v1.2.2 your-app:latest
docker-compose up -d

Monitoring and Observability

Recommended tools:

Integrate Sentry for error tracking:

bash
npm install @sentry/node
javascript
// index.js (at the top)
const Sentry = require('@sentry/node');

Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV || 'development',
    tracesSampleRate: 1.0,
});

// Add Sentry request handler after body parsers
app.use(Sentry.Handlers.requestHandler());

// Add routes here

// Add Sentry error handler before other error handlers
app.use(Sentry.Handlers.errorHandler());

Set up health check monitoring:

Configure your monitoring service to ping https://your-app.com/health every 1–5 minutes and alert on failures.

Conclusion

You've built a production-ready Node.js and Express application that sends SMS messages programmatically via the Vonage Messages API. This comprehensive guide covered:

  • Complete project setup with Node.js, Express, and Vonage SDK
  • Secure credential management with environment variables
  • Core SMS API implementation with robust error handling
  • Security best practices (authentication, rate limiting, input validation)
  • Special case handling (phone formatting, international sending, compliance)
  • Comprehensive testing strategies (manual and automated)
  • Production deployment with CI/CD pipelines and monitoring

Use this foundation to integrate SMS capabilities into your applications for notifications, OTP verification, two-factor authentication, alerts, and customer engagement.

Next steps:

Resources:

Frequently Asked Questions

How to send SMS with Node.js and Express

Use the Vonage Messages API and the @vonage/server-sdk. Set up an Express server, create a /send-sms endpoint, and use the SDK to send messages. Don't forget to configure your Vonage credentials in a .env file.

What is the Vonage Messages API?

The Vonage Messages API is a communication platform for sending SMS, MMS, WhatsApp messages, and more. It provides a simple and reliable way to integrate messaging into your applications, abstracting away the complexity of dealing with carriers directly.

Why use Node.js and Express for sending SMS?

Node.js, with its asynchronous nature, is efficient for I/O operations like API calls. Express simplifies routing and HTTP request handling, making it ideal for creating the API endpoint for sending SMS messages.

How to set up Vonage credentials for Node.js

Create a Vonage Application in the Vonage Dashboard, link a Vonage virtual number, and download your private key. Store your Application ID, Private Key Path, and Vonage Number in a .env file for secure access.

What is the purpose of private.key file?

The private.key file is used to authenticate your Node.js application with the Vonage API. It should be kept secure and never committed to version control. Its path is specified in the VONAGE_PRIVATE_KEY_PATH environment variable.

How to install necessary dependencies for Node.js SMS app

Use npm install express @vonage/server-sdk dotenv. This installs Express for the web server, the Vonage Server SDK for interacting with the API, and dotenv for managing environment variables.

How to create a Vonage Application?

Log in to your Vonage Dashboard, navigate to 'Applications', and click 'Create a new application'. Generate your public and private keys, enable the 'Messages' capability, and link a Vonage virtual number.

How to handle errors when sending SMS with Node.js

Implement a try-catch block around the vonage.messages.send() call. Return appropriate HTTP status codes (400/500) and informative JSON error messages for client-side and API errors. Consider adding retries for transient network issues.

How to implement input validation for /send-sms endpoint

Use middleware like Joi or express-validator to validate incoming phone numbers and message text. Check for required parameters and enforce limits on text length to ensure only valid data reaches the Vonage API.

What is the project structure for the Node.js SMS sender app

The main project files are index.js (containing the server logic), .env (for environment variables), .gitignore, package.json, and package-lock.json. The private.key is in the root directory.

How to deploy Node.js SMS application to Heroku

Use the Heroku CLI. Create a Procfile, set config vars for your Vonage credentials and private key path, commit changes, and push to Heroku. Ensure your private.key is handled securely during deployment.

When should I use application-level retries for sending SMS?

Implement retries with exponential backoff when network issues might disrupt communication between your server and the Vonage API. Use libraries like async-retry for easier implementation, but avoid retrying on certain client errors.

Why use dotenv in the Node.js SMS project

Dotenv loads environment variables from the .env file into process.env, allowing you to securely manage your Vonage API credentials and server configuration without exposing them in your code.

How to troubleshoot 'Credentials could not be found' error with Vonage

Double-check that your .env file has the correct VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, and that the private.key file exists at the specified path. Verify dotenv is correctly loaded in index.js.

What is rate limiting and why is it important for SMS sending

Rate limiting prevents abuse by restricting the number of requests from a single IP address within a time window. Use express-rate-limit to protect your Vonage account and API endpoint.