code examples

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

Build Two-Way SMS with Vonage Messages API, Node.js & Express (2025)

Complete guide to building production-ready two-way SMS messaging with Vonage Messages API, Node.js, and Express. Covers sending/receiving SMS, webhooks, ngrok setup, security, and deployment.

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

Build a production-ready Node.js SMS application with Express to handle two-way messaging using the Vonage Messages API. Send and receive SMS programmatically through webhooks, enabling customer communication without requiring app installation.

This complete guide covers Vonage Node.js integration, project setup, webhook configuration with ngrok, security best practices, error handling, and production deployment. You'll create a functional SMS messaging system that sends messages via API calls and receives inbound messages through Express webhooks.

Project Overview and Goals

Create a Node.js application that can:

  1. Send outbound SMS messages using the Vonage Messages API.
  2. Receive inbound SMS messages sent to a Vonage virtual number via a webhook.

Problem Solved: Interact with users via SMS – a direct and widely accessible communication method without requiring users to install a separate app.

Technologies Used:

  • Node.js: JavaScript runtime environment for building server-side applications. Recommended: Node.js v22 LTS (Active LTS until October 2025, Maintenance until April 2027).
  • Express: Minimal and flexible Node.js web application framework for creating webhook endpoints. Current version: Express 5.1.0 (default on npm).
  • Vonage Messages API: Unified API from Vonage for sending and receiving messages across various channels (SMS, MMS, WhatsApp, etc.). We'll focus on SMS.
  • @vonage/server-sdk: Official Vonage Node.js SDK for interacting with the Vonage APIs. Current version: v3.24.1.
  • ngrok: Tool to expose local development servers to the internet, necessary for Vonage webhooks to reach your local machine during development. Free tier: 2-hour session limit, random URLs.
  • dotenv: Module to load environment variables from a .env file into process.env.

System Architecture:

text
+-------------+      SMS       +----------------+ Receive SMS Webhook +-----------------+
| User's Phone| <-----------> | Vonage Platform| ----------------> | Your Node.js App|
|             | ------------> | (Messages API) | <---------------- | (Express Server)|
+-------------+               +----------------+ (Send SMS API Call)+-----------------+

The diagram shows a User's Phone communicating bi-directionally with the Vonage Platform via SMS. The Vonage Platform sends incoming messages to Your Node.js App via a Receive SMS Webhook (HTTP POST). Your Node.js App sends outgoing messages via a Send SMS API Call to the Vonage Platform.

Expected Outcome: A running Node.js application that sends SMS messages and logs incoming SMS messages received on a configured Vonage number.

Prerequisites:

  • A Vonage API account. Sign up here if you don't have one.
  • Node.js and npm (or yarn) installed. Download Node.js v22 LTS (recommended for 2025).
  • A Vonage virtual phone number capable of sending/receiving SMS. Purchase one from the Vonage dashboard.
  • ngrok installed. Download ngrok. A free account is sufficient (note: free tier has 2-hour session expiration and random URLs per session).
  • Basic understanding of JavaScript, Node.js, and REST APIs.

1. Set Up Your Node.js SMS Project

Initialize your 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 Node.js Project: Initialize the project using npm. This creates a package.json file.

bash
npm init -y

The -y flag accepts the default settings.

3. Install Dependencies: Install Express for the web server, the Vonage SDK, and dotenv for managing environment variables.

bash
npm install express @vonage/server-sdk dotenv

4. Install Development Dependencies (Optional but Recommended): Install nodemon to automatically restart the server upon file changes, speeding up development.

bash
npm install --save-dev nodemon

5. Configure package.json Scripts: Add scripts to your package.json for easily starting the server:

json
{
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "send-sms": "node src/send-sms.js"
  }
}

6. Create Project Structure: Organize your code for better maintainability.

text
vonage-sms-app/
├── node_modules/
├── src/
│   ├── server.js       # Express server for receiving SMS
│   └── send-sms.js     # Script for sending SMS
├── .env                # Environment variables (API keys, etc.) - DO NOT COMMIT
├── .gitignore          # Files/folders to ignore in Git
├── private.key         # Vonage application private key - DO NOT COMMIT
└── package.json

Create the src directory:

bash
mkdir src

7. Create .gitignore: Prevent committing sensitive information and unnecessary files. Create a .gitignore file in the project root:

plaintext
# .gitignore

# Dependencies
node_modules

# Environment Variables
.env
*.env

# Vonage Private Key
private.key
*.key

# Log files
*.log

# OS generated files
.DS_Store
Thumbs.db

8. Set Up Environment Variables (.env): Create a .env file in the project root. Populate this with credentials obtained from Vonage in the next steps.

dotenv
# .env

# Vonage API Credentials (Obtained from Vonage Dashboard > API Settings)
VONAGE_API_KEY=YOUR_VONAGE_API_KEY
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET

# Vonage Application Credentials (Obtained after creating a Vonage Application)
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key

# Vonage Number (Your purchased virtual number)
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER

# Server Port
PORT=3000

# Recipient Number (For testing sending SMS)
TO_NUMBER=RECIPIENT_PHONE_NUMBER_WITH_COUNTRY_CODE

Using .env keeps sensitive credentials out of your source code, making it more secure and easier to manage different configurations for development, staging, and production. The dotenv package loads these variables into process.env when the application starts.

2. Configure Vonage Messages API Credentials

Configure your Vonage account and obtain the necessary credentials before writing code.

1. Obtain API Key and Secret:

  • Log in to your Vonage API Dashboard.
  • Your API Key and API Secret display prominently on the main dashboard page (API settings).
  • Copy these values and paste them into your .env file for VONAGE_API_KEY and VONAGE_API_SECRET.

2. Create a Vonage Application:

Vonage Applications act as containers for your communication configurations (like webhook URLs) and use a private/public key pair for authenticating certain API calls (like sending messages via the Messages API).

  • Navigate to Applications > Create a new application in the dashboard.
  • Give your application a name (e.g., "Nodejs SMS App").
  • Click Generate public and private key. A private.key file downloads immediately. Save this file in the root of your project directory. Ensure the path in VONAGE_APPLICATION_PRIVATE_KEY_PATH in your .env file matches (./private.key).
  • Enable the Messages capability.
  • Enter placeholder URLs for Inbound URL and Status URL like http://example.com/webhooks/inbound and http://example.com/webhooks/status. Update these later when you set up ngrok and your server.
  • Click Generate new application.
  • Copy the Application ID from the application's page.
  • Paste the Application ID into your .env file for VONAGE_APPLICATION_ID.

3. Link Your Vonage Number:

  • On the same application page, scroll down to Link virtual numbers.
  • Find the Vonage virtual number you purchased earlier and click the Link button next to it. This directs incoming messages for that number to this application's configured webhooks.
  • Copy your Vonage virtual number (including the country code, e.g., 14155550100) and paste it into your .env file for VONAGE_NUMBER.

4. Set Default SMS API:

Vonage has two APIs for SMS: the older SMS API and the newer Messages API. Their webhook formats differ. Since you're using the Messages API SDK (@vonage/server-sdk) for sending, configure your account to send inbound webhooks using the Messages API format for consistency.

  • Navigate to your main Vonage Dashboard Account Settings.
  • Scroll down to API settings.
  • Find the Default SMS Setting.
  • Ensure Messages API is selected. If not, select it and click Save changes.

5. Fill Remaining .env Variables:

  • Set PORT to 3000 (or another port if you prefer).
  • Set TO_NUMBER to your personal mobile phone number (including country code, e.g., 12015550101) for testing sending messages.

Your .env file should now contain your specific credentials.

3. Send SMS Messages with Vonage Node.js SDK

Create the script to send an outbound SMS message using the Vonage Messages API.

File: src/send-sms.js

javascript
// src/send-sms.js

require('dotenv').config(); // Load environment variables from .env file

const { Vonage } = require('@vonage/server-sdk');
const { SMS } = require('@vonage/messages');

// --- Configuration ---
// Ensure all required environment variables are set
const requiredEnvVars = [
  'VONAGE_API_KEY',
  'VONAGE_API_SECRET',
  'VONAGE_APPLICATION_ID',
  'VONAGE_APPLICATION_PRIVATE_KEY_PATH',
  'VONAGE_NUMBER',
  'TO_NUMBER'
];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    console.error(`Error: Environment variable ${envVar} is not set.`);
    process.exit(1); // Exit if essential config is missing
  }
}

const vonageApiKey = process.env.VONAGE_API_KEY;
const vonageApiSecret = process.env.VONAGE_API_SECRET;
const vonageAppId = process.env.VONAGE_APPLICATION_ID;
const vonagePrivateKeyPath = process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH;
const vonageNumber = process.env.VONAGE_NUMBER; // Sender number (your Vonage virtual number)
const toNumber = process.env.TO_NUMBER;       // Recipient number
const messageText = 'Hello from Vonage and Node.js!';

// --- Initialize Vonage Client ---
// We use Application ID and Private Key for authentication with the Messages API
const vonage = new Vonage({
  apiKey: vonageApiKey,             // Optional but good practice
  apiSecret: vonageApiSecret,         // Optional but good practice
  applicationId: vonageAppId,
  privateKey: vonagePrivateKeyPath, // Path to your downloaded private key
});

// --- Send SMS Function ---
async function sendSms() {
  console.log(`Attempting to send SMS from ${vonageNumber} to ${toNumber}...`);

  try {
    const resp = await vonage.messages.send(
      new SMS(
        messageText, // The text message content
        toNumber,    // Recipient's phone number
        vonageNumber // Sender's phone number (Your Vonage virtual number)
      )
    );

    console.log('SMS submitted successfully!');
    console.log('Message UUID:', resp.messageUuid); // Unique identifier for the message

  } catch (error) {
    console.error('Error sending SMS:');
    // Log detailed error information if available
    if (error.response) {
      console.error('Status:', error.response.status);
      console.error('Data:', error.response.data);
    } else {
      console.error(error.message);
    }
    // Consider more robust error handling/logging in production
  }
}

// --- Execute Sending ---
sendSms();

Explanation:

  1. require('dotenv').config();: Loads the variables from your .env file into process.env.
  2. Environment Variable Check: Ensures all necessary credentials and numbers are present before proceeding.
  3. Vonage Initialization: We create a Vonage client instance. Crucially, for the Messages API, authentication relies on the applicationId and privateKey. While apiKey and apiSecret aren't strictly required for this specific send call, including them is good practice as other SDK features might use them.
  4. vonage.messages.send(): This is the core function from the SDK for sending messages via the Messages API.
  5. new SMS(...): We create an SMS message object specifying the text content, recipient (to), and sender (from). The sender must be your Vonage virtual number linked to the Application ID.
  6. async/await and try...catch: We use modern JavaScript asynchronous patterns to handle the API call and implement basic error handling. A successful response includes a messageUuid, which is useful for tracking.
  7. Error Handling: The catch block logs errors. In a production system, you'd integrate this with a proper logging framework (like Winston or Pino) and potentially an error tracking service (like Sentry).

To Test Sending: Run the script from your terminal:

bash
npm run send-sms

You should see output indicating success and receive an SMS on the phone number specified in TO_NUMBER. Check your terminal for any error messages if it fails.

4. Receive Inbound SMS with Express Webhooks

Set up the Express server to listen for incoming SMS messages delivered by Vonage webhooks.

File: src/server.js

javascript
// src/server.js

require('dotenv').config(); // Load environment variables

const express = require('express');
const app = express();

// --- Configuration ---
const port = process.env.PORT || 3000; // Use port from .env or default to 3000

// --- Middleware ---
// Express needs to parse JSON request bodies for Vonage webhooks
app.use(express.json());
// It's also good practice to handle URL-encoded data
app.use(express.urlencoded({ extended: true }));

// --- Webhook Endpoint for Inbound SMS ---
// Vonage will send POST requests to this endpoint when an SMS is received
app.post('/webhooks/inbound', (req, res) => {
  console.log('--- Inbound SMS Received ---');
  console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the entire payload

  // --- Basic Validation (Messages API Format) ---
  // Check for essential fields expected from Vonage Messages API for an inbound SMS
  // Note: We rename 'message-timestamp' to 'timestamp' during destructuring.
  const { msisdn, to, text, messageId, 'message-timestamp': timestamp } = req.body;

  if (!msisdn || !to || !text || !messageId || !timestamp) {
    console.warn('Received incomplete or unexpected webhook payload.');
    // Still send 200 OK so Vonage doesn't retry, but log the issue.
    return res.status(200).end();
  }

  // --- Process the Message ---
  console.log(`From: ${msisdn}`);      // Sender's phone number
  console.log(`To: ${to}`);          // Your Vonage number
  console.log(`Text: ${text}`);        // Message content
  console.log(`Message ID: ${messageId}`); // Vonage's message identifier
  console.log(`Timestamp: ${timestamp}`);  // Time message was received by Vonage

  // --- !!! IMPORTANT: Acknowledge the Webhook Immediately !!! ---
  // Vonage expects a 200 OK response quickly.
  // If it doesn't receive one, it will retry sending the webhook,
  // potentially leading to duplicate processing.
  // Perform any time-consuming logic *after* sending the response.
  res.status(200).end();

  // --- Further Processing (Example) ---
  // Here you would typically:
  // 1. Store the message in a database.
  // 2. Trigger business logic (e.g., check keywords, route to support).
  // 3. Potentially send a reply SMS using the `sendSms` logic from send-sms.js
  //    (You would refactor sendSms into a reusable function).
  console.log('Webhook acknowledged. Further processing can happen here.');

});

// --- Optional: Status Webhook Endpoint ---
// Vonage sends delivery receipts and other status updates here
app.post('/webhooks/status', (req, res) => {
  console.log('--- Message Status Received ---');
  console.log('Request Body:', JSON.stringify(req.body, null, 2));

  // Process status updates (e.g., update message status in database)
  // Example: Check for delivered, failed, etc.

  res.status(200).end(); // Acknowledge receipt
});

// --- Basic Health Check Endpoint ---
app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

// --- Start Server ---
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
  console.log(`Webhook endpoints:`);
  console.log(`  Inbound SMS: POST /webhooks/inbound`);
  console.log(`  Status:      POST /webhooks/status`);
  console.log('Waiting for Vonage webhooks...');
});

Explanation:

  1. Middleware (express.json, express.urlencoded): Essential for parsing the incoming request body from Vonage, which is typically JSON.
  2. /webhooks/inbound Endpoint (POST): This is the route Vonage will call when an SMS arrives at your linked number.
  3. Logging: We log the entire request body to see the structure of the data Vonage sends (useful for debugging). The expected format depends on the Default SMS Setting in your Vonage account (we set it to Messages API).
  4. Basic Validation: We check for key fields expected in the Messages API inbound format (msisdn for sender, to for recipient, text, messageId, message-timestamp). We use destructuring with renaming ('message-timestamp': timestamp) to assign the timestamp value to a variable named timestamp. Adjust these fields if using the older SMS API format.
  5. Processing: We extract and log the relevant information. This is where your application's core logic for handling inbound messages would go.
  6. Crucial: res.status(200).end(): Vonage requires a quick 200 OK response to know the webhook was received successfully. Send this response before doing any lengthy processing (like database writes or external API calls) to avoid timeouts and retries from Vonage.
  7. /webhooks/status Endpoint (POST): Optional but recommended. Vonage sends delivery receipts (DLRs) and other status updates about outbound messages to this URL if configured.
  8. /health Endpoint (GET): A simple endpoint often used by monitoring systems to check if the application is running.
  9. app.listen: Starts the Express server on the configured port.

5. Expose Local Server with ngrok for Webhook Testing

Vonage needs a publicly accessible URL to send webhooks. During development, ngrok creates a secure tunnel to your local machine.

Important: The ngrok free tier has limitations:

  • Session Duration: 2-hour maximum – connections expire after 2 hours and require restarting ngrok
  • URL Changes: Each session generates a new random URL, requiring webhook URL updates in Vonage dashboard
  • Browser Warning: Free tier injects an interstitial warning page for HTML browser traffic (doesn't affect API webhooks)

For production or longer development sessions, consider ngrok paid plans starting at $8/month for stable URLs and unlimited session duration.

1. Start Your Local Server: If it's not already running, start your Express server:

bash
npm run dev

Using nodemon automatically restarts on changes. You should see "Server listening on port 3000...".

2. Start ngrok: Open another terminal window/tab (leave the server running) and run:

bash
ngrok http 3000

Replace 3000 if you used a different port.

3. Get ngrok URL: ngrok will display output similar to this:

text
ngrok by @inconshreveable                                         (Ctrl+C to quit)

Session Status                online
Account                       Your Name (Plan: Free)
Version                       x.x.x
Region                        United States (us-east-1)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://<random-string>.ngrok.io -> http://localhost:3000
Forwarding                    https://<random-string>.ngrok.io -> http://localhost:3000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00
  • Copy the https://<random-string>.ngrok.io URL. This is your public URL. Note: Free tier URLs are temporary and change every 2 hours or when you restart ngrok.
  • The Web Interface (http://127.0.0.1:4040) is a useful dashboard to inspect webhook requests ngrok receives.

4. Update Vonage Application Webhooks:

  • Go back to your Vonage Application settings in the dashboard (Applications).
  • Click Edit on the application you created earlier.
  • Update the webhook URLs:
    • Inbound URL: Paste your https://<random-string>.ngrok.io URL and append /webhooks/inbound. Example: https://a1b2c3d4e5.ngrok.io/webhooks/inbound
    • Status URL: Paste your https://<random-string>.ngrok.io URL and append /webhooks/status. Example: https://a1b2c3d4e5.ngrok.io/webhooks/status
  • Ensure the HTTP Method for both is set to POST.
  • Click Save changes.

6. Test Your Two-Way SMS Application

Now, let's test the complete two-way flow.

1. Ensure Services are Running:

  • Your Node.js server (npm run dev) is running in one terminal.
  • ngrok http 3000 is running in another terminal.

2. Test Inbound SMS:

  • Using your personal mobile phone, send an SMS message to your Vonage virtual number (the one stored in VONAGE_NUMBER).
  • Observe the terminal window where your Node.js server is running. You should see log output similar to:
text
--- Inbound SMS Received ---
Request Body: {
  "msisdn": "12015550101",
  "to": "14155550100",
  "messageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "text": "Hello from my phone!",
  "type": "text",
  "keyword": "HELLO",
  "message-timestamp": "2025-04-20T10:30:00Z"
}
From: 12015550101
To: 14155550100
Text: Hello from my phone!
Message ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Timestamp: 2025-04-20T10:30:00Z
Webhook acknowledged. Further processing can happen here.
  • You can also check the ngrok web interface (http://127.0.0.1:4040) to inspect the raw HTTP request received from Vonage.

3. Test Outbound SMS (Again):

  • Run the sending script again:
bash
npm run send-sms
  • You should receive the SMS on your TO_NUMBER phone.
  • Check the status webhook logs in your server terminal (if you sent the message while the server was running with the status webhook configured). You might see delivery updates coming into the /webhooks/status endpoint.

7. Implement Security for Production SMS Webhooks

While this guide covers basics, production applications require robust security.

  • Input Validation/Sanitization: Never trust data received in webhooks. Use libraries like express-validator or joi to validate the structure and content of req.body before processing. Sanitize any data before storing it in a database or displaying it to prevent XSS attacks.
  • Webhook Signature Verification (Highly Recommended): Vonage signs webhook requests using JWT (JSON Web Tokens). Verifying signatures ensures requests genuinely came from Vonage and weren't tampered with. The Vonage Messages API uses HMAC-SHA256 Bearer Authorization – the JWT signature can be decoded and verified using your signing secret. Implementation details:
    • Extract the Authorization header from the incoming webhook request
    • Decode the JWT using your application's signing secret (derived from your private key)
    • Verify the payload_hash (SHA-256 hash of the webhook payload) matches the computed hash
    • Consult the Vonage Signed Webhooks documentation for complete implementation
  • Rate Limiting: Protect your webhook endpoints from abuse or accidental loops by implementing rate limiting. Libraries like express-rate-limit can restrict the number of requests from a single IP address or based on other criteria.
  • Environment Variable Security: Never commit .env files or private keys to version control (.gitignore helps). Use secure methods provided by your deployment platform (e.g., environment variable settings in Heroku, AWS Secrets Manager, etc.) to manage secrets in production.
  • HTTPS: Always use HTTPS for your webhook URLs in production (ngrok provides this, and deployment platforms typically handle SSL termination).

8. Handle Errors and Implement Logging

Production systems need better error handling and logging than simple console.log.

  • Consistent Error Handling: Implement a centralized error handling strategy in Express using error-handling middleware. Log errors with sufficient detail (stack trace, request context).
  • Structured Logging: Use libraries like winston or pino for structured logging (e.g., JSON format). This makes logs easier to parse, filter, and analyze in log aggregation tools (like Datadog, Splunk, ELK stack). Include request IDs to trace requests across services.
  • Vonage Retries: Remember that Vonage will retry sending webhooks if it doesn't receive a 200 OK quickly. Your application logic must be idempotent (safe to run multiple times with the same input) or have mechanisms to detect and handle duplicate webhook deliveries (e.g., by checking the messageId against recently processed messages in your database).
  • Outbound API Call Retries: Network issues can cause API calls to Vonage (like sendSms) to fail. Implement a retry strategy with exponential backoff for transient network errors when sending messages. Libraries like axios-retry (if using axios directly) or custom logic can handle this.

9. Design Your SMS Database Schema

For any real application, you'll want to store message history.

Conceptual Schema (messages table):

  • id (Primary Key, e.g., UUID or Auto-incrementing Integer)
  • vonage_message_uuid (VARCHAR, Unique) - Store the messageId or messageUuid from Vonage. Crucial for tracking and preventing duplicates.
  • direction (ENUM('inbound', 'outbound')) - Track message flow.
  • sender (VARCHAR) - Phone number of the sender.
  • recipient (VARCHAR) - Phone number of the recipient.
  • body (TEXT) - The message content.
  • status (VARCHAR, e.g., 'submitted', 'delivered', 'failed', 'received') - Track message status (updated via status webhook for outbound).
  • vonage_timestamp (TIMESTAMP WITH TIME ZONE) - Timestamp from Vonage webhook (message-timestamp).
  • created_at (TIMESTAMP WITH TIME ZONE) - When the record was created in your system.
  • updated_at (TIMESTAMP WITH TIME ZONE) - When the record was last updated.

Implementation:

  • Use an ORM (Object-Relational Mapper) like Sequelize, Prisma, or TypeORM to interact with your chosen database (PostgreSQL, MySQL, etc.).
  • Implement functions to insert inbound messages and insert/update outbound messages based on status webhooks.
  • Ensure database queries are indexed appropriately (especially on vonage_message_uuid).

10. Deploy Your SMS Application to Production

Deploying your Node.js application involves several steps.

General Steps:

  1. Choose a Platform: Heroku, AWS (EC2, Lambda, Elastic Beanstalk), Google Cloud (App Engine, Cloud Run), DigitalOcean App Platform, Vercel, Netlify (for serverless functions), etc.
  2. Configure Environment Variables: Securely provide your VONAGE_* credentials, PORT, and database connection strings to the production environment. Do not deploy your .env file.
  3. Build Step (If applicable): If using TypeScript or a build process, run the build command.
  4. Install Dependencies: Run npm install --production on the server to install only necessary dependencies.
  5. Start the Application: Use a process manager like pm2 to run your Node.js application (node src/server.js). pm2 handles restarts, clustering (for multi-core utilization), and log management. Example: pm2 start src/server.js --name vonage-sms-app.
  6. Update Vonage Webhooks: Once deployed, update the Inbound and Status URLs in your Vonage Application settings to point to your production server's public URL (e.g., https://your-app-domain.com/webhooks/inbound).

CI/CD Pipeline:

  • Set up a pipeline (using GitHub Actions, GitLab CI, Jenkins, etc.) to automate:
    • Linting/Formatting: Check code style (eslint, prettier).
    • Testing: Run unit and integration tests (jest, mocha).
    • Building: Create a production build (if needed).
    • Deploying: Push the code/build artifact to your hosting platform.
  • Implement rollback procedures in case a deployment fails.

11. Troubleshoot Common Vonage SMS Issues

  • Ngrok Session Expired: Free ngrok URLs are temporary. You'll need to restart ngrok and update Vonage webhooks if your session expires or you restart your computer.
  • Incorrect Webhook URL/Method: Ensure URLs in Vonage match your server routes exactly (/webhooks/inbound, /webhooks/status) and the method is POST.
  • Firewall Issues: Firewalls (local or network) might block incoming connections from Vonage or outgoing connections from ngrok.
  • Missing 200 OK Response: If your server doesn't respond quickly with 200 OK to webhooks, Vonage will retry, causing duplicate processing. Ensure res.status(200).end() is called promptly.
  • Invalid Credentials/Keys: Double-check API Key/Secret, Application ID, and the path to your private.key. Ensure the key file has the correct read permissions for your Node.js process.
  • Number Not Linked: Make sure the Vonage number is correctly linked to the Vonage Application in the dashboard.
  • Messages API vs SMS API Mismatch: Ensure your Vonage account default setting matches the webhook format your code expects (we used Messages API). If you see unexpected webhook body formats, check this setting. The key timestamp field is message-timestamp in the Messages API webhook.
  • Character Encoding/Limits: Standard SMS are limited (160 GSM-7 characters). Longer messages or those with non-GSM characters (like many emojis) become multi-part Unicode messages, affecting pricing and delivery. The Messages API handles this, but be aware of potential length issues.
  • Sender ID Rules: Regulations vary by country. Some countries require pre-registration for alphanumeric sender IDs, while others (like the US) typically require using a virtual number as the sender ID.
  • Message Delivery Delays: SMS delivery is not always instantaneous and can be subject to carrier delays. Use Status webhooks to track final delivery.

12. Access Complete Code Repository

A repository containing the complete code for this guide can be found here:

https://github.com/sent-dev/vonage-node-express-sms-guide

Frequently Asked Questions About Vonage SMS Integration

How do I send SMS with Vonage Messages API?

Use the @vonage/server-sdk package with your Application ID and private key. Initialize the Vonage client, create an SMS object with recipient, sender, and message text, then call vonage.messages.send(). The API returns a message UUID for tracking.

What's the difference between Vonage Messages API and SMS API?

The Messages API is Vonage's newer unified API supporting SMS, MMS, WhatsApp, and other channels with consistent webhook formats. The older SMS API only handles SMS. Use Messages API for new projects – it offers better integration and future-proofs your application for multi-channel messaging.

How do I secure Vonage webhooks in production?

Implement JWT signature verification using the Authorization header sent by Vonage. Decode the JWT with your signing secret and verify the payload_hash (SHA-256 hash). Add rate limiting with express-rate-limit, validate all input with libraries like joi, and always use HTTPS for webhook URLs.

Why do I need ngrok for local Vonage webhook development?

Vonage webhooks require a publicly accessible HTTPS URL to deliver inbound messages. ngrok creates a secure tunnel from the internet to your local development server. The free tier provides 2-hour sessions with random URLs – sufficient for development but requiring webhook URL updates when restarting.

How much does it cost to send SMS with Vonage?

Vonage SMS pricing varies by destination country and message type. Costs typically range from $0.0035 to $0.10 per message depending on the recipient's location. Check the Vonage pricing page for current rates. Multi-part or Unicode messages may incur additional charges.

Can I use Vonage to send SMS internationally?

Yes, Vonage supports international SMS to 200+ countries. You'll need to purchase a virtual number appropriate for your sending region and ensure your account has sufficient credits. Some countries have specific sender ID requirements or restrictions – consult Vonage documentation for country-specific regulations.

How do I handle duplicate webhook deliveries from Vonage?

Vonage retries webhooks if it doesn't receive a 200 OK response quickly. Make your webhook processing idempotent by checking the messageId against recently processed messages in your database before taking action. Always send the 200 OK response immediately, then process the message asynchronously.

What Node.js version should I use with Vonage SDK?

Use Node.js v22 LTS (recommended for 2025) or v20 LTS minimum. The @vonage/server-sdk v3.24.1 supports these LTS versions and provides the most stable experience with long-term support until 2027.

Next Steps for Your Vonage SMS Application

Now that you understand Vonage two-way SMS messaging, you can:

  1. Enhance webhook security with JWT signature verification and rate limiting
  2. Add database persistence using PostgreSQL, MySQL, or MongoDB with an ORM like Prisma
  3. Implement conversation threading to track SMS exchanges between users and your application
  4. Set up monitoring with tools like Datadog, New Relic, or Sentry for production error tracking
  5. Scale horizontally using load balancers and multiple server instances with shared session storage
  6. Extend to multi-channel by adding WhatsApp, Facebook Messenger, or Viber using the same Messages API

For advanced features, explore the official Vonage Messages API documentation and join the Vonage Community Slack for support.


This guide provides a solid foundation for building two-way SMS interactions using Node.js, Express, and Vonage. Layer in robust security, error handling, logging, and testing as you move towards production deployment. Consult the official Vonage Messages API documentation for more advanced features and details.

Frequently Asked Questions

How to send SMS messages with Node.js and Vonage

Use the `@vonage/server-sdk` and the `messages.send()` method with your API credentials, application ID, private key, Vonage virtual number, and the recipient's number. The sender's number must be your Vonage virtual number linked to the application. This method handles the API call and provides a message UUID for tracking.

What is the Vonage Messages API used for

The Vonage Messages API is a unified API for sending and receiving messages across multiple channels like SMS, MMS, and WhatsApp. This tutorial focuses on using it for two-way SMS communication, enabling features such as notifications, customer support, and reminders.

Why does Vonage need a webhook for receiving SMS

Vonage uses webhooks to deliver incoming SMS messages to your application in real-time. When a message is sent to your Vonage virtual number, Vonage sends an HTTP POST request to your specified webhook URL containing the message details. Your server needs to listen on this URL.

When should I use the Vonage Application ID and private key

The Vonage Application ID and private key are essential for authenticating with the Messages API, specifically when sending SMS messages or configuring webhooks. They ensure secure communication between your app and the Vonage platform.

Can I receive SMS messages on my local machine during development

Yes, by using ngrok, you can create a secure tunnel to your local development server, providing a public URL that Vonage can use to deliver webhooks. Make sure to update the inbound webhook URL in your Vonage application settings.

How to set up a Vonage webhook with Express

Create a POST route (e.g., '/webhooks/inbound') in your Express app. Vonage will send JSON data to this endpoint. Your code should parse this JSON, process the message details, and immediately respond with a 200 OK status. Any time-consuming operations should occur *after* acknowledging the webhook.

What is the purpose of the .env file in a Node.js project

The `.env` file stores environment variables, such as API keys, secrets, and configuration settings, keeping them separate from your code. The `dotenv` package loads these variables into `process.env`.

How to handle Vonage webhook retries in Node.js

Vonage retries webhooks if it doesn't receive a quick 200 OK. Implement idempotent logic or use the `messageId` to track processed messages, preventing duplicate actions if the webhook is delivered multiple times. Store the `messageId` in your database.

What are important security considerations for Vonage SMS webhooks

Implement input validation and sanitization, verify webhook signatures, and use rate limiting to protect against abuse. Ensure HTTPS is used for webhooks, and never expose your API secrets or private keys in version control.

How to link a Vonage virtual number to an application

In the Vonage dashboard, navigate to your application settings. Under 'Link virtual numbers', find your purchased number and click 'Link'. This directs incoming messages to the application's webhooks.

What data does Vonage send in an inbound SMS webhook

The Vonage Messages API sends a JSON payload containing the sender's number (`msisdn`), recipient number (`to`), message content (`text`), message ID (`messageId`), timestamp (`message-timestamp`), and other metadata. Ensure your 'Default SMS Setting' is set to Messages API in the Vonage dashboard.

How to troubleshoot Vonage inbound SMS not reaching my server

Check your ngrok tunnel is active, verify the webhook URL in the Vonage dashboard matches your server route, confirm the HTTP method is POST, and ensure your server responds with 200 OK quickly. Check for firewall issues and correct credentials in your `.env` file.