code examples

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

Building Two-Way SMS Messaging with Vonage Messages API, Node.js, and Express

Build production-ready two-way SMS with Vonage Messages API, Node.js, and Express. Covers JWT authentication, E.164 validation, webhooks, error handling, and security best practices.

Last Updated: October 5, 2025

This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to handle two-way SMS (Short Message Service) messaging via the Vonage Messages API (Application Programming Interface). You'll learn how to send outbound SMS messages and receive inbound messages through webhooks, enabling interactive communication.

Node.js Version Requirement: Node.js 18.x LTS (Long-Term Support) – Active LTS or Node.js 20.x LTS – Maintenance LTS or Node.js 22.x LTS – Active LTS. Node.js 24 entered Current status on May 6, 2025. Production applications should use Active LTS or Maintenance LTS releases.

Source: Node.js Release Schedule (nodejs.org/en/about/previous-releases, verified October 2025)

By the end of this tutorial, you will have a functional Node.js server capable of:

  1. Sending SMS messages programmatically using the Vonage Node.js SDK (Software Development Kit) (@vonage/server-sdk).
  2. Receiving incoming SMS messages sent to your Vonage virtual number via webhooks.
  3. Automatically replying to incoming SMS messages.
  4. Handling message delivery status updates.

This guide focuses on a practical setup, outlining steps towards production readiness. Essential security features for production, such as webhook signature verification, are discussed later as crucial additions to the basic implementation shown here.

Project overview and goals

Create a simple web service that leverages Vonage's communication capabilities to facilitate two-way SMS conversations. This solves the common need for applications to interact with users via SMS for notifications, alerts, customer support, or simple command processing.

Technologies used:

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Express: A minimal and flexible Node.js web application framework used to create API endpoints (webhooks).
  • Vonage Messages API: A unified API for sending and receiving messages across various channels (focus on SMS).
  • Vonage Node.js SDK (@vonage/server-sdk): Simplifies interaction with Vonage APIs.
  • dotenv: A module to load environment variables from a .env file.
  • ngrok: A tool to expose your local development server to the internet, allowing Vonage webhooks to reach it.

System architecture:

text
+-------------+      +-------------------+      +--------+      +---------------------+      +-------+
| User's Phone| ---- | Carrier Network   | ---- | Vonage | ---- | Your Node.js/Express| ---- | User  |
| (Sends SMS) |      |                   |      | (SMS)  |      | App (Webhook)       |      |       |
+-------------+      +-------------------+      +--------+      +---------------------+      +-------+
      ^                                             |                     |                      ^
      | (Receives SMS)                              | (Sends SMS)         | (Sends Reply)        | (Triggers Send)
      +---------------------------------------------+---------------------+----------------------+
  1. Outbound: Your application uses the Vonage SDK to send an SMS message via the Vonage platform to the user's phone.
  2. Inbound: A user sends an SMS to your Vonage virtual number. Vonage forwards this message via an HTTP POST request (webhook) to your application's designated endpoint.
  3. Reply: Your application receives the inbound webhook, processes the message, and can optionally use the Vonage SDK again to send a reply back to the user's phone number.

Prerequisites:

  • Node.js and npm (or yarn): Node.js 18.x LTS, 20.x LTS, or 22.x LTS installed on your system. Download Node.js
  • Vonage API Account: A free account is sufficient to start. Sign up for Vonage.
  • Vonage CLI (Command-Line Interface): Installed globally (npm install -g @vonage/cli).
  • ngrok: Installed and authenticated. Download ngrok. A free account is sufficient. Note: Free ngrok sessions expire after 2 hours and generate new URLs on restart, requiring webhook URL updates in Vonage dashboard.
  • A text editor or IDE (Integrated Development Environment), e.g., VS Code.
  • Basic understanding of JavaScript and Node.js concepts.
  • Phone Number Format: All phone numbers must be in E.164 format (ITU-T Recommendation E.164: 7 – 15 digits, country code first, no leading + or 00 in API requests). Example: 447700900000 for UK, 14155550100 for US.

Source: Vonage Messages API v1.0 documentation (developer.vonage.com/en/api/messages-olympus, verified October 2025)

1. Setting up the project

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

  1. Create Project Directory: Open your terminal or command prompt 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. The -y flag accepts default settings.

    bash
    npm init -y

    This creates a package.json file.

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

    bash
    npm install express @vonage/server-sdk dotenv
  4. Set up Environment Variables: Create a file named .env in the root of your project directory. This file will store sensitive credentials and configuration. Never commit this file to version control.

    Code
    # .env
    # Vonage API Credentials (Optional for Messages API w/ Private Key, but potentially useful)
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Vonage Application Credentials (Required for Messages API)
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
    VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root where script is run
    
    # Vonage Virtual Number (Purchase this via Dashboard or CLI)
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
    
    # Application Port
    APP_PORT=3000
    • VONAGE_API_KEY, VONAGE_API_SECRET: Found directly on your Vonage API Dashboard. While the Messages API primarily uses the Application ID and Private Key for authentication, providing the API Key and Secret here allows the SDK to potentially perform other account-level actions if needed. They are generally not strictly required for sending/receiving messages when using Application authentication.
    • VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH: Required for the Messages API. We'll generate these shortly. The Application ID uniquely identifies your Vonage application configuration. The private key authenticates requests specific to this application. The VONAGE_PRIVATE_KEY_PATH should be the path to the key file relative to the directory where you run your Node.js script (typically the project root).
    • VONAGE_NUMBER: The virtual phone number you rent from Vonage, capable of sending/receiving SMS. We'll acquire this next.
    • APP_PORT: The local port your Express server will listen on.
  5. Configure .gitignore: Create a .gitignore file in the project root to prevent committing sensitive files and unnecessary directories.

    Code
    # .gitignore
    node_modules
    .env
    private.key
    npm-debug.log
    *.log
  6. Acquire Vonage Credentials and Number:

    • API Key and Secret: Log in to your Vonage API Dashboard. Your API Key and Secret are displayed at the top. Copy these into your .env file if you wish to include them.

    • Set Default SMS API: Crucially, navigate to Account Settings in the Vonage Dashboard. Scroll down to ""API Keys"" settings, find ""Default SMS Setting"" and ensure Messages API is selected. Save changes. This ensures webhooks use the Messages API format.

    • Purchase a Virtual Number: You need a Vonage number to send and receive SMS. You can buy one via the dashboard (Numbers -> Buy Numbers) or using the Vonage CLI (ensure you are logged in via vonage login first):

      bash
      # Replace XX with the desired 2-letter country code (e.g., US, GB, CA)
      vonage numbers:buy --country XX --features SMS --confirm

      Copy the purchased number (including the country code, e.g., 14155550100) into the VONAGE_NUMBER field in your .env file.

  7. Create a Vonage Application: The Messages API requires a Vonage Application to associate configuration (like webhook URLs) and authentication (via public/private keys).

    • Go to your Vonage Dashboard and click ""Create a new application"".
    • Give your application a meaningful name (e.g., ""Node Two-Way SMS App"").
    • Click ""Generate public and private key"". Immediately save the private.key file that downloads into the root of your project directory (vonage-sms-app/private.key). The public key is stored by Vonage.
    • Enable the Messages capability.
    • You'll see fields for ""Inbound URL"" and ""Status URL"". We need ngrok running first to fill these. Leave them blank for now, but keep this page open or note down the Application ID generated on this page.
    • Scroll down to ""Link virtual numbers"" and link the Vonage number you purchased earlier to this application.
    • Click ""Create application"".
    • Copy the generated Application ID into the VONAGE_APPLICATION_ID field in your .env file.
  8. Expose Local Server with ngrok: To allow Vonage's servers to send webhook events (like incoming messages) to your local machine during development, we use ngrok.

    • Open a new terminal window/tab (keep your project terminal open).

    • Run ngrok, telling it to forward traffic to the port your application will run on (defined in .env as APP_PORT=3000).

      bash
      ngrok http 3000
    • ngrok will display output including a Forwarding URL (e.g., https://<random-string>.ngrok-free.app). Copy the https version of this URL. This is your public base URL.

  9. Configure Webhook URLs in Vonage Application:

    • Go back to the Vonage Application you created (Dashboard -> Applications -> Your App Name -> Edit).
    • Paste your ngrok Forwarding URL into the Inbound URL field and append /webhooks/inbound. Example: https://<random-string>.ngrok-free.app/webhooks/inbound
    • Paste your ngrok Forwarding URL into the Status URL field and append /webhooks/status. Example: https://<random-string>.ngrok-free.app/webhooks/status
    • Click ""Save changes"".

Now your Vonage application is configured to send incoming SMS messages and status updates to your (soon-to-be-running) local server via the ngrok tunnel.

2. Implementing core functionality: Sending and receiving SMS

Let's write the Node.js code using Express.

  1. Create Server File: Create a file named server.js in your project root.

  2. Initialize Server and Vonage Client: Add the following code to server.js to set up the Express server, load environment variables, and initialize the Vonage client.

    javascript
    // server.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    const { Vonage } = require('@vonage/server-sdk');
    
    // --- Configuration ---
    const app = express();
    const port = process.env.APP_PORT || 3000; // Use port from .env or default to 3000
    
    // --- Middleware ---
    // Parse incoming requests with JSON payloads
    app.use(express.json());
    // Parse incoming requests with URL-encoded payloads
    app.use(express.urlencoded({ extended: true }));
    
    // --- Vonage Client Initialization ---
    // Use Application ID and Private Key for Messages API authentication
    const vonage = new Vonage({
        apiKey: process.env.VONAGE_API_KEY, // Optional: For potential other SDK uses
        apiSecret: process.env.VONAGE_API_SECRET, // Optional: For potential other SDK uses
        applicationId: process.env.VONAGE_APPLICATION_ID,
        privateKey: process.env.VONAGE_PRIVATE_KEY_PATH // SDK handles loading the key from this path
    }, {
        // Optional: Add custom logger or other options here
        // logger: customLogger
    });
    
    // --- Helper Function for Sending SMS ---
    async function sendSms(to, text) {
        // Basic validation
        if (!to || !text) {
            console.error('Send SMS Error: Missing "to" or "text" parameter.');
            return; // Avoid sending invalid requests
        }
        if (!process.env.VONAGE_NUMBER) {
             console.error('Send SMS Error: VONAGE_NUMBER environment variable not set.');
            return;
        }
    
        // E.164 format validation: remove + prefix if present, verify 7-15 digits
        const cleanNumber = to.replace(/^\+/, '');
        if (!/^\d{7,15}$/.test(cleanNumber)) {
            console.error(`Send SMS Error: Invalid E.164 format for number "${to}". Must be 7-15 digits (country code + number), no spaces or special characters.`);
            return;
        }
    
        try {
            const resp = await vonage.messages.send({
                channel: 'sms',
                message_type: 'text',
                to: cleanNumber,                    // Recipient phone number (E.164 without + prefix)
                from: process.env.VONAGE_NUMBER.replace(/^\+/, ''),// Your Vonage virtual number (E.164 without + prefix)
                text: text                          // The message content
            });
            console.log(`SMS sent successfully to ${to}. Message UUID: ${resp.message_uuid}`);
            return resp; // Return the response object if needed
        } catch (err) {
            console.error(`Error sending SMS to ${to}:`, err?.response?.data || err.message || err);
            // More specific error handling based on err.response.status might be needed
            // e.g., if (err.response.status === 401) { handle auth error }
        }
    }
    
    // --- Webhook Endpoints ---
    
    // Inbound SMS Webhook
    app.post('/webhooks/inbound', (req, res) => {
        console.log('--- Inbound SMS Received ---');
        console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the full payload
    
        const { from, message } = req.body;
    
        // Basic validation of expected structure
        if (!from?.number || !message?.content?.text) {
             console.warn('Received incomplete inbound message payload.');
             // Still send 200 OK to prevent Vonage retries for malformed requests
             res.status(200).send('OK');
             return;
        }
    
        const senderNumber = from.number;
        const receivedText = message.content.text;
    
        console.log(`Message received from ${senderNumber}: ""${receivedText}""`);
    
        // --- Two-Way Logic: Respond to the incoming message ---
        const replyText = `Thanks for your message: ""${receivedText}"". We received it!`;
        console.log(`Sending reply to ${senderNumber}: ""${replyText}""`);
    
        // Send the reply asynchronously (don't wait for it before responding to Vonage)
        sendSms(senderNumber, replyText)
            .then(() => console.log(`Reply initiated to ${senderNumber}`))
            .catch(err => console.error(`Failed to initiate reply to ${senderNumber}:`, err)); // Log reply failure separately
    
        // --- IMPORTANT: Acknowledge receipt to Vonage ---
        // Vonage expects a 200 OK response quickly to know the webhook was received.
        // Failure to respond promptly will result in retries.
        res.status(200).send('OK');
    });
    
    // Message Status Webhook
    app.post('/webhooks/status', (req, res) => {
        console.log('--- Message Status Update Received ---');
        console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the full payload
    
        const { message_uuid, status, timestamp, to, error } = req.body;
    
        console.log(`Status for message ${message_uuid} to ${to}: ${status} at ${timestamp}`);
    
        if (error) {
            console.error(`Error details: Code ${error.code}, Reason: ${error.reason}`);
            // Application-specific logic can be added here based on status
            // e.g., update database record for the message_uuid
        }
    
        // Acknowledge receipt to Vonage
        res.status(200).send('OK');
    });
    
    // --- Root Endpoint (Optional: for basic testing) ---
    app.get('/', (req, res) => {
        res.send(`Vonage SMS App is running! Send an SMS to ${process.env.VONAGE_NUMBER} to test.`);
    });
    
    // --- Start Server ---
    app.listen(port, () => {
        const now = new Date();
        const dateStr = now.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
        const timeStr = now.toLocaleTimeString();
    
        console.log(`Server listening at http://localhost:${port}`);
        console.log(`Vonage number: ${process.env.VONAGE_NUMBER || 'Not Set'}`);
        console.log(`Webhook endpoints configured for ngrok URL (ensure ngrok is running on port ${port}):`);
        console.log(`  Inbound: POST /webhooks/inbound`);
        console.log(`  Status:  POST /webhooks/status`);
        console.log(`(${dateStr} ${timeStr}) - Ready to receive messages.`);
    });
    
    // --- Example: Sending an initial SMS on startup (for testing) ---
    // Comment this out for production deployment if not needed
    /*
    const testRecipient = 'REPLACE_WITH_YOUR_PERSONAL_PHONE_NUMBER'; // e.g., '15551234567'
    if (testRecipient.startsWith('REPLACE')) {
        console.warn(""Please replace 'REPLACE_WITH_YOUR_PERSONAL_PHONE_NUMBER' to test sending SMS on startup."");
    } else {
        console.log(`Sending test SMS to ${testRecipient}...`);
        sendSms(testRecipient, 'Hello from the Vonage Node.js App!');
    }
    */
    
    // --- Graceful Shutdown (Optional but Recommended) ---
    process.on('SIGINT', () => {
        console.log('\nSIGINT received. Shutting down gracefully...');
        // Add any cleanup logic here (e.g., close database connections)
        process.exit(0);
    });

Code explanation:

  • Dependencies & Config: Loads dotenv, imports express and Vonage, sets up the Express app, and defines the port.
  • Middleware: express.json() and express.urlencoded() are essential for parsing the incoming webhook request bodies sent by Vonage.
  • Vonage Client: Initialized using the applicationId and the path to the privateKey file (VONAGE_PRIVATE_KEY_PATH). The SDK handles reading the key from the specified path. API Key/Secret are included optionally.
  • sendSms Function: An asynchronous helper function that encapsulates the logic for sending an SMS using vonage.messages.send(). It includes basic parameter validation and error handling using a try-catch block. It specifies channel: 'sms' and message_type: 'text'.
  • /webhooks/inbound: This is the core endpoint for receiving SMS messages.
    • It listens for POST requests at the path configured in the Vonage dashboard.
    • It logs the incoming request body (req.body) for debugging. The structure contains details like from.number (sender) and message.content.text (message body).
    • It extracts the sender's number and the message text.
    • Crucially, it sends a 200 OK status back to Vonage immediately using res.status(200).send('OK');. This confirms receipt; without it, Vonage will retry sending the webhook.
    • It then constructs a reply message and calls the sendSms function asynchronously to send the reply back to the original sender. Sending the reply happens after acknowledging the webhook.
  • /webhooks/status: This endpoint receives delivery status updates for messages you've sent.
    • It logs the status payload, which includes the message_uuid, the final status (e.g., delivered, failed, rejected), the recipient number (to), and potential error details.
    • It also sends a 200 OK response. You would typically add logic here to update message delivery status in a database or trigger alerts on failure.
  • Root Endpoint (/): A simple GET endpoint to verify the server is running via a web browser.
  • Server Start: app.listen() starts the server on the configured port and logs useful startup information including the current date and time.
  • Test SMS (Commented Out): Includes an example of how to use sendSms directly, useful for initial testing but should be removed or adapted for production.
  • Graceful Shutdown: Basic handling for SIGINT (Ctrl+C) to allow for cleanup if needed.

3. Building a complete API layer

The webhook endpoints (/webhooks/inbound, /webhooks/status) effectively form the API layer for interacting with Vonage.

  • Authentication/Authorization: Vonage Messages API v1.0 supports both JWT (JSON Web Token) and Basic authentication. For webhook requests from Vonage to your application, you must implement JWT Signature Verification in production. Vonage signs webhook requests using JWT with your application's private key. You verify this signature to ensure the request genuinely originated from Vonage and wasn't tampered with.

JWT Signature Verification Implementation:

javascript
// Add this to your webhook handlers for production
app.post('/webhooks/inbound', (req, res) => {
    // Verify JWT signature from Authorization header
    const token = req.headers['authorization'];

    if (!token) {
        console.warn('Missing authorization header in webhook request');
        return res.status(401).send('Unauthorized');
    }

    try {
        // Verify JWT signature using Vonage SDK
        // The SDK uses your application's public key (stored by Vonage) to verify
        const isValid = vonage.credentials.verifySignature(token);

        if (!isValid) {
            console.error('Invalid webhook signature');
            return res.status(401).send('Unauthorized');
        }

        // Signature verified, proceed with webhook processing
        console.log('--- Inbound SMS Received (Signature Verified) ---');
        // ... rest of your webhook handler code
    } catch (error) {
        console.error('Signature verification error:', error);
        return res.status(401).send('Unauthorized');
    }

    // ... existing webhook handler code
});

Source: Vonage Messages API v1.0 documentation (developer.vonage.com/en/api/messages-olympus, verified October 2025), Vonage webhook security best practices

  • Request Validation: Basic validation is included (checking for from.number and message.content.text). For production, use a dedicated validation library (like Joi or express-validator) to define schemas for the expected webhook payloads and reject unexpected or malformed requests early.

  • Webhook Payload Structure:

    • Inbound webhook includes: channel (e.g., "sms"), message_uuid (UUID format), to (7-15 digits), from (7-15 digits), timestamp (ISO 8601 format, e.g., "2025-02-03T12:14:25Z"), text (UTF-8 encoded), sms.num_messages (concatenation count), usage.currency (ISO 4217, e.g., "EUR"), usage.price (stringified decimal)
    • Status webhook includes: message_uuid, to, from, timestamp (ISO 8601), status (submitted/delivered/rejected/undeliverable), error.type (URL to error details), error.title (error code), error.detail (description), channel, sms.count_total (SMS segments)

Source: Vonage Messages API v1.0 Webhook Reference (developer.vonage.com/en/api/messages-olympus, verified October 2025)

  • API Endpoint Documentation:
    • POST /webhooks/inbound: Receives inbound SMS messages.
      • Request Body (JSON): See Vonage Messages API Inbound Message Webhook Reference. Key fields: from.type, from.number, to.type, to.number, message_uuid, message.content.type, message.content.text, timestamp.
      • Response: 200 OK (Empty body or text "OK").
    • POST /webhooks/status: Receives message status updates.
      • Request Body (JSON): See Vonage Messages API Message Status Webhook Reference. Key fields: message_uuid, to.type, to.number, from.type, from.number, timestamp, status, usage, error.
      • Response: 200 OK (Empty body or text "OK").

4. Integrating with Vonage (Covered in Setup)

The integration steps involving API keys, application creation, number purchasing/linking, and webhook configuration were covered in the ""Setting up the project"" section. Secure handling of API keys and the private key is achieved by using environment variables (dotenv) and ensuring .env and private.key are in .gitignore.

5. Implementing error handling and logging

  • Error Handling Strategy:
    • Use try...catch blocks around asynchronous operations, especially Vonage API calls (sendSms).
    • Log errors clearly using console.error, including relevant context (e.g., recipient number, operation attempted).
    • For webhook handlers (/webhooks/inbound, /webhooks/status), always send a 200 OK response to Vonage, even if internal processing fails after receiving the request. Log the internal error separately. This prevents unnecessary retries from Vonage flooding your server. If the request itself is invalid before processing, a 4xx might be appropriate, but generally, 200 OK is safest for acknowledged receipt.
    • Check for specific error conditions from Vonage responses if needed (e.g., err.response.status or err.response.data from the SDK).

Common Vonage API Error Codes:

Error CodeDescriptionRecommended Action
1000Throttled - Exceeded submission capacityImplement exponential backoff, retry after delay
1010Missing params - Incomplete requestValidate all required parameters before API call
1020Invalid params - Parameter value invalidCheck parameter format (E.164, character limits)
1120Illegal Sender Address - SenderID rejectedUse purchased Vonage number, check regional restrictions
1170Invalid or Missing MSISDN - Phone number invalid/missingValidate E.164 format (7-15 digits)
1210Anti-Spam Rejection - Content/SenderID blocked by carrierReview message content, avoid spam triggers
1240Illegal Number - Recipient opted out (STOP)Remove from contact list, maintain suppression database
1250Unroutable - Number on unsupported networkVerify number validity, check carrier support
1330Unknown - Carrier error or invalid recipientVerify recipient number format and validity
1460Daily message limit exceeded - 10DLC complianceCheck compliance registration, monitor daily quotas
1470Fraud Defender Traffic Rule - Prefix blockedReview traffic rules, contact support if legitimate

Source: Vonage Messages API v1.0 Error Codes (developer.vonage.com/en/api/messages-olympus, verified October 2025)

  • Logging:

    • The current implementation uses console.log and console.error. For production, use a more robust logging library like pino or winston.

    • Configure log levels (e.g., info, warn, error, debug).

    • Output logs in a structured format (like JSON) for easier parsing by log analysis tools.

    • Include timestamps and potentially request IDs in logs.

    • Example (Conceptual using Pino):

      javascript
      // const pino = require('pino');
      // const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
      // ... replace console.log with logger.info, console.error with logger.error etc.
      // logger.info({ reqBody: req.body }, 'Inbound SMS Received');
  • Retry Mechanisms: Vonage handles retries for webhook delivery if it doesn't receive a 200 OK response. For outgoing messages (sendSms) that fail due to potentially transient network issues or Vonage service errors (e.g., 5xx status codes), you could implement a retry strategy with exponential backoff within your sendSms function or using a dedicated library like async-retry. However, be cautious about retrying errors related to invalid numbers or insufficient funds (4xx errors).

6. Creating a database schema (Optional - Beyond Scope)

This basic guide doesn't include database integration. For a production application, you would typically store:

  • Messages: Incoming and outgoing messages (sender, recipient, text, timestamp, Vonage message UUID, status).

  • Conversations: Grouping messages by participants.

  • Users/Contacts: If managing known contacts.

  • Schema (Conceptual - PostgreSQL):

    sql
    CREATE TABLE messages (
        message_id SERIAL PRIMARY KEY,
        vonage_message_uuid VARCHAR(255) UNIQUE,
        direction VARCHAR(10) NOT NULL, -- 'inbound' or 'outbound'
        sender_number VARCHAR(20) NOT NULL,
        recipient_number VARCHAR(20) NOT NULL,
        message_text TEXT,
        status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, delivered, failed, read
        vonage_status_timestamp TIMESTAMPTZ,
        error_code VARCHAR(50),
        error_reason TEXT,
        created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
    );
    
    -- Index for querying by status or number
    CREATE INDEX idx_messages_status ON messages(status);
    CREATE INDEX idx_messages_recipient ON messages(recipient_number);
    CREATE INDEX idx_messages_sender ON messages(sender_number);
  • Data Layer: Use an ORM (like Sequelize or Prisma) or a query builder (Knex.js) to interact with the database, handle migrations, and manage connections. Update webhook handlers to save/update message records.

7. Adding security features

  • Input Validation: As mentioned, use libraries like Joi or express-validator in webhook handlers to validate the structure and types of incoming data (req.body). Sanitize any data before storing or using it in replies if necessary, although SMS content is often treated as plain text.
  • Webhook Security: Implement Webhook Signature Verification (see Section 3) as the primary defense against spoofed requests. This is essential for production.
  • API Key/Secret Security: Use dotenv and .gitignore to protect credentials. Use tools like git-secrets to prevent accidental commits of secrets. Consider using a dedicated secrets management service in production (e.g., AWS Secrets Manager, HashiCorp Vault).
  • Rate Limiting: Implement rate limiting on your webhook endpoints using middleware like express-rate-limit to prevent abuse or denial-of-service attacks. Configure sensible limits based on expected traffic.
  • Common Vulnerabilities (General Node/Express): Keep dependencies updated (npm audit), use security headers (helmet middleware), protect against Cross-Site Scripting (XSS) if rendering user content in web views (not applicable here), and Cross-Site Request Forgery (CSRF) if you have web forms (not applicable here).

8. Handling special cases

  • Character Encoding & Emojis: The Vonage Messages API handles UTF-8 encoding automatically. The text parameter supports up to 1000 characters. The API automatically detects unicode characters and adjusts encoding accordingly (GSM-7 for standard text, UCS-2 for unicode/emojis) unless explicitly set via sms.encoding_type parameter (options: "text", "unicode", "auto"). Spanish, French, German, and other European language characters with diacritics are typically supported in GSM-7 encoding.

SMS Concatenation: Messages exceeding single SMS limits are automatically split:

  • GSM-7 encoding: 160 characters per single SMS, 153 characters per segment for concatenated messages
  • UCS-2 encoding (unicode/emojis): 70 characters per single SMS, 67 characters per segment

Source: Vonage Messages API v1.0 documentation (developer.vonage.com/en/api/messages-olympus, verified October 2025)

  • Message Time-To-Live (TTL): The ttl parameter (in seconds) controls delivery attempt duration. Default is 72 hours (259200 seconds), but effective maximum depends on carrier (typically 24-48 hours). Minimum recommended: 1800 seconds (30 minutes). Example: ttl: 90000 (25 hours).

  • Multipart SMS: Longer messages are automatically split by Vonage. The Messages API handles this transparently for sending. For inbound multipart messages, Vonage typically delivers them as a single webhook request with concatenated text. The sms.num_messages field in the inbound webhook indicates how many SMS segments were concatenated.

  • Alphanumeric Sender IDs: For outbound SMS, from can sometimes be a text string (e.g., "MyBrand") instead of a number in supported countries. Check Vonage documentation and local regulations. US numbers generally require sending from a purchased Vonage number. Alphanumeric sender IDs are one-way only (no replies possible).

  • International Numbers: Ensure numbers are in E.164 format (7-15 digits, country code first, no + or 00 prefix in API requests). Examples: 447700900000 (UK), 14155550100 (US), 61412345678 (Australia), 5511998765432 (Brazil).

Source: ITU-T Recommendation E.164 (International numbering plan), Vonage API documentation

  • Stop/Help Keywords: Be aware of carrier requirements and regulations regarding opt-out keywords (STOP, UNSUBSCRIBE) and help keywords (HELP). Vonage may offer features to manage opt-outs automatically, or you may need to implement logic in your inbound webhook to detect these keywords and take appropriate action (e.g., adding the number to a blocklist, sending a standard help response). For US 10DLC compliance, STOP/HELP keyword support is mandatory.

9. Implementing performance optimizations

  • Asynchronous Operations: The code uses async/await and handles the reply sending asynchronously after responding 200 OK to the webhook. This is crucial for performance and responsiveness.
  • Webhook Response Time: Responding 200 OK quickly to webhooks is paramount. Avoid long-running synchronous operations within the webhook handler before sending the response. Offload heavy processing to background jobs if necessary (e.g., using message queues like RabbitMQ or Redis queues).
  • Node.js Clustering: For handling higher loads, use Node.js's built-in cluster module or a process manager like PM2 in cluster mode to run multiple instances of your application across CPU cores.
  • Caching: If fetching user data or templates frequently, implement caching (e.g., using Redis or Memcached) to reduce database load. Not critical for this simple example.
  • Resource Usage: Monitor CPU and memory usage. Optimize database queries if using a database.

10. Adding monitoring, observability, and analytics

  • Health Checks: Add a simple health check endpoint (e.g., GET /health) that returns 200 OK if the server is running and can connect to essential services (like Vonage, if possible, or a database). Monitoring services can ping this endpoint.
  • Performance Metrics: Use libraries like prom-client to expose application metrics (request latency, error rates, throughput) in a Prometheus-compatible format. Monitor Node.js event loop lag.
  • Error Tracking: Integrate an error tracking service (e.g., Sentry, Bugsnag) to capture, aggregate, and alert on unhandled exceptions and logged errors.
  • Logging & Dashboards: Ship logs (using a library like Pino configured for JSON output) to a centralized logging platform (e.g., Elasticsearch/Logstash/Kibana (ELK), Datadog Logs, Splunk). Create dashboards to visualize key metrics like inbound/outbound message volume, error rates, delivery rates (from status webhooks), and response times.
  • Vonage Dashboard: Utilize the Vonage Dashboard's analytics and logs for insights into API usage, message delivery, and costs.

11. Troubleshooting and Caveats

  • ngrok Issues:
    • Not Running/Wrong Port: Ensure ngrok http 3000 (or your APP_PORT) is running in a separate terminal.
    • URL Expired: Free ngrok URLs expire after a session or time limit. Restart ngrok and update the webhook URLs in the Vonage dashboard if needed. Paid plans offer stable subdomains.
    • Firewall: Local or network firewalls might block ngrok. Check your settings.
  • Webhook Not Received:
    • Check ngrok is running and the URL is correct (HTTPS) in the Vonage Application settings (Inbound/Status URLs).
    • Verify the server is running (node server.js) and listening on the correct port.
    • Check the ngrok web interface (http://127.0.0.1:4040) for incoming requests. If they appear there but not in your server logs, check server-side routing and middleware.
    • Check the Vonage Dashboard logs for webhook delivery failures or errors.

Frequently Asked Questions

How do I send SMS messages using Vonage Messages API with Node.js?

Send SMS messages using Vonage Messages API with Node.js by: (1) Install the @vonage/server-sdk package via npm, (2) Create a Vonage application in the dashboard and download the private.key file, (3) Purchase a Vonage virtual number with SMS capabilities, (4) Initialize the Vonage client with your Application ID and private key path, (5) Use vonage.messages.send() with parameters: channel: 'sms', message_type: 'text', to (recipient in E.164 format: 7–15 digits, no + prefix), from (your Vonage number), and text (message content up to 1000 characters). The API supports both JWT (JSON Web Token) and Basic authentication. Example: await vonage.messages.send({ channel: 'sms', message_type: 'text', to: '14155550100', from: '14155559999', text: 'Hello from Vonage!' }). The response includes a message_uuid for tracking. Node.js 18.x LTS, 20.x LTS, or 22.x LTS required. Automatic encoding detection handles GSM-7 (160 chars) and UCS-2/unicode (70 chars) with automatic message concatenation for longer content.

How do I set up webhooks to receive inbound SMS messages from Vonage?

Set up Vonage SMS webhooks by: (1) Create a Vonage Application in the dashboard (Dashboard → Applications → Create), (2) Enable the Messages capability, (3) Configure webhook URLs – set Inbound URL to https://your-domain.com/webhooks/inbound and Status URL to https://your-domain.com/webhooks/status, (4) For local development, use ngrok (ngrok http 3000) to expose your local server and use the generated HTTPS URL (note: free ngrok URLs expire after 2 hours), (5) Create Express POST endpoints matching your webhook URLs, (6) Parse incoming JSON with express.json() middleware, (7) Extract data from req.body (inbound webhook includes from.number, message.content.text, message_uuid, timestamp in ISO 8601 format), (8) Crucial: Always respond with 200 OK immediately to prevent Vonage retries (Vonage retries failed webhook deliveries), (9) Process messages asynchronously after sending the response. Webhook payloads include channel, message_uuid (UUID format), phone numbers in E.164 format (7–15 digits), timestamp (ISO 8601), and usage data (currency in ISO 4217, price as stringified decimal).

What is E.164 phone number format and how do I validate it for Vonage?

E.164 format is the ITU-T international phone numbering standard requiring: (1) 7–15 total digits (country code + subscriber number), (2) Country code first (e.g., 1 for US/Canada, 44 for UK, 61 for Australia), (3) No leading + or 00 prefix in Vonage API requests (unlike display format), (4) No spaces, hyphens, or special characters. Validation regex: /^\d{7,15}$/ after removing + prefix. Examples: 14155550100 (US), 447700900000 (UK), 5511998765432 (Brazil). Implementation: const cleanNumber = phoneNumber.replace(/^\+/, ''); if (!/^\d{7,15}$/.test(cleanNumber)) { throw new Error('Invalid E.164 format'); }. Common mistakes: (1) Including + prefix in API calls (accepted in display only), (2) Not removing spaces/formatting, (3) Wrong digit count (too short/long), (4) Missing country code. Vonage API error 1170 ("Invalid or Missing MSISDN") indicates E.164 validation failure. Use E.164 format consistently across all Vonage Messages API calls for to and from parameters.

How do I implement JWT webhook signature verification for Vonage?

Implement JWT (JSON Web Token) signature verification for Vonage webhooks by: (1) Extract JWT token from the Authorization header in incoming webhook requests, (2) Verify the signature using Vonage SDK method vonage.credentials.verifySignature(token), (3) Return 401 Unauthorized if token is missing or invalid, (4) Only process webhook payload after successful verification. Implementation: const token = req.headers['authorization']; if (!token) return res.status(401).send('Unauthorized'); try { const isValid = vonage.credentials.verifySignature(token); if (!isValid) return res.status(401).send('Unauthorized'); } catch (error) { return res.status(401).send('Unauthorized'); }. Vonage uses your application's public key (generated during application creation) to sign webhooks. The SDK verifies using the corresponding private key stored locally. Critical for production: Prevents spoofed webhook requests, man-in-the-middle attacks, and replay attacks. Messages API v1.0 supports both JWT and Basic authentication. JWT is recommended for production deployments. Without verification, malicious actors could send fake webhook requests to your endpoints, potentially triggering unauthorized SMS sends or data corruption.

What are the common Vonage API error codes and how do I handle them?

Common Vonage Messages API error codes and handling: 1000 (Throttled – exceeded submission capacity) → implement exponential backoff retry (1s, 2s, 4s, 8s intervals), 1120 (Illegal Sender Address – SenderID rejected) → use purchased Vonage number, check regional restrictions for alphanumeric sender IDs, 1170 (Invalid MSISDN – phone number invalid) → validate E.164 format (7–15 digits, no + prefix), 1210 (Anti-Spam Rejection – content/SenderID blocked) → review message content, avoid spam triggers (ALL CAPS, excessive punctuation, suspicious URLs), 1240 (Illegal Number – recipient opted out via STOP) → maintain suppression database, remove from contact list, 1460 (Daily limit exceeded – 10DLC compliance) → register for 10DLC, monitor daily quotas, 1470 (Fraud Defender Traffic Rule) → review traffic rules in dashboard, contact support. Error handling strategy: (1) Use try-catch blocks around vonage.messages.send(), (2) Log errors with context (recipient, error code, timestamp), (3) Categorize as retryable (1000, 5xx) vs permanent (1240, 1170), (4) Implement retry logic with exponential backoff for transient errors only, (5) Alert on permanent failures. Access errors via err.response.data or err.response.status from SDK.

How do I handle SMS message delivery status updates from Vonage?

Handle Vonage SMS delivery status updates by: (1) Configure Status URL webhook in your Vonage Application (Dashboard → Applications → Your App → Edit → Status URL: https://your-domain.com/webhooks/status), (2) Create Express POST endpoint at /webhooks/status, (3) Parse incoming JSON status updates from req.body, (4) Extract key fields: message_uuid (UUID for tracking), status (submitted/delivered/rejected/undeliverable), timestamp (ISO 8601 format), to (recipient number), error object (if failed: error.type URL, error.title code, error.detail description), channel, sms.count_total (SMS segments sent), (5) Always respond 200 OK immediately to acknowledge receipt, (6) Process status asynchronously: update database records, trigger alerts for failures, track delivery rates. Status flow: submitted → delivered (success) OR submitted → rejected/undeliverable (failure). Store message_uuid from send response to correlate with status updates. Example: const { message_uuid, status, error } = req.body; if (error) { console.error(Message ${message_uuid} failed: ${error.detail}); } await db.updateMessageStatus(message_uuid, status);. Monitor delivery rates (target: 95%+ for transactional SMS). Status updates arrive seconds to minutes after sending, depending on carrier.

How do I configure ngrok for local Vonage webhook development?

Configure ngrok for local Vonage webhook development by: (1) Download and install ngrok from ngrok.com/download, (2) Create free ngrok account and authenticate: ngrok config add-authtoken YOUR_TOKEN, (3) Start ngrok tunnel to your local port: ngrok http 3000 (match your Express APP_PORT), (4) Copy the HTTPS Forwarding URL from ngrok output (format: https://abc123.ngrok-free.app), (5) Update Vonage Application webhook URLs: Inbound URL: https://abc123.ngrok-free.app/webhooks/inbound, Status URL: https://abc123.ngrok-free.app/webhooks/status, (6) Keep ngrok running in separate terminal window while developing, (7) View webhook requests in ngrok web interface at http://127.0.0.1:4040. Important limitations: Free ngrok sessions expire after 2 hours, URLs change on restart (requires updating Vonage webhook URLs each time), maximum 40 requests/minute. For stable development URLs, use ngrok paid plan ($8/month) with reserved domains. Production deployment: Replace ngrok with permanent domain (AWS, Heroku, Vercel) with HTTPS enabled. Ngrok alternatives: localtunnel, serveo, CloudFlare Tunnel (free tier available).

How do I handle SMS character encoding and message concatenation?

Handle SMS character encoding in Vonage Messages API by: (1) Automatic detection: API automatically detects character types and selects GSM-7 (standard) or UCS-2 (unicode) encoding unless explicitly set via sms.encoding_type parameter (options: "text", "unicode", "auto"), (2) GSM-7 encoding supports 160 characters per single SMS, 153 characters per segment for concatenated messages (7 characters reserved for UDH – User Data Header). Includes: A-Z, 0-9, basic punctuation, Spanish/French/German diacritics (á, é, ñ, ü), (3) UCS-2 encoding (unicode/emojis) supports 70 characters per single SMS, 67 characters per segment. Triggered by: emojis, Chinese/Japanese/Arabic characters, special symbols, (4) Text limit: 1000 characters maximum in text parameter, (5) Concatenation: Automatic – Vonage splits long messages transparently. Inbound webhook field sms.num_messages shows segment count. Status webhook field sms.count_total shows segments sent. Cost impact: Each segment billed separately (161-character message costs 2× single SMS). Best practice: Keep messages ≤160 characters (GSM-7) or ≤70 characters (unicode) to avoid concatenation charges. Validate character count client-side before sending.

What security best practices should I follow for production Vonage SMS applications?

Production security best practices for Vonage SMS applications: (1) JWT Signature Verification – implement vonage.credentials.verifySignature() on all webhook endpoints to prevent spoofed requests (critical, see Section 3), (2) Environment Variables – store credentials in .env file, never hardcode API keys/secrets, add .env and private.key to .gitignore, use secrets manager in production (AWS Secrets Manager, HashiCorp Vault), (3) Input Validation – use Joi or express-validator to validate webhook payloads, sanitize user inputs, validate E.164 format before API calls, (4) Rate Limiting – implement express-rate-limit middleware on webhook endpoints (recommended: 100 requests/minute per IP), prevent DoS attacks, (5) HTTPS Only – enforce HTTPS for webhook URLs (Vonage requirement), use TLS 1.2+ certificates, (6) Dependency Security – run npm audit regularly, update dependencies, use npm audit fix, (7) Error Handling – never expose sensitive data in error messages, log errors securely with context (timestamp, user ID, request ID), (8) Webhook Response – always respond 200 OK to valid requests (even if internal processing fails) to prevent retry floods, (9) Security Headers – use helmet middleware for Express (XSS protection, CSP, HSTS), (10) Monitoring – integrate error tracking (Sentry, Bugsnag), monitor failed authentications, track unusual traffic patterns.

How do I deploy a Vonage SMS application to production?

Deploy Vonage SMS application to production by: (1) Choose hosting provider: AWS (EC2, Lambda), Heroku, Google Cloud Platform, DigitalOcean, or Vercel (Node.js support required), (2) Update webhook URLs: Replace ngrok URLs in Vonage Application settings with permanent domain HTTPS URLs (Dashboard → Applications → Your App → Edit → Inbound/Status URLs), (3) Environment configuration: Set production environment variables (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, VONAGE_NUMBER, APP_PORT), upload private.key file securely (use secrets manager, not version control), (4) Node.js version: Use Active LTS (Node.js 18.x, 20.x, or 22.x), avoid Current versions for production stability, (5) Process management: Use PM2 (pm2 start server.js -i max for cluster mode) or Docker containers for reliability, enable auto-restart on crash, (6) SSL/TLS certificate: Configure HTTPS (Let's Encrypt free certificates, or CloudFlare SSL), Vonage requires HTTPS webhook URLs, (7) Implement security: Enable JWT signature verification (Section 3), rate limiting, input validation, security headers (helmet middleware), (8) Monitoring: Set up health checks (GET /health endpoint), error tracking (Sentry), logging (Pino/Winston to centralized platform), performance metrics (Prometheus), (9) Database: Integrate PostgreSQL/MongoDB for message persistence, use connection pooling, implement database migrations, (10) Testing: Load test webhook endpoints, verify error handling, test failover scenarios.

Source: Vonage Messages API v1.0 documentation, Node.js production deployment best practices (verified October 2025)