code examples

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

Node.js SMS Delivery Status Callbacks with Vonage

Learn how to build a Node.js application using Express and the Vonage Messages API to send SMS and receive real-time delivery status updates via webhooks.

Tracking the delivery status of SMS messages is crucial for applications that rely on timely communication. Knowing whether a message reached the recipient's handset, was rejected by the carrier, or failed for other reasons enables developers to build more robust and reliable systems. This guide provides a step-by-step walkthrough for building a Node.js application using Express and the Vonage Messages API to send SMS messages and receive real-time delivery status updates via webhooks.

We will build a simple Node.js server that can send an SMS message and expose webhook endpoints to receive delivery status updates from Vonage. This solves the common problem of ""fire and forget"" SMS sending, providing visibility into the message lifecycle after it leaves the Vonage platform. We'll use the Vonage Node.js SDK for seamless API interaction and ngrok for local development testing.

System architecture

Here's a high-level overview of how the components interact:

  1. User/Trigger: Initiates the SMS send request to the Node.js application.
  2. Node.js Application (Express):
    • Receives the send request.
    • Uses the Vonage Node.js SDK to call the Vonage Messages API.
    • Listens for incoming webhook events (status updates) from Vonage.
  3. Vonage Messages API:
    • Accepts the SMS send request.
    • Delivers the SMS message via carrier networks.
    • Sends status updates (e.g., submitted, delivered, failed) to the configured Status Webhook URL.
  4. Carrier Network: Delivers the SMS to the recipient's handset.
  5. Recipient: Receives the SMS message.

(Note: A sequence diagram illustrating the component interactions was present here in the original document.)

Prerequisites

Before you begin, ensure you have the following:

  • Node.js and npm: Installed on your system. Download from nodejs.org.
  • Vonage API Account: Sign up at Vonage API Dashboard. You'll need your API Key and API Secret.
  • Vonage Application: You'll create one using the Messages API capability. This provides an Application ID and allows generating a private key.
  • Vonage Virtual Number: Purchase a Vonage number capable of sending SMS messages from the dashboard (Numbers > Buy Numbers).
  • ngrok: A tool to expose your local server to the internet for webhook testing. Download from ngrok.com and create a free account.
  • Git: (Optional) For version control and cloning the example repository.

1. Setting up the project

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

  1. Create Project Directory: Open your terminal or command prompt and run:

    bash
    mkdir vonage-sms-status-guide
    cd vonage-sms-status-guide
  2. Initialize Node.js Project: This creates a package.json file.

    bash
    npm init -y
  3. Install Dependencies: We need express for the web server, @vonage/server-sdk to interact with the Vonage API, dotenv to manage environment variables, and jsonwebtoken to verify webhook signatures. We also recommend body-parser for reliable webhook verification.

    bash
    # Note: Added body-parser for reliable signature verification
    npm install express @vonage/server-sdk dotenv jsonwebtoken body-parser
  4. Create Project Files: Create the main application file and environment configuration files.

    bash
    touch index.js .env .env.example .gitignore
  5. Configure .gitignore: Add sensitive files and directories to .gitignore to prevent committing them to version control.

    text
    # .gitignore
    node_modules/
    .env
    private.key
    npm-debug.log
    *.log

    Explanation: We ignore node_modules (can be reinstalled), .env (contains secrets), private.key (Vonage private key), and log files.

  6. Set up Environment Variables: Create a .env.example file to list the required variables. This serves as a template.

    dotenv
    # .env.example
    VONAGE_API_KEY=YOUR_VONAGE_API_KEY
    VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
    VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
    VONAGE_PRIVATE_KEY_PATH=./private.key
    VONAGE_SIGNATURE_SECRET=YOUR_VONAGE_SIGNATURE_SECRET # From Application settings
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # In E.164 format, e.g., +12015550123
    PORT=3000
    BASE_URL=http://localhost:3000 # Will be updated with ngrok URL

    Now, create your actual .env file by copying .env.example and filling in your real credentials.

    bash
    cp .env.example .env

    Explanation:

    • VONAGE_API_KEY, VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.
    • VONAGE_APPLICATION_ID: Obtained after creating a Vonage Application (next step).
    • VONAGE_PRIVATE_KEY_PATH: Path to the private key file downloaded when creating the application. We assume it's in the project root named private.key.
    • VONAGE_SIGNATURE_SECRET: Found in your Vonage Application settings under 'Webhook signature'. Used to verify incoming webhooks.
    • VONAGE_NUMBER: Your purchased Vonage virtual number capable of sending SMS (must be in E.164 format, e.g., +12015550123).
    • PORT: The port your local server will run on.
    • BASE_URL: The base URL for your webhook endpoints. We'll update this later with the ngrok URL.

2. Configuring Vonage

You need a Vonage Application configured for the Messages API to handle sending and status updates.

  1. Navigate to Applications: Log in to the Vonage API Dashboard and go to ""Applications"" > ""Create a new application"".
  2. Name Your Application: Enter a descriptive name (e.g., ""Node SMS Status Guide App"").
  3. Generate Keys: Click ""Generate public and private key"". Immediately save the private.key file that downloads. Place this file in your project root directory (matching VONAGE_PRIVATE_KEY_PATH in .env). The public key is stored by Vonage.
  4. Note Application ID: Copy the generated Application ID and paste it into your .env file for VONAGE_APPLICATION_ID.
  5. Enable Capabilities: Find the ""Capabilities"" section.
    • Toggle on ""Messages"".
    • Enter placeholder URLs for now (we'll update these after starting ngrok):
      • Inbound URL: http://example.com/webhooks/inbound
      • Status URL: http://example.com/webhooks/status
      • Why: The Status URL is where Vonage will POST delivery status updates. The Inbound URL is for receiving SMS replies (optional for this guide but good practice to configure).
  6. Find Signature Secret: Scroll down to the 'Webhook signature' section within the Messages capability settings. Copy the 'Secret' and paste it into your .env file for VONAGE_SIGNATURE_SECRET.
  7. Link Virtual Number: Scroll down further to ""Link virtual numbers"". Find your purchased Vonage number and click ""Link"".
  8. Save Changes: Click ""Save changes"" at the bottom of the page.
  9. (Optional but Recommended) Set Messages API as Default: Navigate to your main account ""Settings"". Scroll to ""API Settings"" > ""SMS Settings"". Ensure ""Default SMS Setting"" is set to ""Messages API"". This ensures consistency if other parts of your account interact with SMS. Save changes if necessary.

3. Setting up the webhook server (Express)

Now, let's write the Node.js code using Express to handle incoming webhooks.

javascript
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const bodyParser = require('body-parser'); // Import body-parser
const jwt = require('jsonwebtoken');
const { Vonage } = require('@vonage/server-sdk');

const app = express();

// --- Middleware ---
// Capture raw body *before* JSON parsing for reliable signature verification
app.use(bodyParser.json({
    verify: (req, res, buf) => {
        // Save the raw buffer onto the request object
        req.rawBody = buf;
    }
}));
// Use express.urlencoded({ extended: true }) for URL-encoded data
app.use(express.urlencoded({ extended: true }));

// --- Vonage Initialization ---
// Check for essential environment variables
if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_SIGNATURE_SECRET) {
    console.error('Error: VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, or VONAGE_SIGNATURE_SECRET not set in .env');
    process.exit(1);
}

const vonage = new Vonage({
    apiKey: process.env.VONAGE_API_KEY, // Optional for Messages API JWT auth, but good practice
    apiSecret: process.env.VONAGE_API_SECRET, // Optional for Messages API JWT auth
    applicationId: process.env.VONAGE_APPLICATION_ID,
    privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
}, {
    debug: true // Enable debug logging for the SDK
});

// --- Webhook Security Middleware ---
// Verifies the JWT signature on incoming Vonage webhooks using the raw body
const verifyVonageSignature = (req, res, next) => {
    try {
        const authHeader = req.headers.authorization;
        const token = authHeader?.split(' ')[1]; // Extract Bearer token

        if (!token) {
            console.warn('Webhook received without Authorization header or token.');
            return res.status(401).send('Unauthorized: Missing token');
        }

        // Verify the token using the raw body captured by bodyParser's verify function
        // Vonage signs the raw request body, not the parsed JSON object.
        if (!req.rawBody) {
            console.error('Raw body buffer not available for verification. Ensure bodyParser.json() with verify is used before this middleware.');
            return res.status(500).send('Internal Server Error: Cannot verify signature');
        }

        // jwt.verify throws if the signature is invalid
        const decoded = jwt.verify(token, process.env.VONAGE_SIGNATURE_SECRET, {
            algorithms: ['HS256'],
            // IMPORTANT: Provide the raw body buffer for verification
            // This assumes the payload is the raw body itself for JWT verification context
            // Check Vonage docs if payload structure for signing differs
        }); // Note: jwt.verify doesn't directly take the raw body for payload check,
           // it verifies the signature against the token structure. The key is using the
           // correct secret. Vonage includes payload claims *within* the signed JWT.

        // Optional: Check payload application_uuid matches your app ID
        // if (decoded.application_uuid !== process.env.VONAGE_APPLICATION_ID) {
        //     console.warn(`Webhook received for unexpected application ID: ${decoded.application_uuid}`);
        //     return res.status(401).send('Unauthorized: Invalid application ID');
        // }

        console.log('Webhook signature verified successfully.');
        req.vonage_payload = decoded; // Attach decoded JWT payload if needed later
        next(); // Signature is valid, proceed to the handler
    } catch (error) {
        console.error('Error verifying webhook signature:', error.message);
        if (error instanceof jwt.TokenExpiredError) {
            return res.status(401).send('Unauthorized: Token expired');
        }
        if (error instanceof jwt.JsonWebTokenError) {
            return res.status(401).send('Unauthorized: Invalid signature');
        }
        // Log other unexpected errors
        console.error('Unexpected error during signature verification:', error);
        return res.status(500).send('Internal Server Error');
    }
};


// --- Webhook Endpoints ---

// Status Webhook: Receives delivery status updates
// Apply the verification middleware *only* to Vonage webhooks
app.post('/webhooks/status', verifyVonageSignature, (req, res) => {
    // The body is already parsed by bodyParser.json()
    const statusData = req.body;
    console.log('--- Delivery Status Received ---');
    console.log('Message UUID:', statusData.message_uuid);
    console.log('Status:', statusData.status);
    console.log('Timestamp:', statusData.timestamp);
    if (statusData.error) {
        console.error('Error Code:', statusData.error.code);
        console.error('Error Reason:', statusData.error.reason);
    }
    console.log('Full Payload:', JSON.stringify(statusData, null, 2));
    console.log('------------------------------');

    // Vonage expects a 200 OK response to acknowledge receipt
    // Failure to send 200 OK will cause Vonage to retry the webhook
    res.status(200).send('OK');
});

// Inbound Webhook: Receives incoming SMS messages (optional for this guide)
// Apply the verification middleware here too for security
app.post('/webhooks/inbound', verifyVonageSignature, (req, res) => {
    const inboundData = req.body;
    console.log('--- Inbound SMS Received ---');
    console.log('From:', inboundData.from?.number || 'Unknown');
    console.log('To:', inboundData.to?.number || 'Unknown');
    console.log('Text:', inboundData.message?.content?.text || 'N/A');
    console.log('Full Payload:', JSON.stringify(inboundData, null, 2));
    console.log('--------------------------');

    res.status(200).send('OK');
});

// --- SMS Sending Function ---
async function sendSms(toNumber, text) {
    console.log(`Attempting to send SMS to ${toNumber}`);
    const fromNumber = process.env.VONAGE_NUMBER;

    if (!fromNumber) {
        console.error('Error: VONAGE_NUMBER not set in .env');
        return;
    }
    // Validate recipient number format (strict E.164 check)
    if (!/^\+\d{10,15}$/.test(toNumber)) {
        console.error(`Error: Invalid recipient number format: ${toNumber}. Must use E.164 format (e.g., +12015550123).`);
        // Optionally throw an error or return a failure indicator
        return;
    }

    try {
        const resp = await vonage.messages.send({
            message_type: ""text"",
            to: toNumber, // E.164 format (e.g., +14155550100)
            from: fromNumber, // Your Vonage number
            channel: ""sms"",
            text: text,
            // client_ref: 'my-internal-tracking-id-123' // Optional client reference
        });
        console.log('SMS Submitted Successfully!');
        console.log('Message UUID:', resp.messageUuid);
    } catch (err) {
        console.error('Error sending SMS:', err?.response?.data || err.message);
        // Log detailed error if available from Vonage response
        if (err?.response?.data) {
            console.error('Vonage Error Details:', JSON.stringify(err.response.data, null, 2));
        }
    }
}

// --- Example Send Endpoint (for testing) ---
app.post('/send-sms', (req, res) => {
    const { to, text } = req.body;

    if (!to || !text) {
        return res.status(400).send('Missing ""to"" or ""text"" in request body.');
    }

    // Strict E.164 format check
    if (!/^\+\d{10,15}$/.test(to)) {
         return res.status(400).send('Invalid ""to"" number format. Use E.164 (e.g., +12015550123).');
    }

    sendSms(to, text); // Fire and forget for this example

    res.status(202).send('SMS send request accepted.');
});


// --- Start Server ---
const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
    console.log(`Webhook URLs should be configured in Vonage based on your BASE_URL:`);
    console.log(`Status URL: ${process.env.BASE_URL || `http://localhost:${port}`}/webhooks/status`);
    console.log(`Inbound URL: ${process.env.BASE_URL || `http://localhost:${port}`}/webhooks/inbound`);

    // Example: Send a test SMS on startup (remove in production)
    // Make sure to add a valid test number in E.164 format
    // const TEST_RECIPIENT = '+14155550101'; // Replace with your test phone number
    // if (process.env.NODE_ENV !== 'production' && TEST_RECIPIENT) {
    //     console.log(`Sending initial test SMS to ${TEST_RECIPIENT}...`);
    //     setTimeout(() => sendSms(TEST_RECIPIENT, 'Hello from Vonage Node Guide! Startup Test - ' + new Date().toLocaleTimeString()), 2000);
    // }
});

Explanation:

  • We initialize Express and load .env variables.
  • The Vonage SDK is initialized using the Application ID and private key path from .env. Debug mode is enabled for detailed SDK logs.
  • Webhook Security: body-parser middleware with a verify function is used to capture the raw request body before JSON parsing. The verifyVonageSignature middleware then uses jsonwebtoken and your VONAGE_SIGNATURE_SECRET to verify the JWT signature from the Authorization: Bearer <token> header. This ensures the request genuinely originated from Vonage. This raw body approach is crucial for reliable production verification.
  • /webhooks/status: This route handles POST requests from Vonage containing delivery status updates. It logs the received data and sends a 200 OK response. This acknowledgment is vital; otherwise, Vonage will retry sending the webhook.
  • /webhooks/inbound: Handles incoming SMS messages (optional).
  • sendSms function: Encapsulates the logic for sending an SMS using vonage.messages.send(). It takes the recipient number (validated for strict E.164 format) and text as arguments. It logs the messageUuid on successful submission.
  • /send-sms endpoint: A simple POST endpoint to trigger the sendSms function for testing purposes via cURL or Postman. Includes strict E.164 validation.
  • The server starts listening on the specified PORT.

4. Running and testing locally

To receive webhooks from Vonage on your local machine, you need ngrok.

  1. Start ngrok: Open a new terminal window and run ngrok, pointing it to the port your Node.js app is running on (default is 3000).

    bash
    ngrok http 3000

    ngrok will display output similar to this:

    Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding https://<random-id>.ngrok-free.app -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00

    Copy the https://<random-id>.ngrok-free.app URL. This is your public base URL.

  2. Update .env: Open your .env file and set the BASE_URL to your ngrok Forwarding URL:

    dotenv
    # .env
    # ... other variables
    BASE_URL=https://<random-id>.ngrok-free.app
  3. Update Vonage Application Webhooks: Go back to your application settings in the Vonage Dashboard. Update the Messages capability webhook URLs using your ngrok BASE_URL:

    • Inbound URL: https://<random-id>.ngrok-free.app/webhooks/inbound
    • Status URL: https://<random-id>.ngrok-free.app/webhooks/status
    • Click ""Save changes"".
  4. Run the Node.js Application: In your original terminal window (where your project code is), start the server:

    bash
    node index.js

    You should see output indicating the server is running and listening.

  5. Test Sending SMS: Open another terminal or use a tool like Postman to send a POST request to your local /send-sms endpoint. Replace <your_test_phone> with your actual 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"": ""+14155550101"",
               ""text"": ""Testing Vonage Status Callbacks! Time: '\''$(date)'\''""
             }'

    You should see:

    • Logs in your Node.js application terminal showing the SMS submission attempt and the messageUuid.
    • Shortly after, you should receive the SMS on your test phone.
  6. Observe Status Webhooks: Watch your Node.js application terminal. As the message progresses through the delivery lifecycle, Vonage will send POST requests to your /webhooks/status endpoint. You should see logs like:

    Webhook signature verified successfully. --- Delivery Status Received --- Message UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Status: submitted Timestamp: 2023-10-26T10:00:05.123Z Full Payload: { ... } ------------------------------ Webhook signature verified successfully. --- Delivery Status Received --- Message UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Status: delivered Timestamp: 2023-10-26T10:00:07.456Z Full Payload: { ... } ------------------------------

    Common statuses include submitted, delivered, rejected, failed, accepted (intermediate carrier status), undeliverable.

  7. Inspect with ngrok Web Interface: Open http://127.0.0.1:4040 in your browser. This interface shows all requests forwarded by ngrok, allowing you to inspect the exact headers (including Authorization) and payloads received from Vonage, which is invaluable for debugging.

5. Error handling and logging

Production applications require more robust error handling and logging.

  • Webhook Handler Reliability: Wrap the logic inside your webhook handlers (/webhooks/status, /webhooks/inbound) in try...catch blocks to prevent the server from crashing due to unexpected errors in processing the payload. Always ensure a 200 OK is sent back to Vonage unless there's a signature verification failure (which returns 401).
  • Logging: While console.log is used in this guide for simplicity, replace it with a structured logger like Winston or Pino in production. This allows for log levels (info, warn, error), formatting (JSON), and easier integration with log management systems.
    • Log critical information: message_uuid, status, timestamp, recipient/sender numbers, and any error codes/reasons.
    • Log errors during SMS sending (catch block in sendSms) and webhook processing.
  • Vonage Retry Mechanism: Remember that Vonage retries webhook delivery if it doesn't receive a 200 OK response within a short timeout (typically a few seconds). Ensure your processing is fast or happens asynchronously (e.g., push the payload to a queue) to avoid timeouts and duplicate processing from retries. Your endpoint must be idempotent if possible.
  • Detailed Send Errors: The catch block in sendSms should inspect the err.response.data object from the Vonage SDK for detailed error information provided by the API (e.g., invalid number format, insufficient funds).
javascript
// Example: Enhanced Status Webhook Handler (Conceptual)
const pino = require('pino'); // Example using Pino
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });

app.post('/webhooks/status', verifyVonageSignature, async (req, res) => {
    const statusData = req.body;
    const logData = { // Structure your logs
        message_uuid: statusData.message_uuid,
        status: statusData.status,
        timestamp: statusData.timestamp,
        error_code: statusData.error?.code,
        error_reason: statusData.error?.reason,
        webhook_type: 'status'
    };

    logger.info(logData, 'Processing status update for message');

    try {
        // --- Add your business logic here ---
        // Example: Update a database record based on status
        // await updateMessageStatusInDB(statusData.message_uuid, statusData.status, statusData.timestamp, statusData.error);
        if (statusData.error) {
            logger.warn(logData, `Message status update indicates error`);
        }
        // ------------------------------------

        res.status(200).send('OK'); // Acknowledge receipt *after* basic processing attempt

    } catch (error) {
        logger.error({ err: error, ...logData }, 'Error processing status webhook');
        // Still send 200 OK if the error is in *your* processing logic,
        // unless you specifically want Vonage to retry (use with caution).
        // If the error indicates bad data you can't handle, 200 might still be appropriate
        // to prevent endless retries. Log the error thoroughly.
        // Consider sending 500 only for unexpected server errors preventing acknowledgement.
        res.status(500).send('Internal Server Error during processing'); // Or potentially 200 OK depending on error strategy
    }
});

6. Security considerations

Securing your application and webhooks is vital.

  • Webhook Signature Verification: Always verify the JWT signature using the VONAGE_SIGNATURE_SECRET and the raw request body, as implemented in the verifyVonageSignature middleware. This is the primary mechanism to ensure the request genuinely originated from Vonage and hasn't been tampered with.
  • API Credential Security:
    • Never commit your .env file or private.key to version control. Use .gitignore.
    • In production, use environment variables provided by your hosting platform or a dedicated secrets management service (like AWS Secrets Manager, HashiCorp Vault, etc.) instead of a .env file.
  • HTTPS: Always use HTTPS for your webhook URLs in production. ngrok provides HTTPS forwarding, and your deployment environment should also be configured for HTTPS.
  • Input Validation: Sanitize and validate any user-provided input, especially if exposing the /send-sms endpoint publicly. The E.164 check is a good start; also consider message lengths and potentially filter content.
  • Rate Limiting: If the /send-sms endpoint is exposed, implement rate limiting (e.g., using express-rate-limit) to prevent abuse.

7. Deployment

Deploying this application involves moving beyond ngrok.

  • Choose a Hosting Platform: Options include Heroku, AWS (EC2, Lambda, Elastic Beanstalk), Google Cloud (Cloud Run, App Engine), DigitalOcean, Vercel, Render, etc.
  • Environment Variables: Configure your production environment variables (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_SIGNATURE_SECRET, VONAGE_NUMBER, PORT, VONAGE_PRIVATE_KEY_PATH) securely through your hosting provider's interface or secrets management. Ensure the private.key file is deployed securely to the location specified by VONAGE_PRIVATE_KEY_PATH.
  • Stable Public URL: Your deployed application will have a stable public URL (e.g., https://your-app-name.herokuapp.com). Update the webhook URLs in your Vonage Application settings to use this production URL (must be HTTPS).
  • Procfile/Dockerfile: Depending on the platform, you might need a Procfile (Heroku) or Dockerfile (container-based deployments) to define how to start your application (node index.js).
  • CI/CD: Set up a Continuous Integration/Continuous Deployment pipeline (e.g., GitHub Actions, GitLab CI, Jenkins) to automate testing and deployment.

8. Troubleshooting and Caveats

  • Webhook Not Received:
    • Check ngrok/server logs for errors (startup, request handling, signature verification).
    • Verify the Status URL in the Vonage Dashboard exactly matches your ngrok/production URL + /webhooks/status (case-sensitive, HTTPS in production).
    • Ensure your server is running and accessible from the internet (ngrok status should be ""online""; check production deployment status).
    • Check the Vonage Dashboard API Logs and Application event logs for errors reported by Vonage when trying to reach your webhook (e.g., 4xx/5xx responses, timeouts).
    • Firewall issues might block incoming requests to your server.
  • Invalid Signature (401 Unauthorized):
    • Double-check VONAGE_SIGNATURE_SECRET in your environment variables matches the secret in the Vonage Application settings exactly (no extra spaces, etc.).
    • Confirm that bodyParser.json({ verify: ... }) is correctly capturing the raw body (req.rawBody) before the verifyVonageSignature middleware runs. Use the ngrok web interface (http://127.0.0.1:4040) to inspect the Authorization header on incoming requests.
    • Ensure the system clocks on your server and Vonage's servers are reasonably synchronized (JWTs have expiration times).
  • SMS Not Sending:
    • Check Node.js logs for errors from the sendSms function. Look closely at err.response.data for specific Vonage error codes and descriptions.
    • Verify API Key/Secret/Application ID/Private Key path are correct in your environment variables.
    • Ensure your Vonage account has sufficient credit.
    • Check if the destination number (to) is valid and strictly in E.164 format (e.g., +14155550101).
    • Confirm the Vonage number (from) linked to the Application is SMS-capable for the destination country and correctly formatted in E.164.
    • Trial Account Limitations: New Vonage accounts might have restrictions (e.g., only sending to verified numbers listed in the dashboard) until topped up or fully verified.
  • Delayed Status Updates: Delivery receipts depend on downstream carriers; delays are possible. Not all carriers provide timely or reliable DLRs. submitted or accepted statuses usually appear quickly, but delivered or failed can take longer or sometimes not arrive at all for certain destinations/networks.
  • Payload Variations: While the general structure is consistent, webhook payloads might occasionally have minor variations or new fields added by Vonage. Rely on documented core fields like message_uuid, status, timestamp. Refer to the official Vonage Messages API documentation for the definitive schemas.

9. Verification checklist

Before considering the implementation complete, verify the following:

  • Vonage account created and API credentials noted.
  • Vonage Application created with Messages capability enabled.
  • Private key downloaded, stored securely, and path correctly set in environment variables.
  • Application ID and Signature Secret correctly set in environment variables.
  • Vonage SMS-capable number purchased, correctly formatted (E.164), and linked to the Application.
  • Status and Inbound webhook URLs correctly configured in Vonage Application settings (pointing to ngrok or production HTTPS URL).
  • Node.js project initialized, dependencies installed (express, @vonage/server-sdk, dotenv, jsonwebtoken, body-parser).
  • .env file created (or environment variables set) with all necessary variables filled.
  • index.js contains Express server setup, Vonage SDK initialization, bodyParser with raw body capture, verifyVonageSignature middleware, and webhook handlers (/webhooks/status).
  • verifyVonageSignature middleware is applied to webhook routes and correctly uses the raw body for verification.
  • Webhook handlers log incoming data and return 200 OK upon successful processing acknowledgment.
  • sendSms function correctly uses vonage.messages.send and validates the to number format (strict E.164).
  • Application runs locally without errors (node index.js).
  • ngrok (or production deployment) correctly forwards requests to the Node.js application.
  • Sending a test SMS via the /send-sms endpoint (or other trigger) succeeds (check logs).
  • Test SMS is received on the target device.
  • Status webhook events (submitted, delivered, etc.) are received and logged by the /webhooks/status endpoint.

Frequently Asked Questions

How to track SMS delivery status with Vonage?

Use Vonage's Messages API and webhooks. Set up a webhook endpoint in your application to receive real-time status updates like 'submitted,' 'delivered,' or 'failed' from Vonage after sending an SMS message via their API.

What is the Vonage Messages API?

It's an API that lets you send and receive messages, including SMS, and track their delivery status. You integrate it into your application using the Vonage Node.js SDK or other language-specific libraries. It's more robust than 'fire and forget' methods.

Why does Vonage use webhooks for SMS status?

Webhooks provide real-time delivery updates pushed directly to your application. This is more efficient than constantly polling the API and lets you react immediately to changes in message status, building more reliable systems.

When should I use the Vonage Messages API?

Whenever your application needs to send SMS messages reliably and track their delivery status in real time. It's especially important for time-sensitive communications and two-factor authentication.

How to set up a Node.js server for Vonage SMS?

Use Express.js to create a server and the Vonage Node.js SDK to interact with the Messages API. You'll also need environment variables for your API credentials and webhook URLs. Ngrok helps during development.

What is a Vonage Application ID?

A unique identifier for your application within the Vonage platform. It's required for using the Messages API. You create an application in the Vonage Dashboard and note its ID for configuration.

What is the purpose of ngrok in Vonage webhook setup?

Ngrok creates a public, secure tunnel to your local development server, allowing Vonage to send webhooks to your machine during testing. This is necessary because local servers aren't publicly accessible on the internet.

Why use JWT signature verification for Vonage webhooks?

It guarantees the integrity and authenticity of the webhook requests. By using a shared secret, you confirm requests genuinely originated from Vonage and haven't been tampered with. This is crucial for security.

How to verify Vonage webhook signature in Node.js?

Use the 'jsonwebtoken' library and the Vonage Signature Secret from your application dashboard. Importantly, use the *raw* request body, not the parsed JSON, when verifying the signature against the token from the 'Authorization' header.

What are common Vonage SMS delivery statuses?

Typical statuses include 'submitted' (sent to Vonage), 'delivered' (reached handset), 'failed,' 'rejected' (by the carrier), 'accepted' (intermediate carrier status), and 'undeliverable.' Not all statuses are guaranteed due to carrier limitations.

How to handle Vonage webhook errors in Node.js?

Wrap your webhook handler logic in 'try...catch' blocks to handle errors gracefully. Always acknowledge receipt with a 200 OK response, even if your internal processing fails, to prevent Vonage retries. Log errors thoroughly using a structured logger.

What should I do if Vonage webhooks are not received?

Check server and ngrok logs, verify the webhook URL in your Vonage application settings matches your server's URL, and check for firewall issues. Inspect the Vonage API logs for errors reported by Vonage while trying to reach your webhook.

Why is it important to capture the raw body for webhook verification?

Vonage uses the raw, unaltered request body to generate the JWT signature, not the parsed JSON. Therefore, you must capture the raw body using middleware like `body-parser` *before* any JSON parsing occurs to ensure accurate signature verification.

What's the correct E.164 number format for Vonage?

Use the + sign followed by the country code and phone number without any spaces or special characters. Example: +14155550101. Validate user-provided numbers strictly to avoid errors.