code examples

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

Build Two-Way SMS Messaging with Node.js Express & Vonage API (Complete Tutorial)

Learn how to implement inbound and outbound SMS messaging in Node.js using Express and Vonage. Step-by-step guide covers webhooks, auto-replies, and production deployment.

Build Two-Way SMS Messaging with Node.js Express & Vonage API

This comprehensive guide shows you how to build a production-ready Node.js application using the Express framework to handle two-way SMS messaging with the Vonage Messages API. You will learn how to send outbound SMS messages, receive inbound SMS via webhooks, and create automated SMS reply workflows.

Project Overview and Goals

What We're Building:

An Express.js application capable of:

  1. Sending SMS: Exposing an API endpoint to send SMS messages programmatically via the Vonage Messages API.
  2. Receiving SMS: Handling incoming SMS messages sent to a Vonage virtual number via webhooks.
  3. Two-Way Interaction: Replying automatically to inbound messages, demonstrating a basic two-way conversation flow.

Problem Solved:

This application enables businesses and developers to integrate programmatic SMS communication into their workflows, facilitating automated notifications, customer support interactions, alerts, and more, directly from their Node.js backend.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Express.js: A minimal and flexible Node.js web application framework used to build the API and handle webhook requests.
  • Vonage Messages API: A unified API for sending and receiving messages across various channels, including SMS. We will use it for both outbound and inbound SMS.
  • @vonage/server-sdk: The official Vonage Node.js SDK for interacting with Vonage APIs.
  • ngrok: A tool to expose local development servers to the internet, essential for testing Vonage webhooks locally.
  • dotenv: A module to load environment variables from a .env file into process.env.

System Architecture:

+-----------------+ +-----------------------+ +----------------+ +-------------+ | User/Client App | ---> | Node.js/Express App | ---> | Vonage API | ---> | User's Phone| | (e.g., curl, UI)| | (Sends SMS, | | (Messages API) | | (Receives SMS)| | | <--- | Handles Webhooks) | <--- | | <--- | (Sends SMS) | +-----------------+ +-----------------------+ +----------------+ +-------------+ | ^ | | API Call | | Webhook Notification v +-----+ +-----------------------+ | Vonage Application | | (Configured Webhooks) | +-----------------------+

Prerequisites:

  • Vonage API Account: Sign up at Vonage.com. You'll need your API Key and Secret.
  • Vonage Virtual Number: Purchase an SMS-capable number from the Vonage Dashboard.
  • Node.js and npm: Installed on your development machine (LTS version recommended). Download Node.js
  • ngrok: Installed and authenticated. Download ngrok
  • Basic JavaScript/Node.js Knowledge: Familiarity with asynchronous programming (async/await or Promises).
  • Terminal/Command Line Access: For running commands.

Expected Outcome:

By the end of this guide, you will have a running Node.js Express application that can send SMS messages via an API call and automatically reply to any SMS messages received on your configured Vonage number.

1. Setting Up Your Node.js SMS Project

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

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

    bash
    mkdir vonage-sms-app
    cd vonage-sms-app
  2. Initialize npm: Initialize the project with npm to create a package.json file. The -y flag accepts default settings.

    bash
    npm init -y
  3. Install Dependencies: Install Express, the Vonage Server SDK, and dotenv.

    bash
    npm install express @vonage/server-sdk dotenv
  4. Create .gitignore: Create a .gitignore file to prevent sensitive information and unnecessary files from being committed to version control.

    bash
    touch .gitignore

    Add the following lines to your .gitignore file:

    text
    # .gitignore
    
    # Node dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Private Key file (if stored in project)
    private.key
    
    # Log files
    *.log
    
    # OS generated files
    .DS_Store
    Thumbs.db
  5. Set Up Environment Variables: Create a .env file in the root of your project to store sensitive credentials and configuration.

    bash
    touch .env

    Populate .env with the following variables. We'll fill in the values in the next section.

    dotenv
    # .env
    
    # Vonage API Credentials (Found in your Vonage Dashboard)
    VONAGE_API_KEY=YOUR_VONAGE_API_KEY
    VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
    
    # Vonage Application Credentials (Generated in the next step)
    VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
    VONAGE_PRIVATE_KEY_PATH=./private.key # Or the full path to your key
    
    # Vonage Number (Purchased from Vonage Dashboard)
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
    
    # Application Port
    PORT=3000
    • Why .env? Storing configuration and secrets in environment variables is a best practice. It keeps sensitive data out of your codebase and makes configuration easier across different environments (development, staging, production). dotenv helps load these variables during development.
  6. Create Basic Server File: Create an app.js file for your Express application logic.

    bash
    touch app.js

    Add the initial Express setup:

    javascript
    // app.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    const { json, urlencoded } = express;
    
    const app = express();
    app.use(json()); // Middleware to parse JSON request bodies
    app.use(urlencoded({ extended: true })); // Middleware to parse URL-encoded request bodies
    
    const PORT = process.env.PORT || 3000; // Use port from .env or default to 3000
    
    // Basic route for testing
    app.get('/', (req, res) => {
      res.send('Vonage SMS App is running!');
    });
    
    app.listen(PORT, () => {
      console.log(`Server listening at http://localhost:${PORT}`);
    });
  7. Run the Basic Server: Start your server to ensure the basic setup is working.

    bash
    node app.js

    You should see Server listening at http://localhost:3000 in your terminal. You can stop the server with Ctrl+C.

2. Vonage Account and Application Setup

Now, let's configure your Vonage account, get the necessary credentials, and set up a Vonage Application to handle messages.

  1. Log in to Vonage Dashboard: Access your Vonage API Dashboard.

  2. Get API Key and Secret: Your API Key and Secret are displayed at the top of the dashboard home page. Copy these values and paste them into your .env file for VONAGE_API_KEY and VONAGE_API_SECRET.

  3. Purchase a Vonage Number:

    • Navigate to Numbers -> Buy numbers.
    • Search for a number with SMS capabilities in your desired country.
    • Purchase the number.
    • Copy this number (including the country code, e.g., 14155550100) and paste it into your .env file for VONAGE_NUMBER.
  4. Set Default SMS API to Messages API:

    • Navigate to Account -> API settings.
    • Scroll down to the SMS settings section.
    • Under Default SMS Setting, select Messages API.
    • Click Save changes.
    • Why? Vonage has multiple APIs for SMS. The Messages API is the modern, unified API that supports multiple channels. Setting it as default ensures consistency and that webhooks use the Messages API format.
  5. Create a Vonage Application:

    • Navigate to Applications -> Create a new application.
    • Give your application a descriptive name (e.g., Node Two-Way SMS App).
    • Click Generate public and private key. This will automatically download a private.key file. Save this file securely. Move it to the root of your project directory (or another secure location). Update VONAGE_PRIVATE_KEY_PATH in your .env file to point to its location (e.g., ./private.key if it's in the project root).
    • Enable the Messages capability.
    • You'll need to enter Inbound URL and Status URL webhooks. For now, you can enter placeholder URLs like http://example.com/webhooks/inbound and http://example.com/webhooks/status. We will update these later with our ngrok URL.
    • Click Generate new application.
    • After creation, you'll see the Application ID. Copy this ID and paste it into your .env file for VONAGE_APPLICATION_ID.
  6. Link Your Number to the Application:

    • Go back to the Applications list and find the application you just created.
    • Click Link next to the Vonage number you purchased earlier.
    • Why? This tells Vonage that any messages received on this specific number should be handled by this application and its configured webhooks.

3. Implementing Outbound SMS (Sending Messages)

Let's add the functionality to send SMS messages via an API endpoint.

  1. Initialize Vonage SDK: In app.js, initialize the Vonage SDK using the credentials from your environment variables. Place this near the top, after require('dotenv').config();.

    javascript
    // app.js
    // ... (dotenv, express requires)
    const { Vonage } = require('@vonage/server-sdk');
    
    // Initialize Vonage
    const vonage = new Vonage({
      apiKey: process.env.VONAGE_API_KEY,
      apiSecret: process.env.VONAGE_API_SECRET,
      // Note: Application ID and Private Key are configured in .env and are crucial
      // for linking the number and ensuring Vonage directs incoming webhooks correctly,
      // even though they might not be explicitly required in this SDK client instance
      // *just* for sending SMS via Key/Secret authentication.
      // applicationId: process.env.VONAGE_APPLICATION_ID,
      // privateKey: process.env.VONAGE_PRIVATE_KEY_PATH
    }, { debug: true }); // Enable debug logging for Vonage SDK
    
    // ... (express app setup)
    • Note on Authentication: We initialize the SDK using the API Key and Secret, which are sufficient for sending SMS via the Messages API as demonstrated in this guide. The Application ID and Private Key (set in .env) are essential for Vonage to correctly associate your number with this application and route incoming message webhooks.
  2. Create Sending Function: It's good practice to encapsulate the sending logic in a function.

    javascript
    // app.js
    // ... (Vonage initialization)
    
    async function sendSms(to, text) {
      const from = process.env.VONAGE_NUMBER; // Your Vonage number from .env
    
      try {
        const resp = await vonage.messages.send({
          message_type: "text",
          text: text,
          to: to, // The recipient's phone number
          from: from, // Your Vonage virtual number
          channel: "sms"
        });
        console.log("Message sent successfully:", resp.message_uuid);
        return { success: true, message_uuid: resp.message_uuid };
      } catch (err) {
        console.error("Error sending SMS:", err.response ? err.response.data : err.message);
        // Log more detail if available from Vonage response
        if (err.response && err.response.data) {
            console.error("Vonage Error Details:", JSON.stringify(err.response.data, null, 2));
        }
        return { success: false, error: err.message };
      }
    }
    
    // ... (express app setup)
    • Why async/await? The vonage.messages.send method is asynchronous (it makes a network request). async/await provides a cleaner way to handle promises compared to .then()/.catch() chains.
    • Parameters Explained:
      • message_type: "text": Specifies a standard text message.
      • text: The content of the SMS.
      • to: The recipient's phone number in E.164 format (e.g., 14155550101).
      • from: Your Vonage virtual number (must be linked to your app if required by regulations).
      • channel: "sms": Explicitly specifies the SMS channel.
  3. Create API Endpoint for Sending: Add a POST route to your Express app to trigger the sendSms function.

    javascript
    // app.js
    // ... (sendSms function)
    
    // API Endpoint to send SMS
    app.post('/send-sms', async (req, res) => {
      const { to, text } = req.body;
    
      if (!to || !text) {
        return res.status(400).json({ error: 'Missing required fields: to, text' });
      }
    
      // Basic validation (can be more robust)
      if (!/^\d+$/.test(to.replace(/^\+/, ''))) {
         return res.status(400).json({ error: 'Invalid "to" phone number format.' });
      }
    
      const result = await sendSms(to, text);
    
      if (result.success) {
        res.status(200).json({ message: 'SMS sent successfully', message_uuid: result.message_uuid });
      } else {
        res.status(500).json({ error: 'Failed to send SMS', details: result.error });
      }
    });
    
    // ... (app.listen)
    • Input Validation: Basic checks are included for required fields (to, text) and a simple format check for the to number. Production apps should have more robust validation.
  4. Test Sending:

    • Restart your Node.js server: node app.js.

    • Open a new terminal window or use a tool like Postman/Insomnia.

    • Send a POST request using curl:

      bash
      curl -X POST http://localhost:3000/send-sms \
      -H "Content-Type: application/json" \
      -d '{
        "to": "YOUR_PERSONAL_PHONE_NUMBER",
        "text": "Hello from Vonage Node App!"
      }'
      • Replace YOUR_PERSONAL_PHONE_NUMBER with your actual mobile number in E.164 format (e.g., 14155550101).
    • You should receive an SMS on your phone, and see success logs in your Node.js console and a JSON response in your curl terminal.

4. Implementing Inbound SMS Webhooks (Receiving Messages)

To receive messages, Vonage needs a publicly accessible URL (a webhook) to send HTTP POST requests to when your Vonage number gets an SMS. We'll use ngrok for local development.

  1. Start ngrok: If your app is running on port 3000 (as configured in .env or defaulted in app.js), run ngrok to expose this port. While ngrok is excellent for local development, for staging or production environments where you have a publicly accessible server, you would use your server's actual public URL instead. Other tunneling services (like localtunnel or cloud provider specific tools) also exist.

    bash
    # Make sure your Node app is running in another terminal: node app.js
    ngrok http 3000

    ngrok will provide a Forwarding URL (e.g., https://<random-string>.ngrok.io). Copy the https version of this URL.

  2. Update Vonage Application Webhooks:

    • Go back to your Vonage Application settings in the dashboard (Applications -> Your App -> Edit).
    • Update the Messages webhooks:
      • Inbound URL: Paste your ngrok https URL, adding /webhooks/inbound at the end (e.g., https://<random-string>.ngrok.io/webhooks/inbound). Set the method to POST.
      • Status URL: Paste your ngrok https URL, adding /webhooks/status at the end (e.g., https://<random-string>.ngrok.io/webhooks/status). Set the method to POST.
    • Click Save changes.
    • Why both?
      • Inbound URL: Receives data about incoming messages (SMS sent to your Vonage number).
      • Status URL: Receives delivery receipts and status updates for outgoing messages (SMS sent from your Vonage number).
  3. Create Webhook Handlers in Express: Add POST routes in app.js to handle requests from Vonage.

    javascript
    // app.js
    // ... (after /send-sms route)
    
    // Webhook endpoint for incoming SMS messages
    app.post('/webhooks/inbound', (req, res) => {
      console.log('--- Inbound Message ---');
      console.log(JSON.stringify(req.body, null, 2)); // Log the full inbound payload
    
      // TODO: Process the inbound message (e.g., reply)
    
      // Vonage needs a 200 OK response to know the webhook is working
      res.status(200).end();
    });
    
    // Webhook endpoint for SMS status updates
    app.post('/webhooks/status', (req, res) => {
      console.log('--- Message Status ---');
      console.log(JSON.stringify(req.body, null, 2)); // Log the status update
    
      // TODO: Process the status update (e.g., track delivery)
    
      // Respond with 200 OK
      res.status(200).end();
    });
    
    // ... (app.listen)
    • Crucial: Always respond with a 200 OK status to Vonage webhooks promptly. If Vonage doesn't receive a 200 OK, it will assume the delivery failed and may retry, leading to duplicate processing.
    • Logging: Logging the full req.body is essential for understanding the data structure Vonage sends.
  4. Test Receiving:

    • Make sure your Node.js app (node app.js) and ngrok (ngrok http 3000) are running.

    • From your personal mobile phone, send an SMS message to your Vonage virtual number (the one in VONAGE_NUMBER).

    • Check your Node.js console. You should see the "--- Inbound Message ---" log followed by a JSON object containing details about the SMS you sent (sender number, text content, timestamp, etc.).

    • Example Inbound Payload (req.body):

      json
      {
        "to": "18335787204",
        "from": "19999999999",
        "channel": "sms",
        "message_uuid": "a580f869-e995-4d76-9b80-a7befe3186a3",
        "timestamp": "2022-12-07T23:04:32Z",
        "usage": {
          "price": "0.0057",
          "currency": "EUR"
        },
        "message_type": "text",
        "text": "Hello Vonage!",
        "sms": {
          "num_messages": "1"
        }
      }
    • You can also inspect the request details in the ngrok web interface, usually accessible at http://127.0.0.1:4040.

5. Building Two-Way SMS Communication (Auto-Reply)

Now, let's connect the inbound handler to the outbound function to create an auto-reply bot.

  1. Modify Inbound Webhook Handler: Update the /webhooks/inbound route to extract the sender's number and message, then call sendSms to reply.

    javascript
    // app.js
    
    // Webhook endpoint for incoming SMS messages
    app.post('/webhooks/inbound', async (req, res) => { // Make the handler async
      console.log('--- Inbound Message ---');
      console.log(JSON.stringify(req.body, null, 2));
    
      const inboundData = req.body;
    
      // Ensure it's an inbound SMS text message
      if (inboundData.channel === 'sms' && inboundData.message_type === 'text') {
        const sender = inboundData.from;
        const messageText = inboundData.text;
    
        console.log(`Received message "${messageText}" from ${sender}`);
    
        // Simple auto-reply logic
        const replyText = `You said: "${messageText}". Thanks for messaging!`;
    
        // Send the reply using the existing sendSms function
        await sendSms(sender, replyText);
      } else {
        console.log('Received non-SMS or non-text message, skipping reply.');
      }
    
      // Always respond with 200 OK
      res.status(200).end();
    });
    
    // ... (rest of the code)
    • Key Changes:
      • The handler is now async to allow awaiting the sendSms call.
      • We extract inboundData.from (the sender's number) and inboundData.text.
      • We call sendSms with the original sender as the new recipient (to).
  2. Test Two-Way Messaging:

    • Restart your Node.js app (node app.js). Ensure ngrok is still running.
    • Send another SMS from your personal phone to your Vonage number.
    • You should see the inbound message logged in your Node console.
    • Shortly after, you should receive an SMS reply on your personal phone like: "You said: "<Your Message>". Thanks for messaging!".
    • Check the Node console again; you should see logs for both the inbound message and the status of the outbound reply message (coming to the /webhooks/status endpoint).

6. Error Handling and Logging

Robust error handling and clear logging are crucial for production applications.

  • Vonage SDK Errors: The try...catch block in sendSms already handles errors from the Vonage API. Logging err.response.data provides specific Vonage error details when available.

  • Webhook Errors:

    • Ensure your webhook endpoints (/webhooks/inbound, /webhooks/status) always return 200 OK. Use try...catch within these handlers if performing complex logic that might fail, but ensure the res.status(200).end() is called even if an internal error occurs during processing.
    • Log any processing errors clearly within the webhook handlers.
  • Enhanced Logging: For production, consider using a structured logging library like winston or pino instead of console.log. This enables better log parsing, filtering, and integration with log management systems.

    bash
    npm install winston
    javascript
    // Example using Winston (replace console.log calls)
    const winston = require('winston');
    
    const logger = winston.createLogger({
      level: 'info',
      format: winston.format.json(),
      transports: [
        // - Write all logs with importance level of `error` or less to `error.log`
        // - Write all logs with importance level of `info` or less to `combined.log`
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' }),
      ],
    });
    
    // If we're not in production then log to the `console`
    if (process.env.NODE_ENV !== 'production') {
      logger.add(new winston.transports.Console({
        format: winston.format.simple(),
      }));
    }
    
    // Replace console.log('...') with logger.info('...')
    // Replace console.error('...') with logger.error('...')
    
    // --- Example Usage ---
    // logger.info('Server started');
    // logger.error('Failed to send SMS', { error: err.message });
    • Placement: This logger setup code should typically be placed early in your application startup, for instance, near the top of your app.js file after requiring modules, or encapsulated within a separate logging module (logger.js) that you import where needed.
  • Retry Mechanisms: Vonage automatically retries sending webhook notifications if it doesn't receive a 200 OK. For outgoing SMS failures (e.g., network issues calling the Vonage API), you might implement your own retry logic within the sendSms function's catch block, potentially using libraries like async-retry with exponential backoff.

7. Security Considerations

  • Environment Variables: Never commit your .env file or hardcode credentials (API Key, Secret, Private Key) directly in your code. Use environment variables managed securely in your deployment environment.

  • Webhook Security (Signature Verification): Vonage can sign its webhook requests, allowing you to verify they genuinely came from Vonage. This prevents attackers from sending fake requests to your endpoints. Implementing signature verification (using JWT or Signed Webhooks based on Vonage settings) is highly recommended for production. Refer to the Vonage Webhook Security Documentation for implementation details using the SDK. The @vonage/server-sdk often includes helper functions or utilities to simplify this verification process; consult the SDK's documentation for specific methods related to webhook signature validation.

  • Input Validation: Sanitize and validate all input, especially the text from incoming SMS messages, if you plan to store it, display it, or use it in further processing (e.g., database queries) to prevent injection attacks. Libraries like express-validator can help.

  • Rate Limiting: Protect your /send-sms endpoint (if publicly exposed) and potentially your webhook endpoints from abuse by implementing rate limiting using middleware like express-rate-limit.

  • Private Key Security: Treat your private.key file like any other secret. Ensure its file permissions are restricted and it's stored securely on your server.

8. Testing Your SMS Application

Comprehensive testing ensures your application works reliably.

  • Manual Testing (as performed above):

    • Use curl/Postman to test the /send-sms endpoint. Verify SMS delivery.
    • Send SMS to your Vonage number. Verify logs and the auto-reply.
  • Automated Testing (Unit/Integration):

    • Unit Tests: Use frameworks like Jest or Mocha/Chai to test individual functions (e.g., the logic inside your webhook handlers, input validation) in isolation. Mock the Vonage SDK to avoid actual API calls during unit tests.
    • Integration Tests: Test the interaction between your API endpoints, the Vonage SDK (potentially mocked or hitting a Vonage sandbox if available), and your internal logic. Tools like supertest are excellent for testing Express routes.

    Before writing tests that require the Express app instance, ensure you export it from your main application file (app.js). Add the following line at the bottom of app.js:

    javascript
    module.exports = app;
    bash
    # Install testing dependencies
    npm install --save-dev jest supertest

    Example Jest/Supertest Integration Test (app.test.js):

    javascript
    // app.test.js
    const request = require('supertest');
    // Assuming your app.js exports the express app instance (see instruction above)
    const app = require('./app'); // Adjust path if needed
    
    // Mock the Vonage SDK to avoid real API calls
    jest.mock('@vonage/server-sdk', () => {
        const mockMessages = {
            send: jest.fn().mockResolvedValue({ message_uuid: 'mock-uuid-12345' })
        };
        return {
            Vonage: jest.fn().mockImplementation(() => ({
                messages: mockMessages
            }))
        };
    });
    
    
    describe('SMS Endpoints', () => {
        it('should return 400 if "to" is missing for /send-sms', async () => {
            const res = await request(app)
                .post('/send-sms')
                .send({ text: 'Test message' });
            expect(res.statusCode).toEqual(400);
            expect(res.body.error).toContain('Missing required fields');
        });
    
        it('should return 200 and mock UUID for valid /send-sms', async () => {
            const res = await request(app)
                .post('/send-sms')
                .send({ to: '15551234567', text: 'Test message' });
            expect(res.statusCode).toEqual(200);
            expect(res.body.message_uuid).toEqual('mock-uuid-12345');
        });
    
        it('should return 200 for /webhooks/inbound POST', async () => {
            const res = await request(app)
                .post('/webhooks/inbound')
                .send({ // Simulate Vonage inbound payload
                   channel: 'sms',
                   message_type: 'text',
                   from: '15559876543',
                   to: process.env.VONAGE_NUMBER || '18885551212', // Use env var or a default test number
                   text: 'Hello inbound test'
                 });
            expect(res.statusCode).toEqual(200);
            // You could add more assertions here to check if the mocked sendSms was called
        });
    
         it('should return 200 for /webhooks/status POST', async () => {
            const res = await request(app)
                .post('/webhooks/status')
                .send({ message_uuid: 'some-uuid', status: 'delivered' });
            expect(res.statusCode).toEqual(200);
        });
    });

    Add to package.json scripts:

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

    Run tests: npm test

9. Deployment

Deploying requires moving your app from your local machine to a server accessible on the internet.

  1. Choose a Hosting Provider: Options include PaaS (Heroku, Render, Fly.io), IaaS (AWS EC2, Google Compute Engine, DigitalOcean Droplets), or Serverless platforms.

  2. Environment Variables: Configure your production environment variables (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, VONAGE_NUMBER, PORT, NODE_ENV=production) securely using your hosting provider's interface. Do not commit .env to production.

  3. Private Key: Securely transfer your private.key file to the production server or use a secrets management service. Ensure the VONAGE_PRIVATE_KEY_PATH environment variable points to the correct location on the server.

  4. Update Vonage Webhooks: Once your application is deployed and has a public URL (e.g., https://your-app-name.herokuapp.com), update the Inbound URL and Status URL in your Vonage Application settings to point to your production endpoints (e.g., https://your-app-name.herokuapp.com/webhooks/inbound). Remove the ngrok URLs.

  5. Build/Deployment Process: Follow your hosting provider's instructions. This typically involves pushing your code to Git, connecting the repository to the provider, and triggering a build/deploy. Ensure npm install --omit=dev (or similar command) is run in production to avoid installing development dependencies.

  6. Process Manager: Use a process manager like pm2 or your platform's built-in mechanism (e.g., Heroku Dynos) to keep your Node.js application running reliably and restart it if it crashes.

    bash
    # Install pm2 globally on the server (if needed)
    npm install pm2 -g
    # Start your app
    pm2 start app.js --name vonage-sms-app
    # Monitor
    pm2 list
    pm2 logs vonage-sms-app
  7. CI/CD (Optional but Recommended): Set up a Continuous Integration/Continuous Deployment pipeline (using GitHub Actions, GitLab CI, Jenkins, etc.) to automatically test and deploy your application whenever you push changes to your repository.

10. Troubleshooting and Caveats

  • SMS Not Sending:

    • Check Node console logs for errors from the Vonage SDK.
    • Verify API Key/Secret in .env are correct.
    • Ensure the to number is in valid E.164 format.
    • Check your Vonage account balance.
    • Confirm the from number (VONAGE_NUMBER) is correct and SMS-capable.
    • Look for error details in the err.response.data if logged.
  • Inbound Webhooks Not Triggering:

    • Verify the Inbound URL in Vonage Application settings is exactly correct (HTTPS, correct path /webhooks/inbound, no typos).
    • Ensure your deployed application is running and accessible at the webhook URL. Check server logs.
    • If using ngrok locally: Ensure ngrok is running and forwarding to the correct local port (3000). Ensure the ngrok URL in Vonage is the current active one (it changes each time you restart ngrok unless you have a paid plan with a static domain). Check the ngrok web interface (http://127.0.0.1:4040) for requests.
    • Verify the Vonage number is correctly linked to the Vonage Application.
    • Check the Vonage Dashboard's API Logs (Logs -> API logs) for errors related to webhook delivery attempts.
  • Receiving Multiple Inbound Messages (Retries):

    • This happens if your webhook doesn't respond with 200 OK quickly enough. Ensure your /webhooks/inbound handler returns res.status(200).end() immediately, even if background processing continues. Offload long-running tasks to a background job queue if necessary.
  • Incorrect Default API: If webhooks have unexpected formats, double-check that the Messages API is set as the default SMS setting in your Vonage account API settings.

Learn more about SMS messaging with Node.js:

Frequently Asked Questions

How to send SMS messages with Node.js and Express

Use the Vonage Messages API and the @vonage/server-sdk. Create an API endpoint in your Express app that takes the recipient's number and message text, then uses the Vonage SDK to send the SMS. Ensure your Vonage number is linked to your Vonage application.

What is the Vonage Messages API?

The Vonage Messages API is a unified API for sending and receiving messages across multiple channels, including SMS. It simplifies communication by providing a single interface for various messaging types.

Why does Vonage need a webhook for inbound SMS?

Vonage uses webhooks to deliver inbound SMS messages to your application in real-time. When someone sends an SMS to your Vonage number, Vonage sends an HTTP POST request to your configured webhook URL with the message details.

When should I use ngrok for Vonage webhooks?

Use ngrok during local development to create a publicly accessible URL that Vonage can use to reach your webhook endpoints. For production, replace the ngrok URL with your deployed application's public URL.

Can I test Vonage SMS sending locally?

Yes, you can test sending SMS locally using the Vonage SDK and your API credentials. Use tools like curl or Postman to make requests to your local Express app's /send-sms endpoint. Replace placeholder numbers with your own for testing.

How to receive SMS messages with Node.js and Express

Set up webhook endpoints in your Express app (e.g., /webhooks/inbound) and configure these URLs in your Vonage Application settings. When an SMS is sent to your Vonage number, Vonage will send an HTTP POST request to your webhook with the message data.

What is a Vonage Application ID?

The Vonage Application ID is a unique identifier for your Vonage application. It's generated when you create an application in the Vonage Dashboard and used to associate your Vonage number and webhook URLs with your application.

Why use dotenv in a Node.js project?

Dotenv is a module that loads environment variables from a .env file into process.env. This is a best practice for managing sensitive credentials (like API keys) and configuration, keeping them out of your codebase.

How to set up two-way SMS messaging with Node.js and Vonage

Combine the send and receive functionalities. In your inbound webhook handler, extract the sender's number and message content. Then, use the Vonage SDK to send an SMS reply back to the sender.

What is the purpose of the Vonage private key?

The Vonage private key, generated along with your Application ID, is used for more secure authentication in certain Vonage API interactions, particularly when using JWT for webhook signatures. This key should be kept confidential and loaded via environment variable from the .env file.

When should I implement webhook signature verification?

Webhook signature verification is highly recommended for production applications. It ensures that webhook requests are genuinely coming from Vonage and prevents malicious actors from spoofing requests. Verify the signatures in your webhook endpoints using JWT (JSON Web Tokens).