code examples

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

Build Two-Way SMS: Node.js, Express & Vonage Messages API Guide

A step-by-step guide to building a Node.js/Express application for sending and receiving SMS messages using the Vonage Messages API, covering setup, implementation, webhooks, and best practices.

Build Two-Way SMS: Node.js, Express & Vonage Messages API Guide

This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to handle both sending and receiving SMS messages via the Vonage Messages API. We'll cover everything from project setup and core implementation to security considerations and deployment.

By the end of this tutorial, you will have a functional application capable of:

  1. Sending SMS messages programmatically using the Vonage Node.js SDK.
  2. Receiving inbound SMS messages sent to your Vonage virtual number via webhooks.

This enables use cases like notifications, alerts, basic customer interactions, or integrating SMS into existing workflows.

Project Overview and Goals

What We're Building: A simple Node.js server using Express that exposes a webhook to receive incoming SMS messages from Vonage and includes a script to send outgoing SMS messages.

Problem Solved: This provides a foundational structure for integrating two-way SMS communication into applications without needing complex infrastructure. It demonstrates the core mechanics of interacting with a powerful communications API.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Express.js: A minimal and flexible Node.js web application framework for handling HTTP requests and routing.
  • Vonage Messages API: A unified API for sending and receiving messages across various channels, including SMS. We'll use the @vonage/server-sdk Node.js library.
  • ngrok: A tool to expose local development servers to the internet, essential for testing webhooks.
  • dotenv: A module to load environment variables from a .env file into process.env.

System Architecture:

mermaid
graph LR
    A[Your Application (Node.js/Express)] <--> B(Vonage Messages API);
    B <--> C(SMS Network);
    C <--> D[End User's Phone];

    subgraph Send SMS
        A -- Send Request --> B;
    end

    subgraph Receive SMS
        D -- Sends SMS --> C;
        C -- Delivers to Vonage Number --> B;
        B -- POST Request (Webhook) --> A;
    end

    subgraph Local Development
        E[ngrok Tunnel] <--> A;
        B -- Webhook via ngrok --> E;
    end

Prerequisites:

  • A Vonage API account (Sign up here).
  • Node.js and npm (or yarn) installed locally. Check with node -v and npm -v.
  • ngrok installed globally or available in your PATH (Download here).
  • A text editor (like VS Code).
  • Basic understanding of JavaScript and Node.js concepts.
  • A Vonage virtual phone number capable of sending/receiving SMS.

Final Outcome: A local Node.js application that can successfully send an SMS message when a script is run and log incoming SMS messages received on your Vonage number to the console. We will also discuss production considerations like security and deployment.

1. Setting up the Project

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

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

    bash
    mkdir vonage-sms-app
    cd vonage-sms-app
  2. Initialize npm: Create a package.json file to manage project dependencies and scripts.

    bash
    npm init -y

    The -y flag accepts the default settings.

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

    bash
    npm install express @vonage/server-sdk dotenv
  4. Create Core Files: Create the main files for our application logic and environment variables.

    bash
    touch server.js send-sms.js .env .gitignore
    • server.js: Will contain the Express server code for receiving messages.
    • send-sms.js: Will contain the script for sending messages.
    • .env: Will store our secret API keys and configuration (DO NOT commit this file).
    • .gitignore: Specifies files and directories that Git should ignore.
  5. Configure .gitignore: Add essential patterns to your .gitignore file to prevent committing sensitive information and unnecessary files.

    text
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Vonage Private Key (if stored directly)
    private.key
    
    # Logs
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    
    # OS generated files
    .DS_Store
    Thumbs.db
  6. Set up Environment Variables (.env): Open the .env file and add the following placeholders. We will populate these values in the ""Integrating with Vonage"" section.

    dotenv
    # Vonage API Credentials (Found in your Vonage Dashboard)
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Vonage Application Details (Generated when creating a Vonage Application)
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
    VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key # Path to your downloaded private key file
    
    # Phone Numbers
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Your purchased Vonage number (E.164 format, e.g., 12015550123)
    MY_TEST_NUMBER=YOUR_PERSONAL_PHONE_NUMBER # Your mobile number for testing (E.164 format)
    
    # Server Configuration
    PORT=3000
    
    # Optional: Vonage Webhook Signature Secret (See Section 7)
    # VONAGE_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET

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

Our basic project structure is now ready.

2. Implementing Core Functionality

We'll split the core logic into two parts: sending SMS and receiving SMS.

Sending SMS (send-sms.js)

This script will initialize the Vonage client using Application ID and Private Key authentication (recommended for server-side applications) and send a single SMS message.

  1. Edit send-sms.js: Add the following code:

    javascript
    // send-sms.js
    require('dotenv').config(); // Load environment variables from .env file
    
    const { Vonage } = require('@vonage/server-sdk');
    
    // --- Configuration ---
    const vonageNumber = process.env.VONAGE_NUMBER;
    const recipientNumber = process.env.MY_TEST_NUMBER; // Your personal number for testing
    const messageText = ""Hello from Vonage and Node.js!"";
    
    // --- Input Validation ---
    if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH) {
        console.error(""Error: Vonage Application ID or Private Key Path not set in .env file."");
        process.exit(1);
    }
    if (!vonageNumber || !recipientNumber) {
        console.error(""Error: VONAGE_NUMBER or MY_TEST_NUMBER not set in .env file."");
        process.exit(1);
    }
    
    // --- Initialize Vonage Client ---
    // Authentication uses Application ID and Private Key
    const vonage = new Vonage({
        applicationId: process.env.VONAGE_APPLICATION_ID,
        privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH,
    }, { debug: false }); // Set debug: true for verbose SDK logging
    
    // --- Send SMS Function ---
    async function sendSms() {
        console.log(`Attempting to send SMS from ${vonageNumber} to ${recipientNumber}...`);
        try {
            const resp = await vonage.messages.send({
                message_type: ""text"",
                text: messageText,
                to: recipientNumber,
                from: vonageNumber, // Must be your Vonage virtual number
                channel: ""sms""
            });
            console.log(""SMS Sent Successfully!"");
            console.log(""Message UUID:"", resp.message_uuid); // Unique ID for the message
        } catch (err) {
            console.error(""Error sending SMS:"");
            // Log the detailed error response if available
            if (err.response && err.response.data) {
                console.error(JSON.stringify(err.response.data, null, 2));
            } else {
                console.error(err);
            }
            process.exitCode = 1; // Indicate failure
        }
    }
    
    // --- Execute ---
    sendSms();

    Explanation:

    • require('dotenv').config(): Loads variables from .env.
    • new Vonage({...}): Initializes the SDK client. We use Application ID and Private Key for authentication, which is more secure for backend services than API Key/Secret.
    • vonage.messages.send({...}): The core function to send a message. We specify:
      • message_type: ""text"": Plain text SMS.
      • text: The content of the message.
      • to: The recipient's phone number (from .env).
      • from: The sender's phone number (must be your Vonage number, from .env).
      • channel: ""sms"": Specifies the channel.
    • async/await with try...catch: Handles the asynchronous nature of the API call and catches potential errors. Detailed error logging helps debugging.

Receiving SMS (server.js)

This script sets up an Express server to listen for incoming POST requests from Vonage on a specific webhook URL.

  1. Edit server.js: Add the following code:

    javascript
    // server.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    
    // --- Configuration ---
    const PORT = process.env.PORT || 3000;
    const inboundWebhookPath = '/webhooks/inbound';
    const statusWebhookPath = '/webhooks/status';
    
    // --- Create Express App ---
    const app = express();
    
    // --- Middleware ---
    // Vonage sends webhook data as JSON or URL-encoded form data depending on API/settings.
    // Use both parsers to be safe.
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    
    // --- Routes ---
    // Health check endpoint
    app.get('/health', (req, res) => {
        res.status(200).send('OK');
    });
    
    // Vonage Inbound Message Webhook Handler
    app.post(inboundWebhookPath, (req, res) => {
        console.log(""--- Inbound SMS Received ---"");
        console.log(""Timestamp:"", new Date().toISOString());
        console.log(""Request Body:"", JSON.stringify(req.body, null, 2)); // Log the entire payload
    
        // Extract key information (adjust based on actual payload structure from Vonage docs)
        const from = req.body.from;
        const to = req.body.to;
        const text = req.body.text;
        const messageId = req.body.message_uuid;
    
        console.log(`From: ${from}`);
        console.log(`To: ${to}`);
        console.log(`Message Text: ${text}`);
        console.log(`Message UUID: ${messageId}`);
    
        // **IMPORTANT:** Respond with 200 OK quickly!
        // Vonage expects a timely 200 response to acknowledge receipt.
        // Failure to respond promptly may result in retries from Vonage.
        // Offload any heavy processing (DB writes, external API calls)
        // to a background job/queue if necessary (See Section 9).
        res.status(200).end();
    });
    
    // Vonage Status Webhook Handler (Delivery Receipts - DLRs)
    app.post(statusWebhookPath, (req, res) => {
        console.log(""--- Message Status Update Received ---"");
        console.log(""Timestamp:"", new Date().toISOString());
        console.log(""Request Body:"", JSON.stringify(req.body, null, 2));
    
        // **IMPORTANT FOR PRODUCTION:** Parse the request body here!
        // You need to extract the message UUID, status (e.g., 'delivered', 'failed'),
        // timestamp, error codes, etc., to track the status of messages you SENT.
        // This is crucial for reliability and understanding message delivery success.
        // Example fields to look for (check Vonage docs for exact structure):
        // const messageUuid = req.body.message_uuid;
        // const status = req.body.status;
        // const timestamp = req.body.timestamp;
        // console.log(`Status for ${messageUuid}: ${status} at ${timestamp}`);
        // Update your database or application state based on the status.
    
        res.status(200).end(); // Acknowledge receipt
    });
    
    // --- Error Handling Middleware (Basic) ---
    app.use((err, req, res, next) => {
        console.error(""--- Unhandled Error ---"");
        console.error(err.stack);
        res.status(500).send('Something broke!');
    });
    
    // --- Start Server ---
    app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
        console.log(`Inbound SMS Webhook available at http://localhost:${PORT}${inboundWebhookPath}`);
        console.log(`Status Webhook available at http://localhost:${PORT}${statusWebhookPath}`);
        console.log('Run ngrok to expose this server: ngrok http', PORT);
    });

    Explanation:

    • express(): Creates an Express application instance.
    • app.use(express.json()) & app.use(express.urlencoded(...)): Middleware to parse incoming request bodies. Vonage might send JSON or form data, so using both covers common cases.
    • app.post('/webhooks/inbound', ...): Defines the route handler for POST requests made by Vonage to /webhooks/inbound when an SMS is received.
    • console.log(req.body): Logs the data sent by Vonage (sender number, recipient number, message text, message ID, timestamp, etc.).
    • res.status(200).end(): Crucial step. Sends an HTTP 200 OK status back to Vonage immediately to acknowledge receipt. If Vonage doesn't receive a 200 OK quickly, it will assume the delivery failed and may retry sending the webhook, leading to duplicate processing.
    • /webhooks/status: Handler for Delivery Receipts (DLRs), which inform you about the status (e.g., delivered, failed) of messages you sent. Parsing the body is essential in production.
    • /health: A simple endpoint for health checks.
    • app.listen(PORT, ...): Starts the server on the specified port (from .env or default 3000).

3. Building a Complete API Layer (Optional Expansion)

The current setup uses a script (send-sms.js) for sending and a webhook handler (server.js) for receiving. For more complex applications, you might expose an API endpoint for sending SMS instead of using a standalone script.

Note: The following example demonstrates adding an API endpoint to server.js. Remember that the // TODO: comments highlight essential steps for production readiness, not optional improvements. Also, if you integrate this into server.js, ensure the Vonage client is initialized only once for the entire application.

Example API Endpoint for Sending:

You could add this to server.js:

javascript
// --- Add Vonage Client initialization near the top of server.js ---
// Ensure this is done only ONCE if merging with the server.
const { Vonage } = require('@vonage/server-sdk');
const vonage = new Vonage({
    applicationId: process.env.VONAGE_APPLICATION_ID,
    privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH,
}, { debug: false });

// --- API Endpoint for Sending SMS ---
app.post('/api/send-sms', async (req, res) => {
    // --- Authentication/Authorization (ESSENTIAL FOR PRODUCTION) ---
    // TODO: Implement proper API key check, JWT validation, session check,
    // or other auth mechanism suitable for your application.
    // Example placeholder: Check for a specific header
    // const apiKey = req.headers['x-api-key'];
    // if (!isValidApiKey(apiKey)) { // Replace isValidApiKey with your actual validation logic
    //     return res.status(401).json({ error: 'Unauthorized' });
    // }

    // --- Request Validation (ESSENTIAL FOR PRODUCTION) ---
    const { to, text } = req.body;
    if (!to || !text || typeof to !== 'string' || typeof text !== 'string') {
        return res.status(400).json({ error: 'Invalid request body. Required: ""to"" (string), ""text"" (string)' });
    }
    // TODO: Add more robust validation:
    // - Check 'to' number format (E.164 recommended, use libphonenumber-js or similar)
    // - Check 'text' length limits (consider SMS segment limits)
    // - Sanitize input if used elsewhere (e.g., logging, database)

    const vonageNumber = process.env.VONAGE_NUMBER;
    if (!vonageNumber) {
        console.error(""VONAGE_NUMBER not configured."");
        return res.status(500).json({ error: 'Server configuration error.' });
    }

    try {
        console.log(`API sending SMS from ${vonageNumber} to ${to}`);
        const resp = await vonage.messages.send({
            message_type: ""text"", text, to, from: vonageNumber, channel: ""sms""
        });
        console.log(""API SMS Sent Successfully. UUID:"", resp.message_uuid);
        res.status(200).json({ success: true, message_uuid: resp.message_uuid });
    } catch (err) {
        console.error(""API Error sending SMS:"");
        if (err.response && err.response.data) {
            console.error(JSON.stringify(err.response.data, null, 2));
        } else {
            console.error(err);
        }
        // Provide a generic error message to the client for security
        res.status(500).json({ success: false, error: 'Failed to send SMS.' });
    }
});

// --- Add error handling and server start from original server.js ---
// ... (rest of server.js code: error handler, app.listen) ...

Testing with cURL:

bash
curl -X POST http://localhost:3000/api/send-sms \
     -H ""Content-Type: application/json"" \
     # -H ""X-API-Key: YOUR_SECRET_API_KEY"" # If you implemented auth
     -d '{
         ""to"": ""RECIPIENT_E164_NUMBER"",
         ""text"": ""API Test Message!""
     }'

Response Example (Success):

json
{
    ""success"": true,
    ""message_uuid"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890""
}

Response Example (Error):

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

This demonstrates structuring the sending logic as a proper API endpoint. Remember that the placeholder authentication and validation comments (// TODO:) represent critical steps needed for any real-world application.

4. Integrating with Vonage

Now, let's configure our Vonage account and populate the .env file with the necessary credentials.

  1. Log in to Vonage Dashboard: Go to https://dashboard.vonage.com/ and log in.

  2. Get API Key and Secret: On the main dashboard page, you'll find your API Key and API Secret (you might need to reveal it) usually under ""API settings"" or your account profile.

    • Copy the API key and paste it as the value for VONAGE_API_KEY in your .env file.
    • Copy the API secret and paste it as the value for VONAGE_API_SECRET in your .env file.

    (Note: While we use Application ID/Private Key for sending via Messages API, the Key/Secret might be needed for other SDK functions or account management).

  3. Purchase a Vonage Number:

    • Navigate to Numbers -> Buy numbers.
    • Search for a number with SMS capability in your desired country.
    • Purchase the number.
    • Copy the purchased number (in E.164 format, e.g., 12015550123 for a US number) and paste it as the value for VONAGE_NUMBER in your .env file.
    • Also, add your personal mobile number (for testing) in E.164 format as the value for MY_TEST_NUMBER.
  4. Create a Vonage Application: This application acts as a container for your configuration and security credentials (specifically the private key) needed for the Messages API.

    • Navigate to Applications -> Create a new application.
    • Give your application a name (e.g., Node Express SMS App).
    • Click Generate public and private key. This will automatically download a private.key file. Save this file securely in your project's root directory (the same place as your .env file). Make sure the path in your .env file (VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key) matches the location and name of this file. Ensure private.key is listed in your .gitignore file.
    • Under Capabilities, find Messages and toggle it ON.
    • Two URL fields will appear: Inbound URL and Status URL. We need ngrok running first to get the public URLs for these.
  5. Run ngrok: Open a new terminal window in your project directory. Start ngrok to expose your local server (running on port 3000, as defined in .env or the default in server.js) to the internet.

    bash
    ngrok http 3000

    ngrok will display output similar to this:

    Session Status online Account Your Name (Plan: Free) Version 3.x.x Region United States (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding http://xxxxxxxx.ngrok.io -> http://localhost:3000 Forwarding https://xxxxxxxx.ngrok.io -> http://localhost:3000 # <-- COPY THIS HTTPS URL Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00

    Copy the https:// Forwarding URL. Keep this terminal window open while testing.

  6. Configure Application Webhooks:

    • Go back to your Vonage Application configuration page (Step 4).
    • In the Inbound URL field, paste your ngrok HTTPS URL and append the path /webhooks/inbound. Example: https://xxxxxxxx.ngrok.io/webhooks/inbound
    • In the Status URL field, paste your ngrok HTTPS URL and append the path /webhooks/status. Example: https://xxxxxxxx.ngrok.io/webhooks/status
    • Click Generate application or Save changes.
    • You will be taken to the application's details page. Copy the Application ID displayed here.
    • Paste the Application ID as the value for VONAGE_APPLICATION_ID in your .env file.
  7. Link Your Number to the Application:

    • Navigate to Numbers -> Your numbers.
    • Find the Vonage number you purchased earlier.
    • Click the Manage button (or the gear icon/three dots) next to the number.
    • In the Forwarding or Application Assignment section (UI might vary), select Forward to Application or Assign Application.
    • Choose the Application you just created (e.g., Node Express SMS App) from the dropdown menu.
    • Click Save.
  8. Set Default SMS API (CRITICAL): The Vonage Node.js SDK's vonage.messages.send method relies on the account's default API setting being Messages API.

    • In the Vonage Dashboard, navigate to Account -> Settings.
    • Scroll down to the API settings section.
    • Find Default SMS Setting.
    • Ensure Messages API is selected. If not, select it and click Save changes. This ensures the webhooks and API calls use the expected format and capabilities for the Messages API.

Your .env file should now be fully populated with your credentials and configuration, and your Vonage account should be correctly set up to route incoming messages to your local application via ngrok.

5. Implementing Error Handling, Logging, and Retry Mechanisms

Our basic scripts include some error handling, but let's refine it.

  • Sending Errors (send-sms.js):

    • The try...catch block already handles API errors.
    • Logging err.response.data provides detailed error information from Vonage (e.g., insufficient funds, invalid number format, throttling).
    • Retries: For transient network errors or temporary Vonage issues (like 429 Too Many Requests or 5xx server errors), you could implement a retry mechanism using libraries like async-retry around the vonage.messages.send call.
    javascript
    // Example using async-retry (install with: npm install async-retry)
    const retry = require('async-retry');
    
    async function sendSmsWithRetry() {
        // Define message details here or pass them as arguments
        const messageDetails = {
            message_type: ""text"",
            text: ""Your message content"", // Replace with actual text
            to: process.env.MY_TEST_NUMBER,
            from: process.env.VONAGE_NUMBER,
            channel: ""sms""
        };
    
        await retry(async (bail, attemptNumber) => {
            // If Vonage returns specific error codes indicating a permanent failure,
            // use bail() to stop retrying. Example: Invalid credentials (401), Invalid number (400).
            // Check err.response.status or err.response.data.type for specific conditions.
            try {
                 console.log(`Attempting to send SMS (attempt ${attemptNumber})...`);
                 const resp = await vonage.messages.send(messageDetails);
                 console.log(""SMS Sent Successfully!"");
                 console.log(""Message UUID:"", resp.message_uuid);
                 return resp; // Return result on success
            } catch (err) {
                console.warn(`Attempt ${attemptNumber} failed: ${err.message}`);
                if (err.response && (err.response.status === 401 || err.response.status === 400)) {
                    console.error(""Permanent error detected, not retrying."");
                    bail(err); // Stop retrying for permanent errors
                    return; // Important to exit after bail
                }
                // For other errors (like 5xx, 429, network issues), throw to trigger retry
                throw err;
            }
        }, {
            retries: 3, // Number of retries
            factor: 2, // Exponential backoff factor
            minTimeout: 1000, // Initial timeout (ms)
            onRetry: (error, attempt) => {
                console.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`);
            }
        });
    }
    // In send-sms.js, replace the call to sendSms() with:
    // sendSmsWithRetry().catch(err => {
    //     console.error(""Failed to send SMS after multiple retries:"", err);
    //     process.exitCode = 1;
    // });
  • Receiving Errors (server.js):

    • The main priority is sending res.status(200).end() quickly.
    • Wrap the core logic within the webhook handler in a try...catch block to handle unexpected errors during processing (e.g., issues parsing req.body, database errors if added).
    • Log errors thoroughly within the catch block.
    • Decide whether to still send 200 OK even if processing fails (to prevent Vonage retries) or send a 500 Internal Server Error if the failure is critical and retrying might help (though Vonage might retry anyway). Sending 200 OK is generally preferred unless you have specific retry logic tied to 5xx errors.
    javascript
    // Inside app.post('/webhooks/inbound', ...)
    app.post(inboundWebhookPath, async (req, res) => { // Make handler async if using await inside
        try {
            console.log(""--- Inbound SMS Received ---"");
            console.log(""Timestamp:"", new Date().toISOString());
            console.log(""Request Body:"", JSON.stringify(req.body, null, 2));
    
            const { from, to, text, message_uuid } = req.body;
            console.log(`From: ${from}`);
            console.log(`To: ${to}`);
            console.log(`Message Text: ${text}`);
            console.log(`Message UUID: ${message_uuid}`);
    
            // --- Add your business logic here ---
            // Example: await saveMessageToDatabase(from, to, text, message_uuid);
            // Example: await triggerAnotherApiCall(text);
            // --- End business logic ---
    
            // If business logic succeeds, acknowledge receipt
            res.status(200).end();
    
        } catch (error) {
            console.error(""--- Error processing inbound SMS ---"");
            console.error(""Timestamp:"", new Date().toISOString());
            console.error(""Request Body:"", JSON.stringify(req.body, null, 2));
            console.error(""Error Stack:"", error.stack);
    
            // Decide on response:
            // Option 1 (Recommended): Acknowledge receipt even on processing error
            // to prevent Vonage retries for potentially malformed but acknowledged data.
            // Log the error thoroughly for investigation.
            if (!res.headersSent) { // Check if response already sent
               res.status(200).end();
            }
    
            // Option 2: Signal server error (Vonage might retry, potentially causing duplicates if the error is intermittent)
            // Use this cautiously, only if a retry is genuinely desired for this specific error.
            // if (!res.headersSent) { // Ensure headers haven't already been sent
            //    res.status(500).send('Internal Server Error');
            // }
        }
    });
    // Note: res.status(200).end() is now inside the try/catch blocks.
  • Logging:

    • console.log is suitable for development.
    • For production, use a structured logging library like Pino or Winston. These enable:
      • Log Levels: info, warn, error, debug.
      • Structured Formats: JSON output for easier parsing by log analysis tools (Datadog, Splunk, ELK stack).
      • Transports: Sending logs to files, databases, or external services.
    • Log key events: Application start, server listening, API calls (request/response summaries), webhook received (key data), errors (with stack traces).

6. Creating a Database Schema and Data Layer (Optional Expansion)

If you need to store message history or related data, you'll need a database.

  1. Choose a Database: PostgreSQL, MongoDB, MySQL, etc.

  2. Choose an ORM/Driver: Prisma, Sequelize, Mongoose, pg, mysql2.

  3. Define Schema:

    • Example (using Prisma schema syntax):
    prisma
    // schema.prisma
    datasource db {
      provider = ""postgresql"" // or ""mysql"", ""mongodb"", etc.
      url      = env(""DATABASE_URL"")
    }
    
    generator client {
      provider = ""prisma-client-js""
    }
    
    model SmsMessage {
      id          String   @id @default(cuid())
      vonageMsgId String   @unique // The message_uuid from Vonage
      direction   String   // ""inbound"" or ""outbound""
      fromNumber  String
      toNumber    String
      body        String?
      status      String?  // e.g., ""submitted"", ""delivered"", ""failed"", ""received"" (for inbound)
      vonageStatusPayload Json? // Store the full status webhook payload for reference
      createdAt   DateTime @default(now())
      updatedAt   DateTime @updatedAt
    
      @@index([createdAt])
      @@index([fromNumber])
      @@index([toNumber])
    }
  4. Implement Data Access:

    • Install Prisma (npm install prisma --save-dev, npm install @prisma/client).
    • Initialize Prisma: npx prisma init.
    • Set DATABASE_URL in .env.
    • Apply schema: npx prisma db push (for development) or npx prisma migrate dev (for migrations).
    • Use the Prisma client in your server.js (or a dedicated data service layer):
    javascript
    // server.js (add near top)
    const { PrismaClient } = require('@prisma/client');
    const prisma = new PrismaClient();
    
    // Inside app.post('/webhooks/inbound', ...) try block:
    // Make the handler async: app.post(inboundWebhookPath, async (req, res) => { ... });
    try {
        // ... previous logging ...
        const { from, to, text, message_uuid } = req.body;
    
        // NOTE: For production/high-volume, move this DB operation
        // to a background queue (See Section 9) to ensure the webhook
        // responds quickly.
        await prisma.smsMessage.create({
            data: {
                vonageMsgId: message_uuid,
                direction: 'inbound',
                fromNumber: from,
                toNumber: to,
                body: text,
                status: 'received', // Initial status for inbound
                // Use Vonage timestamp if available and valid, otherwise fallback
                createdAt: req.body.timestamp && !isNaN(Date.parse(req.body.timestamp)) ? new Date(req.body.timestamp) : new Date(),
            },
        });
        console.log(`Inbound message ${message_uuid} saved to database.`);
    
        // Acknowledge receipt AFTER successful processing (if not queueing)
        if (!res.headersSent) {
            res.status(200).end();
        }
    
    } catch (error) {
       console.error(""--- Error processing inbound SMS ---"");
       console.error(""Timestamp:"", new Date().toISOString());
       console.error(""Request Body:"", JSON.stringify(req.body, null, 2));
       console.error(""Error Stack:"", error.stack);
       console.error(""Database error during inbound processing:"", error);
    
       // Still acknowledge receipt to prevent Vonage retries for DB issues
       if (!res.headersSent) {
           res.status(200).end();
       }
    } finally {
        // Optional: Add graceful shutdown logic for Prisma connection
        // process.on('SIGINT', async () => { await prisma.$disconnect(); process.exit(); });
    }
    // res.status(200).end() moved inside try/catch/finally logic
    • Similarly, update send-sms.js or the /api/send-sms endpoint to record outbound messages and update their status based on the Status Webhook (/webhooks/status). Remember to handle potential database errors gracefully in all data access points.

Frequently Asked Questions

how to send sms with node.js and vonage

Use the Vonage Messages API with the Node.js SDK. After setting up a Vonage application and number, initialize the Vonage client with your API credentials. Then, use `vonage.messages.send()` with the recipient's number, your Vonage number, and the message text. This function handles sending the SMS through the Vonage platform.

what is vonage messages api used for

The Vonage Messages API is a unified platform for sending and receiving messages across various channels like SMS, WhatsApp, Viber, and Facebook Messenger. This guide specifically focuses on SMS, enabling you to build applications that can send notifications, alerts, conduct basic customer interactions, or integrate SMS into existing workflows.

why use ngrok for vonage webhooks

Ngrok creates a secure tunnel from your local development server to the public internet, allowing Vonage to reach your webhook endpoints during development. Since Vonage delivers webhook events via HTTP requests to public URLs, ngrok provides a way to make your local server temporarily accessible externally.

how to receive sms messages in node.js

Create an Express.js route that handles POST requests to a specific webhook URL (e.g., `/webhooks/inbound`). Configure your Vonage application to send incoming SMS messages to this URL. The request body will contain message details like sender, recipient, and text. Respond to Vonage with a 200 OK status code to acknowledge receipt.

what is a vonage application id

A Vonage Application ID is a unique identifier for your application within the Vonage platform. It's essential for authenticating and authorizing access to Vonage APIs, including the Messages API used for sending SMS. The guide demonstrates how to create an application and obtain its ID in the Vonage Dashboard.

when should I use application id/private key authentication

Application ID/Private Key authentication is recommended for server-side applications interacting with Vonage APIs, especially those sending messages like SMS. This approach, as demonstrated in the provided code examples, enhances security over using API Key/Secret directly in your codebase.

how to set up vonage sms webhooks with express

Define POST routes in your Express application for inbound messages (`/webhooks/inbound`) and delivery receipts (`/webhooks/status`). Provide the ngrok HTTPS URL with these paths as webhooks in your Vonage Application settings. Ensure your Vonage number is linked to this application to receive incoming SMS and delivery status updates.

can I use a different port for my express server

Yes, you can change the port your Express server runs on. Modify the `PORT` environment variable in your `.env` file or the default port value in `server.js`. Remember to update the ngrok command and Vonage webhook URLs accordingly if you change the port.

what is the purpose of dotenv library

The `dotenv` library securely loads environment variables from a `.env` file into `process.env`. This allows you to store sensitive information like API keys and secrets outside your codebase, improving security and configuration management across different environments (development, staging, production).

how to handle vonage webhook delivery receipts

Set up a POST route (e.g., `/webhooks/status`) to receive DLRs. Vonage sends these to this URL when the message status changes (e.g., 'delivered', 'failed'). Parse `req.body` in this route to extract the message UUID, status, and timestamp. This information is crucial for tracking message delivery success and handling failures in production.

why respond with 200 ok to vonage webhooks

Responding with a 200 OK status code to Vonage webhooks is crucial to acknowledge message receipt and prevent retries. Vonage expects a prompt 200 OK, and if it doesn't receive this, it might assume failure and resend the webhook, potentially leading to duplicate message processing and incorrect behavior.

how to store received sms in a database

Define a database schema (e.g., using Prisma) with relevant fields like sender, recipient, message text, timestamps, and message status. Implement data access logic in your webhook handler (`/webhooks/inbound`) to save received SMS details to your database. For production applications, consider using a message queue for database operations to avoid blocking the webhook response.

what is the structure of the inbound sms webhook payload

The inbound SMS webhook payload is a JSON object containing message details. Refer to the Vonage API documentation for the exact structure, but it typically includes sender (`from`), recipient (`to`), message content (`text`), a unique message identifier (`message_uuid`), and a timestamp. The article provides examples of how to extract these fields from `req.body`.