code examples

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

Send SMS with Node.js, Express, and Vonage: A Developer Guide

A step-by-step guide to building a Node.js and Express application for sending SMS messages using the Vonage API, covering setup, implementation, testing, and security.

This guide provides a step-by-step walkthrough for building a simple Node.js application using the Express framework to send SMS messages via the Vonage SMS API. We will cover everything from project setup and configuration to implementation, testing, and basic security considerations.

By the end of this guide, you will have a functional Express API endpoint capable of accepting a phone number and message content, and then using the Vonage API to deliver that message as an SMS.

Technologies Used:

  • Node.js: A JavaScript runtime environment for server-side development.
  • Express: A minimal and flexible Node.js web application framework.
  • Vonage SMS API: A service enabling programmatic sending and receiving of SMS messages globally. We'll use the @vonage/server-sdk for Node.js.
  • dotenv: A module to load environment variables from a .env file into process.env.

Prerequisites:

  • Node.js and npm (or yarn) installed on your machine.
  • A Vonage API account (Sign up via the Vonage Developer Portal - new accounts usually receive free credits).
  • A text editor or IDE (like VS Code).
  • Basic understanding of Node.js, JavaScript, and REST APIs.
  • A tool for making HTTP requests (like curl, Postman, or Insomnia).

System Architecture:

The application follows a simple architecture:

  1. Client (e.g., curl, Postman, Frontend App): Sends a POST request to our Express API endpoint (/send-sms) with the recipient's phone number and the message text.
  2. Express API (Our Node.js App): Receives the request, validates the input (basic validation in this guide), uses the Vonage Node.js SDK to interact with the Vonage API, passing the necessary credentials and message details.
  3. Vonage API: Receives the request from our application, processes it, and sends the SMS message to the specified recipient phone number.
  4. Recipient: Receives the SMS message on their mobile device.
text
[Client] ---- HTTP POST ----> [Express API (Node.js)] ---- Vonage SDK ----> [Vonage API] ---- SMS ----> [Recipient]
        <---- HTTP Response <--

Note: This guide focuses on sending SMS. Handling delivery status updates, which involves Vonage sending data back to your API via webhooks, is not covered here.

1. Setting up the Project

Let's start by creating our project directory and initializing our Node.js application.

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

    bash
    mkdir vonage-sms-guide
    cd vonage-sms-guide
  2. Initialize Node.js Project: Initialize the project using npm. The -y flag accepts the default settings.

    bash
    npm init -y

    This creates a package.json file.

  3. Install Dependencies: We need express for the web server, @vonage/server-sdk to interact with the Vonage API, and dotenv to manage our API credentials securely.

    bash
    npm install express @vonage/server-sdk dotenv
  4. Configure package.json for ES Modules: Open the package.json file and add the following line to enable the use of modern import/export syntax:

    json
    {
      ""name"": ""vonage-sms-guide"",
      ""version"": ""1.0.0"",
      ""description"": """",
      ""main"": ""index.js"",
      ""scripts"": {
        ""start"": ""node index.js"",
        ""test"": ""echo \""Error: no test specified\"" && exit 1""
      },
      ""keywords"": [],
      ""author"": """",
      ""license"": ""ISC"",
      ""dependencies"": {
        ""@vonage/server-sdk"": ""^3.0.0"",
        ""dotenv"": ""^16.0.0"",
        ""express"": ""^4.18.0""
      },
      ""type"": ""module""
    }

    Why ES Modules? Using ""type"": ""module"" allows us to use the import and export syntax standard in modern JavaScript, which is cleaner and preferred over the older CommonJS require() syntax. Note: The version numbers (^3.x.x, etc.) shown are examples. You should typically install the latest stable versions unless you have specific compatibility requirements.

  5. Create Core Files: Create the main application file and a helper file for the Vonage logic.

    bash
    touch index.js lib.js .env .gitignore
  6. Configure .gitignore: Prevent sensitive files and unnecessary directories from being committed to version control. Open .gitignore and add:

    text
    node_modules
    .env

    Why ignore .env? The .env file will contain your secret API credentials. It should never be committed to Git or any public repository to prevent security breaches.

2. Vonage Account and API Credentials

To use the Vonage API, you need an account and API credentials.

  1. Sign Up/Log In: Go to the Vonage Dashboard and sign up or log in.

  2. Find API Credentials:

    • Navigate to the ""API settings"" section in your Vonage Dashboard (often accessible directly from the main dashboard page or under your account name/settings).
    • You will find your API key and API secret. Keep these secure.
    • Dashboard Navigation: The exact location might change, but typically look for sections labeled ""API Keys,"" ""Credentials,"" or ""Settings.""
  3. Configure Environment Variables: Open the .env file you created earlier and add your Vonage API credentials and a sender ID.

    dotenv
    # Vonage API Credentials
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Sender ID (Use a purchased Vonage number, or an Alphanumeric Sender ID like 'MyAppSMS')
    # Note: Alphanumeric Sender IDs may have restrictions in some countries and might not support replies.
    # For testing with trial accounts, you might need to use 'Vonage APIs' as the sender or leave it blank if using test numbers only.
    VONAGE_SENDER_ID=MyAppSMS
    
    # Server Port
    PORT=3000
    • Replace YOUR_API_KEY and YOUR_API_SECRET with the actual values from your dashboard.
    • VONAGE_SENDER_ID: This is the 'From' number or name displayed on the recipient's phone. You can use a virtual number purchased from Vonage (in E.164 format, e.g., +12015550123) or an Alphanumeric Sender ID (up to 11 characters, e.g., MyAppSMS). Some countries have restrictions on Alphanumeric Sender IDs. If using a trial account without a purchased number, you might need to omit this or use the default provided during signup.
    • PORT: Defines the port your Express server will run on.
  4. Configure Test Numbers (Trial Accounts):

    • Crucial for Trial Accounts: If you are using a free trial or haven't added billing information, Vonage requires you to pre-register and verify the phone numbers you want to send SMS to. Sending to non-registered numbers will fail.
    • Dashboard Navigation: Go to your Vonage Dashboard -> ""Numbers"" -> ""Test numbers"".
    • Add the recipient phone number(s) you will use for testing. Vonage will send a verification code via SMS or voice call to confirm ownership.
    • Error Indication: If you try to send an SMS to a non-whitelisted number on a trial account, you will likely receive a Non-Whitelisted Destination error (Status code 6).

3. Implementing the SMS Sending Logic

Now, let's write the code to handle incoming requests and send SMS messages.

  1. Create SMS Sending Helper (lib.js): This file will encapsulate the logic for interacting with the Vonage SDK.

    javascript
    // lib.js
    import { Vonage } from '@vonage/server-sdk';
    import 'dotenv/config'; // Load .env variables into process.env
    
    // Initialize Vonage client
    const vonage = new Vonage({
      apiKey: process.env.VONAGE_API_KEY,
      apiSecret: process.env.VONAGE_API_SECRET
    });
    
    // Retrieve sender ID from environment variables
    const sender = process.env.VONAGE_SENDER_ID;
    
    /**
     * Sends an SMS message using the Vonage API.
     * @param {string} recipient - The recipient's phone number in E.164 format (e.g., +12015550123).
     * @param {string} message - The text message content.
     * @returns {Promise<object>} A promise that resolves with the Vonage API response data on success.
     * @throws {Error} Throws an error if sending fails, the API returns an error status, or the SDK encounters an issue.
     */
    export const sendSms = async (recipient, message) => {
      console.log(`Attempting to send SMS from ${sender} to ${recipient}`);
      try {
        // Use the promise-based send method from the SDK
        const resp = await vonage.sms.send({ to: recipient, from: sender, text: message });
    
        // Check the status of the first message in the response
        // Vonage API returns status '0' for success
        if (resp.messages[0].status === '0') {
          console.log('Message sent successfully:', resp.messages[0]);
          return resp; // Return the successful response data
        } else {
          // Log and throw an error if the status indicates failure
          const errorText = resp.messages[0]['error-text'];
          console.error(`Message failed with error: ${errorText}`);
          throw new Error(`Message failed: ${errorText} (Status: ${resp.messages[0].status})`);
        }
      } catch (err) {
        // Catch errors from the SDK call itself OR the error thrown above from API status check
        console.error('Error sending SMS via Vonage SDK or API:', err);
    
        // Re-throw the error to be caught by the caller (in index.js)
        // Ensure it's an Error object for consistent handling
        if (err instanceof Error) {
            throw err;
        } else {
            // If the caught object isn't an Error (less common with modern SDKs), wrap it
            throw new Error(`Vonage SDK Error: ${JSON.stringify(err)}`);
        }
      }
    };

    Code Explanation:

    • We import the Vonage class and dotenv/config (which ensures environment variables are loaded immediately).
    • We instantiate Vonage using the API key and secret loaded from .env.
    • The sendSms function is async and takes the recipient number and message text.
    • It uses await vonage.sms.send() provided by the SDK v3+.
    • It checks resp.messages[0].status. A status of '0' indicates success. Any other status indicates an error, and an Error object is thrown.
    • We log relevant information for debugging.
    • The function returns the success response or throws an Error object containing details from the API or the SDK, which will be caught by the caller.
  2. Create Express Server (index.js): This file sets up the Express server and defines the API endpoint.

    javascript
    // index.js
    import express from 'express';
    import 'dotenv/config'; // Load .env variables
    import { sendSms } from './lib.js'; // Import our SMS sending function
    
    // Initialize Express app
    const app = express();
    
    // Middleware to parse JSON and URL-encoded request bodies
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    
    // Define the port from environment variable or default to 3000
    const PORT = process.env.PORT || 3000;
    
    // Simple root route for health check/info
    app.get('/', (req, res) => {
      res.send(`Vonage SMS Sender API running on port ${PORT}. Use POST /send-sms to send a message.`);
    });
    
    // API endpoint to send SMS
    app.post('/send-sms', async (req, res) => {
      const { recipient, message } = req.body;
    
      // Basic Input Validation
      if (!recipient || !message) {
        console.error('Validation Error: Missing recipient or message in request body');
        return res.status(400).json({
          success: false,
          error: 'Missing required fields: recipient, message'
        });
      }
    
      // Basic recipient format check (starts with '+'). IMPORTANT: This is NOT sufficient for production.
      // For production, use a robust library like 'libphonenumber-js' to validate E.164 format.
      if (!recipient.startsWith('+')) {
          console.error('Validation Error: Recipient number must be in E.164 format (e.g., +12015550123)');
          return res.status(400).json({
              success: false,
              error: 'Invalid recipient format. Use E.164 format (e.g., +12015550123).'
          });
      }
    
      try {
        console.log(`Received request to send SMS to ${recipient}`);
        const result = await sendSms(recipient, message); // Call the async sendSms function
    
        // Send success response back to the client
        res.status(200).json({
          success: true,
          data: result // Include the Vonage response data
        });
      } catch (error) {
        // Log the detailed error on the server (error object comes from sendSms)
        console.error('API Error: Failed to send SMS:', error.message || error);
    
        // Send generic error response back to the client
        res.status(500).json({
          success: false,
          error: 'Failed to send SMS.',
          // Optionally include non-sensitive error details in development
          // details: process.env.NODE_ENV === 'development' ? error.message : undefined
        });
      }
    });
    
    // Start the server
    app.listen(PORT, () => {
      console.log(`Server listening at http://localhost:${PORT}`);
    });

    Code Explanation:

    • We import express, dotenv/config, and our sendSms function.
    • We initialize the Express application and apply middleware (express.json, express.urlencoded) to parse incoming request bodies.
    • We define a PORT.
    • A simple GET / route is added for basic confirmation that the server is running.
    • The core logic resides in the POST /send-sms endpoint, which is marked async.
    • It extracts recipient and message from the request body (req.body).
    • Basic validation is performed. Note: Production apps need much more robust validation, ideally using a library like libphonenumber-js as mentioned in the Security section.
    • It calls sendSms using await within a try...catch block to handle asynchronous operations and potential errors gracefully.
    • On success, it sends a 200 OK response with { success: true, data: ... }.
    • On failure (caught in the catch block), it logs the error server-side (using the error object thrown by sendSms) and sends a 500 Internal Server Error response with { success: false, error: ... }.
    • Finally, app.listen starts the server.

4. Running and Testing the Application

Now, let's run the server and test the endpoint.

  1. Start the Server: In your terminal, run:

    bash
    npm start

    You should see the output: Server listening at http://localhost:3000

  2. Test with curl: Open a new terminal window. Replace +1xxxxxxxxxx with a verified test number from your Vonage dashboard (including the + and country code) and customize the message.

    bash
    curl -X POST http://localhost:3000/send-sms \
         -H ""Content-Type: application/json"" \
         -d '{
               ""recipient"": ""+1xxxxxxxxxx"",
               ""message"": ""Hello from Node.js and Vonage!""
             }'
  3. Test with Postman/Insomnia:

    • Create a new request.
    • Set the method to POST.
    • Set the URL to http://localhost:3000/send-sms.
    • Go to the ""Body"" tab, select ""raw"", and choose ""JSON"" from the dropdown.
    • Enter the request body:
      json
      {
        ""recipient"": ""+1xxxxxxxxxx"",
        ""message"": ""Hello from Node.js and Vonage!""
      }
    • Send the request.
  4. Expected Success Response: If the request is successful and the SMS is accepted by Vonage, you should receive a 200 OK response similar to this:

    json
    {
      ""success"": true,
      ""data"": {
        ""messages"": [
          {
            ""to"": ""1xxxxxxxxxx"",
            ""message-id"": ""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"",
            ""status"": ""0"",
            ""remaining-balance"": ""1.85340000"",
            ""message-price"": ""0.00680000"",
            ""network"": ""12345""
          }
        ],
        ""message-count"": ""1""
      }
    }

    You should also receive the SMS on the specified recipient phone shortly after.

  5. Example Error Response (Validation Failure): If you omit the recipient:

    json
    {
      ""success"": false,
      ""error"": ""Missing required fields: recipient, message""
    }

    Status code: 400 Bad Request

  6. Example Error Response (Vonage API Failure): If sending fails due to an issue like an invalid API key or sending to a non-whitelisted number on a trial account:

    json
    {
      ""success"": false,
      ""error"": ""Failed to send SMS.""
    }

    Status code: 500 Internal Server Error (Check the server logs for more details like Message failed: Non Whitelisted Destination (Status: 6)).

5. Error Handling and Logging

While our basic example includes try...catch and logs errors, production applications require more robust strategies.

  • Specific Error Handling: Catch specific Vonage error codes (like status 1 for Throttled, 2 for Missing Params, 5 for Invalid Credentials, 6 for Non-Whitelisted Destination, 9 for Partner Quota Exceeded, etc.) and respond appropriately. You can parse the error.message or the status code from the error thrown by sendSms.
  • Logging Levels: Use a dedicated logging library like winston or pino. This enables different log levels (debug, info, warn, error), structured logging (JSON format for easier parsing by monitoring tools), and configurable outputs (console, files, external services).
    javascript
    // Conceptual example with Winston
    // npm install winston
    import winston from 'winston';
    
    const logger = winston.createLogger({
      level: 'info',
      format: winston.format.json(),
      transports: [
        new winston.transports.Console({ format: winston.format.simple() }),
        // Add file or other transports for production
      ],
    });
    
    // Replace console.log/error with logger.info/error
    // logger.error('API Error: Failed to send SMS:', error.message || error);
  • Retry Mechanisms: For transient network errors or rate limiting (status 1), implement a retry strategy, potentially with exponential backoff, using libraries like async-retry. Be cautious not to retry errors that are unlikely to succeed on retry (e.g., invalid credentials, invalid number).
  • Monitoring: Integrate with error tracking services (like Sentry, Datadog) to automatically capture and alert on exceptions in production.

6. Security Considerations

Even for this simple service, security is important.

  1. Secrets Management: Never hardcode API keys or secrets in your source code. Use environment variables (.env for local development, secure configuration management in deployment environments). Ensure .env is in your .gitignore.
  2. Input Validation: Sanitize and validate all inputs from the client (recipient, message).
    • Recipient: Use a robust library (e.g., libphonenumber-js) to validate E.164 format accurately. Our current startsWith('+') check is insufficient for production.
    • Message: Check for maximum length (standard SMS is 160 GSM-7 characters, longer messages are concatenated), potentially sanitize against harmful input (like script tags, though SMS is generally text-only), and enforce business rules. Libraries like joi or express-validator can structure this validation.
  3. Rate Limiting: Protect your endpoint from abuse and accidental loops by implementing rate limiting. The express-rate-limit middleware is easy to integrate.
    javascript
    // Example using express-rate-limit
    // npm install express-rate-limit
    import rateLimit from 'express-rate-limit';
    
    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
      legacyHeaders: false, // Disable the `X-RateLimit-*` headers
      message: 'Too many requests from this IP, please try again after 15 minutes'
    });
    
    // Apply the rate limiting middleware to API routes
    app.use('/send-sms', limiter); // Apply specifically to the SMS endpoint
  4. Authentication/Authorization: This example API is open. In a real application, you would protect this endpoint. Methods include:
    • API Keys: Require clients to send a unique API key in a header (Authorization: ApiKey YOUR_CLIENT_KEY). Validate the key server-side.
    • JWT Tokens: For user-specific actions, use JSON Web Tokens.
    • IP Whitelisting: Restrict access to specific IP addresses if applicable.

7. Troubleshooting and Caveats

  • Non-Whitelisted Destination (Status 6): The most common issue for trial accounts. Ensure the recipient number is added and verified under ""Numbers"" > ""Test numbers"" in the Vonage Dashboard.
  • Invalid Credentials (Status 5): Double-check your VONAGE_API_KEY and VONAGE_API_SECRET in your .env file. Ensure the file is being loaded correctly (the import 'dotenv/config' should be early in your files).
  • Invalid Sender Address (From) (Status 15): Ensure your VONAGE_SENDER_ID is either a valid purchased Vonage number (in E.164 format) or a correctly formatted Alphanumeric Sender ID allowed in the destination country. If using a trial account without a purchased number, try removing the VONAGE_SENDER_ID or using the default assigned by Vonage.
  • Incorrect Recipient Format: Ensure the recipient number starts with + and includes the country code (E.164 format). Use a validation library for robustness.
  • Insufficient Funds: Check your Vonage account balance.
  • SDK Version Issues: Ensure you are using compatible versions of Node.js and the @vonage/server-sdk. Check the SDK's documentation if you encounter unexpected behavior.

8. Deployment (Conceptual)

Deploying this application involves running it on a server accessible via the internet.

  • Platforms: Services like Heroku, Vercel, Render, AWS (EC2, Lambda, Elastic Beanstalk), Google Cloud (Cloud Run, App Engine), or DigitalOcean (App Platform, Droplets) can host Node.js applications.
  • Environment Variables: Crucially, configure your VONAGE_API_KEY, VONAGE_API_SECRET, and VONAGE_SENDER_ID securely within your chosen deployment platform's environment variable settings. Do not commit your .env file.
  • Port: Ensure your application listens on the port specified by the environment (often via process.env.PORT), as done in index.js.
  • npm install: The deployment process must run npm install (or npm ci for cleaner installs) to fetch dependencies.
  • Start Command: The platform needs to know how to start your app (e.g., npm start).
  • CI/CD: For robust deployments, set up a Continuous Integration/Continuous Deployment pipeline (using GitHub Actions, GitLab CI, Jenkins, etc.) to automate testing and deployment.

9. Verification and Testing

Beyond the manual curl/Postman tests:

  • Manual Verification Checklist:
      • Server starts without errors (npm start).
      • GET / returns the expected info message.
      • POST /send-sms with valid data returns 200 OK and success: true.
      • The SMS is received on the (verified test) recipient phone.
      • POST /send-sms with missing fields returns 400 Bad Request and success: false.
      • POST /send-sms with invalid recipient format returns 400 Bad Request.
      • (If possible to test) POST /send-sms with invalid credentials results in a 500 Internal Server Error on the client and specific error logs on the server.
      • Check Vonage Dashboard logs (""Logs"" -> ""SMS"") for message status details.
  • Automated Testing:
    • Unit Tests: Use a framework like Jest or Mocha/Chai to test the sendSms function in isolation. Mock the @vonage/server-sdk to simulate success and failure responses without actually calling the API.
    • Integration Tests: Use a library like supertest to make HTTP requests to your running Express application (or an in-memory instance) and assert the responses for different scenarios (/send-sms success, validation errors, etc.). These tests could potentially hit the actual Vonage API in a controlled testing environment if needed, but mocking is usually preferred.

This guide provides a solid foundation for sending SMS messages using Node.js, Express, and Vonage. Remember to enhance error handling, add robust input validation (especially for phone numbers using libraries like libphonenumber-js), implement proper security measures like authentication and rate limiting, and set up comprehensive logging and monitoring for production environments. Refer to the official Vonage Server SDK for Node.js documentation and the Vonage SMS API reference for more advanced features and details.