code examples

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

Vonage (Nexmo) SMS Delivery Status & Callbacks with Node.js Express

Build production-ready SMS applications with Vonage Messages API, Express, and Node.js. Send SMS, receive delivery status callbacks, and handle inbound messages with webhooks.

Note: This filename references "messagebird" and "next-js" but the content covers Vonage (formerly Nexmo) with Express framework. The technical content and code examples are accurate for Vonage Messages API.

Last Updated: October 5, 2025 | Vonage Messages API Status: General Availability | SDK Version: @vonage/server-sdk v3.x+

Build a production-ready Node.js application with Express to send SMS messages, receive inbound SMS, and handle delivery status updates (callbacks) via the Vonage Messages API. This guide covers everything from project setup and core implementation to error handling, security, deployment, and testing.

Technologies: Node.js, Express, Vonage Messages API, Vonage Node SDK (@vonage/server-sdk v3.x+), ngrok (for development)

Official Documentation: Vonage Messages API | Vonage Node SDK

What You'll Build

Create a Node.js application using Express that can:

  1. Send SMS messages programmatically via the Vonage Messages API
  2. Receive incoming SMS messages sent to your Vonage virtual number via webhooks
  3. Track real-time delivery status updates for sent SMS messages via webhooks

Why Build This: Create reliable, two-way SMS communication in your Node.js environment. Send messages, confirm their delivery status, and react to user messages – enabling interactive SMS applications, notification systems with delivery confirmation, and more.

Technologies You'll Use:

  • Node.js: JavaScript runtime for building scalable network applications, ideal for I/O operations like API calls and webhooks
  • Express: Minimal, flexible Node.js framework with robust features for web and mobile applications – perfect for handling webhook requests
  • Vonage Messages API: Unified API for sending and receiving messages across SMS, MMS, WhatsApp, and more – focus on SMS with robust delivery status tracking
  • Vonage Node SDK (@vonage/server-sdk): Simplifies interaction with Vonage APIs in Node.js
  • ngrok (Development Only): Exposes your local development server to the public internet – necessary for Vonage webhooks to reach your machine during development
  • dotenv: Loads environment variables from a .env file into process.env – keeps sensitive credentials out of source code

System Architecture:

mermaid
graph LR
    subgraph Your Application
        direction LR
        A[Node.js/Express App]
        W_In[Inbound Webhook (/webhooks/inbound)]
        W_Stat[Status Webhook (/webhooks/status)]
        DB[(Database - Optional)]
    end

    subgraph Vonage Cloud
        direction LR
        VAPI[Messages API]
        VNum[Vonage Virtual Number]
    end

    User[End User Phone] -- Sends SMS --> VNum
    VNum -- Relays Inbound SMS --> VAPI
    VAPI -- POST --> W_In

    A -- Send SMS Request --> VAPI
    VAPI -- Sends SMS --> Carrier[Carrier Network]
    Carrier -- Delivers SMS --> User
    Carrier -- Sends DLR --> VAPI
    VAPI -- POST Status --> W_Stat

    A -- Optional --> DB{Store/Read Data}
    W_In -- Optional --> DB
    W_Stat -- Optional --> DB

What You'll Have at the End: A functional Node.js Express application that sends SMS, receives SMS, and tracks delivery statuses – structured with security, error handling, and deployment best practices.

Before You Start:

  • Vonage API Account: Sign up free at Vonage Dashboard – you get free credit to start
  • Node.js & npm (or yarn): Install Node.js (includes npm) from nodejs.org – LTS version 18.x or higher recommended (as of October 2025)
  • Vonage Virtual Number: Purchase an SMS-capable number from your Vonage Dashboard (Numbers > Buy Numbers)
  • ngrok: Download and set up from ngrok.com – free account is sufficient. Authenticate it: ngrok config add-authtoken YOUR_TOKEN
  • Basic knowledge of JavaScript, Node.js, Express, and REST APIs

1. Set Up Your Project

Initialize your Node.js project and install the necessary dependencies.

  1. Create Your Project Directory: Open your terminal and create a new directory for your project.

    bash
    mkdir vonage-sms-app
    cd vonage-sms-app
  2. Initialize Your Node.js Project: Initialize with npm (or yarn). The -y flag accepts default settings.

    bash
    npm init -y

    This creates a package.json file.

  3. Install Your Dependencies: Install Express for the web server, the Vonage Node SDK for API interaction, and dotenv for environment variable management. Install v3.x or higher of @vonage/server-sdk for Messages API support.

    bash
    npm install express @vonage/server-sdk dotenv

    Current Versions (October 2025):

    • express: v4.18.x or higher
    • @vonage/server-sdk: v3.15.x or higher
    • dotenv: v16.x or higher
  4. Create Your Project Structure: Build a basic structure for clarity.

    bash
    mkdir src config
    touch src/app.js .env .env.example .gitignore private.key
    • src/app.js: Your main application code
    • config/: (Optional) For complex configurations later
    • .env: Stores sensitive credentials (API keys, etc.) – never commit this file
    • .env.example: Template showing required environment variables – commit this file
    • .gitignore: Specifies files/directories Git should ignore
    • private.key: Placeholder for your Vonage Application private key – you'll replace this with the downloaded file from Vonage
  5. Configure Your .gitignore: Add common Node.js ignores and ensure .env and the private key are excluded.

    text
    # .gitignore
    
    # Dependencies
    node_modules/
    
    # Environment Variables
    .env*
    !.env.example
    
    # Vonage Private Key
    private.key
    
    # Logs
    logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    
    # OS generated files
    .DS_Store
    Thumbs.db
  6. Define Your Environment Variables (.env.example): List the variables you need. Copy this content into both .env and .env.example. Fill .env with actual values later.

    dotenv
    # .env.example
    # Vonage Application Credentials - Generated when creating a Vonage Application
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
    VONAGE_PRIVATE_KEY_PATH=./private.key
    
    # Vonage Number - Your purchased virtual number in E.164 format
    VONAGE_NUMBER=YOUR_VONAGE_NUMBER
    
    # Application Port
    APP_PORT=3000
    
    # Ngrok URL (for development reference - set dynamically or manually when running)
    # NGROK_BASE_URL=https://your-ngrok-subdomain.ngrok.io

    Why Use .env? Store credentials and configuration separate from code – crucial for security and flexibility across environments (development, staging, production).


2. Configure Vonage for SMS and Webhooks

Set up your Vonage account and application to enable SMS sending and receiving with callbacks.

Reference: Vonage Applications Documentation

  1. Log in to Your Vonage Dashboard: Access your account at dashboard.nexmo.com.

  2. Set Your Default SMS API to "Messages API":

    • Navigate to API Settings in the left menu
    • Scroll to SMS settings
    • Set Default SMS provider to Messages API
    • Click Save changes
    • Why Messages API? Provides a unified interface across SMS, WhatsApp, and MMS with consistent webhook payloads and modern authentication (JWT-based) – the current standard as of October 2025
  3. Create Your Vonage Application: Vonage Applications contain your communication configurations, including keys and webhooks.

    • Navigate to Applications > Create a new application
    • Enter an Application name (e.g., "Node Express SMS App")
    • Click Generate public and private key – this downloads the private.key file. Save this in your project root, replacing the placeholder private.key file. Vonage stores the public key. These keys enable JWT authentication for Messages API requests.
    • Enable the Messages capability by toggling it on
    • Two fields appear: Inbound URL and Status URL – you need public URLs for these (that's where ngrok comes in)
  4. Start ngrok: Open a new terminal window in your project directory and start ngrok, forwarding traffic to the port your Express app will use (defaulting to 3000 from .env.example).

    bash
    ngrok http 3000

    ngrok provides Forwarding URLs (http and https). Copy the https URL – looks like https://random-subdomain.ngrok.io.

  5. Configure Your Webhook URLs:

    • Return to your Vonage Application configuration page
    • Paste your ngrok https URL into the webhook fields, appending specific paths:
      • Inbound URL: YOUR_NGROK_HTTPS_URL/webhooks/inbound
      • Status URL: YOUR_NGROK_HTTPS_URL/webhooks/status
    • Why HTTPS? Always use HTTPS for webhook URLs – encrypts data in transit
    • Click Generate application
  6. Copy Your Application ID: After creation, you'll see the application details page. Copy the Application ID – you need this for your .env file.

  7. Link Your Vonage Number:

    • On the application details page, scroll to Link virtual numbers
    • Find your purchased Vonage virtual number and click Link – tells Vonage to send events for this number (like incoming SMS) to your application's webhooks
  8. Update Your .env File: Open your .env file (the one without .example) and fill in the values you obtained:

    dotenv
    # .env (Fill with your actual values)
    VONAGE_APPLICATION_ID=PASTE_YOUR_APPLICATION_ID_HERE
    VONAGE_PRIVATE_KEY_PATH=./private.key
    
    VONAGE_NUMBER=PASTE_YOUR_VONAGE_NUMBER_HERE # e.g., 14155550100
    
    APP_PORT=3000

    Security: .env contains secrets. Keep it in your .gitignore and never commit to version control. Use environment variable management from your deployment platform in production.


3. Write Your Core SMS Functionality

Build the Node.js/Express code to send and receive SMS.

File: src/app.js

javascript
// src/app.js

// 1. Import Dependencies
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const { Vonage } = require('@vonage/server-sdk'); // v3.x+ syntax
const path = require('path'); // Needed for constructing the private key path

// 2. Initialize Express App
const app = express();
app.use(express.json()); // Middleware to parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded request bodies

// 3. Initialize Vonage Client
// Ensure required environment variables are present
if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) {
    console.error('ERROR: Set VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH in your .env file');
    process.exit(1);
}
if (!process.env.VONAGE_NUMBER) {
    console.error('ERROR: Set VONAGE_NUMBER in your .env file');
    process.exit(1);
}

const privateKeyPath = path.resolve(process.env.VONAGE_PRIVATE_KEY_PATH);
let vonage;
try {
    vonage = new Vonage({
        applicationId: process.env.VONAGE_APPLICATION_ID,
        privateKey: privateKeyPath // Provide the absolute path to the key file
    });
    console.log("Vonage client initialized successfully.");
} catch (error) {
    console.error("Error initializing Vonage client:", error);
    process.exit(1);
}


// 4. Define Route for Sending SMS (Example)
// In production, **protect this endpoint** (e.g., API key check, user session authentication, rate limiting). Don't expose it publicly.
app.post('/send-sms', async (req, res) => {
    const { to, text } = req.body;

    // Basic input validation
    if (!to || !text) {
        return res.status(400).json({
            error: 'Missing required fields',
            message: 'Include both "to" (recipient number) and "text" (message content) in your request body'
        });
    }
    if (!process.env.VONAGE_NUMBER) {
        return res.status(500).json({
            error: 'Configuration error',
            message: 'Your Vonage number is missing from the configuration. Add VONAGE_NUMBER to your .env file.'
        });
    }

    console.log(`Attempting to send SMS from ${process.env.VONAGE_NUMBER} to ${to}`);

    try {
        const resp = await vonage.messages.send({
            channel: 'sms',
            message_type: 'text',
            to: to, // E.164 format (e.g., 14155550101)
            from: process.env.VONAGE_NUMBER, // Your Vonage number
            text: text
        });

        console.log('Message sent successfully:', resp);
        // resp contains { message_uuid: '...' } on success
        res.status(200).json({ message_uuid: resp.message_uuid, status: 'Message submitted' });

    } catch (err) {
        console.error('Error sending SMS:', err);
        // Provide more context if available
        const statusCode = err.response?.status || 500;
        const errorMessage = err.response?.data?.title || err.message || 'Failed to send SMS';
        const errorDetails = {
            error: errorMessage,
            details: err.response?.data,
            help: statusCode === 401 ? 'Check your VONAGE_APPLICATION_ID and private.key file' :
                  statusCode === 422 ? 'Verify your "to" number is in E.164 format (e.g., 14155550101)' :
                  statusCode === 429 ? 'You have exceeded the rate limit. Wait before retrying.' :
                  'Check the details field for more information'
        };
        res.status(statusCode).json(errorDetails);
    }
});

// 5. Define Webhook Endpoint for Inbound SMS
app.post('/webhooks/inbound', (req, res) => {
    console.log('--- Inbound SMS Received ---');
    console.log('Request Body:', JSON.stringify(req.body, null, 2));

    // TODO: Process the inbound message (req.body)
    // Example: Store in DB, trigger a response, etc.
    // Key fields: req.body.from, req.body.to, req.body.text, req.body.message_uuid, req.body.timestamp

    // Acknowledge receipt to Vonage immediately
    res.status(200).end();
    // Why 200? Vonage expects a 200 OK response. Without one
    // within the timeout period, Vonage assumes failure and retries the webhook,
    // potentially causing duplicate processing.
});

// 6. Define Webhook Endpoint for Delivery Status Updates
app.post('/webhooks/status', (req, res) => {
    console.log('--- Delivery Status Update Received ---');
    console.log('Request Body:', JSON.stringify(req.body, null, 2));

    // TODO: Process the status update (req.body)
    // Example: Update message status in DB using req.body.message_uuid
    // Key fields: req.body.message_uuid, req.body.status ('delivered', 'failed', 'rejected', 'accepted', 'buffered'),
    // req.body.timestamp, req.body.error (if failed/rejected)

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

// 7. Basic Health Check Endpoint
app.get('/health', (req, res) => {
    res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
});

// 8. Define Port and Start Server (Only if run directly)
const port = process.env.APP_PORT || 3000;
if (require.main === module) { // Check if the script is being run directly
    app.listen(port, () => {
        console.log(`Server listening at http://localhost:${port}`);
        // Remind user about ngrok if not in production environment
        if (process.env.NODE_ENV !== 'production') {
            console.log(`Ensure ngrok is running and forwarding to this port.`);
            console.log(`Configure these webhook URLs in your Vonage Application:`);
            console.log(`   Inbound: YOUR_NGROK_HTTPS_URL/webhooks/inbound`);
            console.log(`   Status:  YOUR_NGROK_HTTPS_URL/webhooks/status`);
        }
    });
}

// 9. Basic Error Handling Middleware (Optional but Recommended)
app.use((err, req, res, next) => {
    console.error('Unhandled Error:', err.stack || err);
    res.status(500).json({
        error: 'Internal server error',
        message: 'Something went wrong. Check the server logs for details.',
        details: process.env.NODE_ENV === 'development' ? err.message : undefined
    });
});

// 10. Export the app for testing
module.exports = app;

Code Breakdown:

  1. Dependencies: Imports necessary modules (dotenv, express, Vonage SDK, path). dotenv.config() loads variables from .env
  2. Express Init: Creates the Express app and adds middleware to parse incoming JSON and URL-encoded request bodies – crucial for handling webhook payloads
  3. Vonage Init: Initializes the Vonage client using applicationId and privateKey path from environment variables. Includes error checking for missing variables. Uses path.resolve to ensure the path to private.key is correct regardless of where you run the script
  4. /send-sms Route: A POST endpoint to send SMS. Expects to (recipient number) and text (message content) in the JSON body. Validates input and calls vonage.messages.send() using the sms channel. Returns success/error responses with helpful messages. Includes a warning about production protection
  5. /webhooks/inbound Route: A POST endpoint matching your Inbound URL in the Vonage Application. Vonage sends data here when your virtual number receives SMS. Logs the payload (req.body), includes a TODO for your logic, and immediately sends 200 OK. Send the 200 OK quickly before heavy processing.
  6. /webhooks/status Route: A POST endpoint matching your Status URL. Vonage sends delivery status updates here for messages you sent. Logs the payload, includes a TODO, and sends 200 OK
  7. /health Route: Simple endpoint for monitoring systems to check if your application is running
  8. Server Start: Reads the port from APP_PORT (defaulting to 3000) and starts the Express server only if you run the script directly (using if (require.main === module)). Includes helpful reminders about ngrok configuration during development
  9. Error Handler: Basic Express error-handling middleware to catch unhandled errors in route handlers
  10. Export: Exports the app instance for testing frameworks like Supertest

4. Run and Test Locally

  1. Ensure ngrok is running (from Step 2.4) and forwarding to port 3000. Verify the HTTPS URL matches the one in your Vonage Application webhook settings.

  2. Verify your .env file contains your Vonage Application ID, number, and the correct path to private.key.

  3. Start Your Node.js Application: In the terminal where you created the project (not the ngrok one), run:

    bash
    node src/app.js

    You should see "Server listening at http://localhost:3000" and the ngrok reminder messages.

  4. Test Sending SMS: Use curl or Postman to send a POST request to your local /send-sms endpoint. Replace YOUR_PHONE_NUMBER with your mobile number in E.164 format (e.g., 14155550101).

    bash
    curl -X POST http://localhost:3000/send-sms \
    -H "Content-Type: application/json" \
    -d '{
      "to": "YOUR_PHONE_NUMBER",
      "text": "Hello from Node.js Vonage App!"
    }'
    • Expected Result:
      • Your terminal running node src/app.js logs "Attempting to send SMS…" then "Message sent successfully: …" with a message_uuid
      • The curl command receives JSON: {"message_uuid":"...","status":"Message submitted"}
      • You receive the SMS on your phone shortly
  5. Test Receiving Status Updates:

    • Wait a few moments after sending the SMS
    • Expected Result:
      • Your terminal running node src/app.js logs "--- Delivery Status Update Received ---" followed by the JSON payload from Vonage. Look for the message_uuid matching your sent message and a status field (e.g., delivered, accepted, buffered) – exact status depends on carrier support
  6. Test Receiving Inbound SMS:

    • From your mobile phone, send an SMS to your Vonage virtual number
    • Expected Result:
      • Your terminal running node src/app.js logs "--- Inbound SMS Received ---" followed by the JSON payload containing message details (from, text, etc.)

5. Implement Error Handling, Logging, and Retries

Build production-ready error handling and logging.

Best Practices (October 2025):

  • Error Handling:

    • Vonage SDK Errors: The try...catch block around vonage.messages.send() handles specific API call errors. Inspect the err object (especially err.response.data or err.message) for details from the Vonage API
    • Common Error Codes:
      • 401 (authentication failed) – Check your VONAGE_APPLICATION_ID and private.key file
      • 403 (forbidden) – Verify permissions and account status
      • 422 (validation error) – Ensure phone numbers are in E.164 format
      • 429 (rate limit exceeded) – Implement exponential backoff retries
      • 500 (server error) – Retry with exponential backoff
    • Webhook Errors: Wrap logic inside your webhook handlers (/webhooks/inbound, /webhooks/status) in try...catch blocks. Log errors but still send a 200 OK response to Vonage to prevent retries (unless the request is fundamentally invalid)
    • Unhandled Errors: The Express error handler (app.use((err, req, res, next) => {...})) catches errors not caught in specific routes
  • Logging:

    • Use pino (recommended for performance) or winston in production instead of console.log
    • Benefits: Structured logging (JSON format), configurable log levels (debug, info, warn, error), ability to write to files or external services (Datadog, Logstash)
    • What to Log: Key events (app start, SMS sent/received, status updates), errors with stack traces, important request details (message_uuid), configuration issues
    • Security: Never log sensitive data like full phone numbers (mask them) or private keys
  • Retries:

    • Vonage Webhook Retries: Vonage automatically retries webhooks if they don't receive 200 OK within ~30 seconds. Respond quickly with 200 OK
    • Retry Schedule: Vonage uses exponential backoff (1s, 5s, 15s, 60s) for up to 24 hours
    • Application-Level Retries (Sending): If sending SMS fails due to temporary issues (network hiccup, 5xx error from Vonage), implement retry strategy with exponential backoff. Use libraries like async-retry
    • Application-Level Retries (Webhook Processing): If processing within a webhook handler fails after sending 200 OK (e.g., database write fails), log the error and use a background job queue (BullMQ, Kue, RabbitMQ) to retry the processing task independently

Integrate a database for persistence and tracking.

Recommended ORMs/ODMs (October 2025):

  • Prisma (SQL databases – PostgreSQL, MySQL, SQLite): Modern, type-safe ORM with excellent Developer Experience
  • Drizzle ORM (SQL databases): Lightweight, TypeScript-first ORM
  • Sequelize (SQL databases): Mature, feature-rich ORM
  • Mongoose (MongoDB): De-facto standard for MongoDB with Node.js

Design Your Schema: Create a messages table:

  • message_uuid (VARCHAR/TEXT, PRIMARY KEY/UNIQUE) - From Vonage response/webhooks
  • direction (ENUM/VARCHAR: 'outbound', 'inbound')
  • to_number (VARCHAR)
  • from_number (VARCHAR)
  • body (TEXT)
  • status (VARCHAR, e.g., 'submitted', 'delivered', 'failed', 'read') - Updated via status webhook
  • vonage_timestamp (TIMESTAMP WITH TIME ZONE) - From webhook payload
  • error_code (VARCHAR, nullable) - From status webhook if failed/rejected
  • created_at (TIMESTAMP WITH TIME ZONE)
  • updated_at (TIMESTAMP WITH TIME ZONE)

Implement Your Database Logic:

  • Sending: After successfully calling vonage.messages.send(), insert a record with message_uuid, direction='outbound', to, from, text, status='submitted', and timestamps
  • Status Webhook: Find the message by req.body.message_uuid and update its status, vonage_timestamp, error_code (if present), and updated_at. Handle cases where the message_uuid might not be found (log an error)
  • Inbound Webhook: Insert a new record with direction='inbound', details from req.body, status='received', and timestamps

7. Secure Your Application

Protect your application and user data.

Secure Your Webhooks:

  1. Webhook URLs are public. The Vonage Messages API uses JWT-based authentication when you send messages, but securing incoming webhook endpoints requires standard practices:
    • HTTPS: Essential – encrypts data in transit. Always use https URLs for webhooks (required by Vonage as of October 2025)
    • Endpoint Secrecy: Avoid overly simple webhook paths. The paths /webhooks/inbound and /webhooks/status are reasonably specific
    • Verify Source (Advanced): Check the source IP address of incoming webhook requests against published Vonage IP ranges (available in Vonage documentation, but subject to change)
    • JWT Verification (Advanced): While Messages API webhooks don't include signed JWTs by default (as of October 2025), implement custom verification headers if needed

Manage Your Secrets:

  • Use environment variables (.env locally, platform-provided secrets in production). Don't commit .env or private.key
  • Rotate keys periodically if your security policy requires it

Validate Your Input:

  • Validate payloads for your endpoints (like /send-sms). Ensure required fields are present, types are correct, and formats match (e.g., E.164 for phone numbers). Use libraries like express-validator or joi
  • Sanitize input if you store or display user-provided text (like inbound SMS content) to prevent XSS attacks. Use proper database parameterization or libraries like dompurify (if rendering in HTML)

Implement Rate Limiting:

  • Protect public-facing endpoints (especially /send-sms if exposed) from abuse. Use middleware like express-rate-limit

Use HTTPS:

  • Always run your production application behind HTTPS. ngrok provides this locally. Use a load balancer or reverse proxy (Nginx, Caddy, or platform services like AWS ALB/Cloudflare) to handle SSL termination in production

8. Handle Special SMS Cases

Address real-world SMS nuances.

Format Your Numbers:

  • Always use E.164 format (e.g., +14155550101 or 14155550101 without spaces) when sending to numbers to Vonage. Vonage provides from numbers in webhooks in similar format
  • Reference: E.164 Format

Understand Character Encoding & Concatenation:

  • Standard SMS (GSM-7): 160 characters per segment
  • Unicode (UCS-2): 70 characters per segment (required for emojis, non-Latin scripts)
  • Concatenated messages: 153 chars (GSM-7) or 67 chars (UCS-2) per segment when splitting longer messages
  • Vonage handles sending long messages automatically – you pay per segment
  • Reference: SMS Concatenation and Encoding

Handle Opt-Outs (STOP):

  • Vonage automatically handles standard opt-out keywords (STOP, UNSUBSCRIBE, QUIT) for US/Canada long codes by default
  • Configure this in Settings > SMS in the Vonage Dashboard
  • Respect opt-outs and maintain an opt-out list in your application

Interpret Delivery Statuses:

  • Not all carriers reliably provide delivered status updates (DLRs – Delivery Receipt Reports)
  • Common statuses: submitted, accepted, buffered, delivered, failed, rejected, expired
  • Design your logic to handle missing or delayed DLRs
  • failed or rejected statuses reliably indicate non-delivery

Manage Time Zones:

  • Timestamps in Vonage webhooks (timestamp) are in UTC (ISO 8601 format: YYYY-MM-DDTHH:MM:SS.sssZ)
  • Store timestamps in UTC in your database and convert to local time zones only for display

9. Optimize Your Performance

Ensure your webhook handlers are fast and efficient.

Respond Quickly:

  • Send your 200 OK response within 30 seconds (Vonage timeout). Aim for <1 second response time – this is your most critical optimization

Process in the Background:

  • Recommended Solutions: BullMQ (Redis-based, modern), Celery (if using Python workers), AWS SQS, Google Cloud Tasks
  • Pattern: Webhook receives data → Validates → Queues job → Returns 200 OK → Worker processes job asynchronously

Index Your Database:

  • Index columns used for lookups:
    • message_uuid (PRIMARY KEY or UNIQUE index)
    • status (if you query by status frequently)
    • created_at (for time-range queries)

Cache Frequently Accessed Data:

  • Use Redis or in-memory caching (node-cache) for frequently accessed data

10. Monitor Your Application

Understand how your application performs and diagnose issues.

Implement Health Checks:

  • The /health endpoint is your starting point. Production monitoring systems (AWS CloudWatch, Datadog, Prometheus/Grafana, UptimeRobot) can ping this endpoint to verify your app is live

Track Your Metrics:

  • Monitor these key performance indicators (KPIs):
    • Webhook request rate (inbound, status)
    • Webhook response times (should be very low)
    • SMS sending rate
    • API call latency (Vonage send calls)
    • Error rates (webhook failures, API call failures)
    • Queue sizes (if using background jobs)
    • Use libraries like prom-client for Prometheus or integrate with APM tools (Datadog APM, New Relic)

Track Your Errors:

  • Integrate services like Sentry or Bugsnag to capture, aggregate, and alert on application errors in real-time

Aggregate Your Logs:

  • Ship logs to a centralized platform (ELK stack, Datadog Logs, Splunk) for easier searching and analysis

Create Your Dashboards:

  • Visualize key metrics and log trends using Grafana, Kibana, or your APM provider's dashboarding features

11. Troubleshoot Common Issues

Solve common problems and their solutions.

Webhooks Not Reaching Your App:

  • ngrok: Is ngrok running? Free URLs expire after 2-8 hours – restart ngrok and update Vonage Application webhook URLs
  • Vonage Config: Verify webhook URLs in Application settings match your ngrok HTTPS URL + exact paths
  • HTTPS Required: Vonage requires HTTPS for webhooks (as of October 2025) – HTTP URLs are rejected
  • Firewall: If deployed, allow incoming traffic from Vonage IP ranges
  • App Running: Check application logs for startup errors

Your App Receives Webhooks but Errors Occur:

  • Check Logs: Look for errors in your Node.js application console output or log files
  • Payload Issues: Are you parsing the req.body correctly? Ensure express.json() middleware is used
  • Missing 200 OK: Does your handler send res.status(200).end() reliably and quickly? Check the Vonage Dashboard (Logs section) for webhook retry attempts

SMS Sending Fails:

  • Credentials: Double-check VONAGE_APPLICATION_ID and the path/content of VONAGE_PRIVATE_KEY_PATH in .env
  • Vonage Balance: Do you have credit in your Vonage account?
  • 'To' Number: Is the recipient number valid and in E.164 format?
  • 'From' Number: Is VONAGE_NUMBER correctly set in .env and linked to the Vonage Application used for initialization?
  • API Errors: Log the err object from the catch block in /send-smserr.response.data often contains detailed error codes and messages from Vonage

No Delivery Status Updates (/webhooks/status not hit):

  • Status URL Config: Verify the Status URL is correctly set in your Vonage Application
  • Carrier Support: DLRs are carrier-dependent – not all networks/countries provide reliable delivery receipts
  • Number Type: Some number types (e.g., short codes) might have different DLR behavior

Messages API vs. Legacy SMS API:

  • Critical: Use the Messages API consistently (not the legacy SMS API)
  • SDK method: vonage.messages.send() (Messages API) vs. vonage.sms.send() (legacy SMS API)
  • Webhook structure differs between APIs
  • Set "Default SMS provider" to "Messages API" in Dashboard > API Settings
  • Application-specific webhooks (used here) are for Messages API – account-level webhooks (in main Settings) are for legacy SMS API

12. Deploy Your Application

Move from local development to production.

Choose Your Deployment Platform (October 2025):

  1. Serverless/PaaS Options:

    • Vercel (Edge Functions): Excellent for Next.js, supports Node.js functions
    • AWS Lambda + API Gateway: Serverless, pay-per-request pricing
    • Google Cloud Run: Containerized apps, auto-scaling
    • Heroku: Simple PaaS for prototypes (note: free tier deprecated in late 2022)
    • Railway.app: Modern alternative to Heroku
    • Fly.io: Global edge deployment
    • DigitalOcean App Platform: Simple containerized deployments
  2. Container/VM Options:

    • AWS ECS/EKS: Production-grade container orchestration
    • Google Kubernetes Engine (GKE): Kubernetes-managed
    • DigitalOcean Droplets: Traditional VMs
    • Linode/Vultr: Cost-effective VPS options
  3. Handle Your Private Key Securely: Provide the private.key file to your production environment securely:

    • Option 1 (Recommended): Store the key content as base64 in environment variable VONAGE_PRIVATE_KEY_BASE64, decode at runtime
    • Option 2: Use secrets management: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault
    • Option 3: For serverless: Include private.key in deployment bundle (ensure it's in .gitignore)
    • Never commit private.key to version control
  4. Run Your Build Process: If using TypeScript or a build step, ensure it runs before deployment

  5. Manage Your Process: Use a process manager like pm2 or rely on the platform's built-in service management (e.g., systemd, Heroku dynos) to keep your Node.js app running, handle restarts, and manage logs

  6. Configure HTTPS: Ensure your deployment platform handles HTTPS termination (most PaaS/Serverless platforms do) or configure a reverse proxy (Nginx, Caddy) if deploying to a VM

  7. Set Up Your CI/CD Pipeline: Modern CI/CD tools:

    • GitHub Actions: Built into GitHub, excellent for open source
    • GitLab CI/CD: Integrated with GitLab repositories
    • CircleCI: Popular third-party CI/CD
    • Jenkins: Self-hosted, highly customizable
    • Typical Pipeline: Lint → Unit Tests → Integration Tests → Build → Deploy to Staging → E2E Tests → Deploy to Production