code examples

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

Vonage WhatsApp Integration with Node.js & Next.js: Complete Tutorial

Build WhatsApp messaging into your Next.js app with Vonage Messages API. Step-by-step guide covering webhooks, API routes, authentication, ngrok setup, and deployment. Includes TypeScript examples and security best practices.

Vonage WhatsApp Integration with Node.js & Next.js: Complete Tutorial

Quick Answer: How to Integrate Vonage WhatsApp with Next.js

<!-- GAP: Missing version requirements for @vonage packages (Type: Critical, Priority: High) --> <!-- DEPTH: Quick answer lacks error handling overview (Priority: Medium) -->

Integrate Vonage WhatsApp messaging into your Next.js application by creating API routes in pages/api/vonage/ to handle inbound messages and status webhooks. Install @vonage/server-sdk, @vonage/messages, and @vonage/jwt packages. Configure environment variables for your Vonage API credentials, application ID, and private key. Use ngrok to expose your local development server for webhook testing. The Vonage Messages API (Application Programming Interface) sends POST requests to your /inbound route when users message your WhatsApp number, and your app sends replies using the Vonage Node SDK (Software Development Kit). Verify webhook signatures using JWT (JSON Web Token) authentication for security.


This guide provides a step-by-step walkthrough for integrating Vonage's Messages API (Application Programming Interface) to send and receive WhatsApp messages within a Next.js application. You'll build a simple Next.js app with API routes that handle incoming WhatsApp messages via Vonage webhooks and send replies back using the Vonage Node SDK (Software Development Kit).

<!-- DEPTH: Introduction lacks use case examples or complexity estimates (Priority: Medium) -->

This setup enables you to build applications that interact directly with users on WhatsApp, opening possibilities for customer support bots, notification systems, interactive services, and more, leveraging the familiar Next.js development environment.

Project Goals:

  • Set up a Next.js project configured for Vonage WhatsApp integration.
  • Create API routes to handle incoming messages and delivery status updates from Vonage.
  • Use the Vonage Node SDK to send WhatsApp messages in response to user input.
  • Securely manage Vonage API credentials and webhook signatures.
  • Deploy the application and configure production webhooks.
<!-- GAP: Missing estimated time to complete and skill level prerequisites (Type: Substantive, Priority: High) -->

Technology Stack:

  • Next.js: React framework for building the frontend and API routes (using Pages Router structure for examples).
  • Node.js: Runtime environment for Next.js API routes and the Vonage SDK.
  • Vonage Messages API: Service used to send and receive WhatsApp messages.
  • Vonage Node SDK: Simplifies interaction with the Vonage APIs.
  • WhatsApp Sandbox: Vonage's testing environment for WhatsApp messaging.
  • ngrok: Tool to expose local development server to the internet for webhook testing.

System Architecture:

text
+-------------+     +----------+     +---------------+     +-------------------------+     +-----------------+     +----------+     +-------------+
| User        | --> | WhatsApp | --> | Vonage        | --> | ngrok (Dev) /           | --> | Next.js API Route | --> | Vonage    | --> | WhatsApp | --> | User        |
| (WhatsApp)  |     | Network  |     | Messages API  |     | Vercel URL (Prod)       |     | (/api/vonage/inbound) |     | Messages API|     | Network  |     | (WhatsApp)  |
+-------------+     +----------+     +---------------+     +-------------------------+     +-----------------+     +----------+     +-------------+
      ^                                                               |                                   ^
      |---------------------------------------------------------------|-----------------------------------|
                                      Reply Message Sent

+---------------+     +-------------------------+     +-----------------------+
| Vonage        | --> | ngrok (Dev) /           | --> | Next.js API Route     |
| Messages API  |     | Vercel URL (Prod)       |     | (/api/vonage/status)  |  (Logs status)
+---------------+     +-------------------------+     +-----------------------+
 (Status Update)
<!-- EXPAND: Could benefit from latency/timing information in architecture (Type: Enhancement, Priority: Low) -->

Prerequisites:

<!-- GAP: Missing specific Node.js installation verification commands (Type: Substantive, Priority: Medium) -->
  • Node.js: Version 20 or higher installed.
  • npm or yarn: Package manager for Node.js.
  • Vonage API Account: Sign up for free credit. You'll need your API Key and Secret.
  • ngrok: Installed and authenticated for testing webhooks locally.
  • WhatsApp Account: A personal WhatsApp account on a smartphone for testing.

1. Setting Up the Next.js Project

Start by creating a new Next.js application and installing the necessary dependencies.

<!-- DEPTH: Setup section lacks troubleshooting for common installation issues (Priority: Medium) -->
  1. Create a Next.js App: Open your terminal and run:

    bash
    npx create-next-app@latest vonage-whatsapp-nextjs
    cd vonage-whatsapp-nextjs

    Follow the prompts. This guide uses the pages/api directory structure for API routes.

  2. Install Dependencies:

    <!-- GAP: Missing specific version compatibility information (Type: Critical, Priority: High) -->

    Install the Vonage SDKs and dotenv for managing environment variables locally.

    bash
    npm install @vonage/server-sdk @vonage/messages @vonage/jwt dotenv
    • @vonage/server-sdk: Core SDK for authentication and client initialization.
    • @vonage/messages: Specifically for using the Messages API.
    • @vonage/jwt: For verifying webhook signatures (JWTs – JSON Web Tokens).
    • dotenv: To load environment variables from a .env.local file during local development (Next.js has built-in support, but this ensures consistency).
  3. Configure Environment Variables:

    <!-- GAP: Missing security warning about .gitignore verification (Type: Critical, Priority: High) -->

    Create a file named .env.local in the root of your project. This file is gitignored by default in Next.js projects, keeping your secrets safe. Add the following variables:

    dotenv
    # .env.local
    
    # Vonage API Credentials (Found on Vonage Dashboard homepage)
    VONAGE_API_KEY=YOUR_VONAGE_API_KEY
    VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
    
    # Vonage Application Credentials (Generated in Application setup)
    VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
    # Use the *relative path* from your project root to your downloaded private key file
    VONAGE_PRIVATE_KEY=./private.key
    
    # Vonage WhatsApp Sandbox Number (Found on Messages API Sandbox page)
    # Use the format without leading + or 00, e.g., 14157386102
    VONAGE_WHATSAPP_NUMBER=VONAGE_SANDBOX_WHATSAPP_NUMBER
    
    # Vonage Signature Secret (Found in Vonage Dashboard -> Settings)
    VONAGE_API_SIGNATURE_SECRET=YOUR_VONAGE_SIGNATURE_SECRET
    
    # Base URL for Webhooks (Set dynamically or for local dev/manual setup)
    # Example for ngrok: https://<your-id>.ngrok.app
    # Example for Vercel: https://your-app-name.vercel.app
    BASE_URL=http://localhost:3000

    How to Obtain Each Value:

    <!-- EXPAND: Could add screenshots or more visual guidance (Type: Enhancement, Priority: Low) -->
    • VONAGE_API_KEY & VONAGE_API_SECRET: Go to your Vonage API Dashboard. They are displayed prominently at the top.
    • VONAGE_APPLICATION_ID & VONAGE_PRIVATE_KEY:
      1. Navigate to Applications under Build & Manage in the Vonage Dashboard.
      2. Click Create a new application.
      3. Give it a name (e.g., "NextJS WhatsApp App").
      4. Under Capabilities, toggle Messages. You'll need to provide temporary webhook URLs (you'll update these later). Enter http://localhost/inbound for Inbound and http://localhost/status for Status for now.
      5. Click Generate public and private key. Crucially, download the private.key file and save it in the root of your Next.js project. The public key is managed by Vonage.
      6. Click Create application.
      7. The VONAGE_APPLICATION_ID will be displayed on the application's page. Copy it.
      8. For VONAGE_PRIVATE_KEY in .env.local, use the relative path from your project root to the downloaded key, e.g., ./private.key.
    • VONAGE_WHATSAPP_NUMBER:
      1. Navigate to Messages API Sandbox under Build & Manage.
      2. Activate the WhatsApp Sandbox if you haven't already.
      3. The Sandbox phone number will be displayed (e.g., 14157386102). Use this number.
      4. Follow the instructions to allowlist your personal WhatsApp number by sending the specified message to the Sandbox number.
    • VONAGE_API_SIGNATURE_SECRET:
      1. Navigate to Settings in the Vonage Dashboard.
      2. Under API settings, find your Default Signature secret. Copy this value.
    • BASE_URL: Set this to your local development URL (http://localhost:3000) initially. You'll use ngrok to expose this publicly later. For deployment, this will be your production URL. This variable isn't used directly by the code but helps conceptualize the webhook URLs.
  4. Project Structure: Place your API logic within the pages/api/ directory. Create a subdirectory for Vonage webhooks:

    bash
    mkdir -p pages/api/vonage

    Also create a utility file for the Vonage client:

    bash
    mkdir lib
    touch lib/vonageClient.js

2. Implementing Core Functionality (API Routes)

Now, let's create the API routes to handle incoming messages and status updates from Vonage.

  1. Initialize Vonage Client: Create a reusable Vonage client instance.

    <!-- DEPTH: Client initialization lacks connection testing/health check example (Priority: Medium) -->
    javascript
    // lib/vonageClient.js
    import { Vonage } from "@vonage/server-sdk";
    import path from "path"; // Import path module
    
    // Ensure environment variables are loaded (Next.js does this automatically for .env.local)
    // require("dotenv").config(); // Usually not needed in Next.js API routes
    
    // Resolve the path to the private key relative to the project root
    const privateKeyPath = path.resolve(process.cwd(), process.env.VONAGE_PRIVATE_KEY);
    
    let vonage;
    
    try {
      vonage = new Vonage(
        {
          apiKey: process.env.VONAGE_API_KEY,
          apiSecret: process.env.VONAGE_API_SECRET,
          applicationId: process.env.VONAGE_APPLICATION_ID,
          privateKey: privateKeyPath, // Use the resolved path
        },
        {
          // Use sandbox for testing; remove or set to "https://messages.nexmo.com" for production
          apiHost: "https://messages-sandbox.nexmo.com",
          // Optional: Add custom logger, timeout, user agent etc.
          // logger: console, // Example: Log SDK activities
        }
      );
      console.log("Vonage client initialized successfully.");
    } catch (error) {
      console.error("Error initializing Vonage client:", error);
      // Handle initialization error appropriately – maybe throw or set vonage to null
      vonage = null; // Ensure vonage is null or handle differently if init fails
    }
    
    export default vonage;
    • We use path.resolve to ensure the path to the private key is correct, regardless of where the script is run from.
    • We explicitly set apiHost to the sandbox URL. Remember to remove this or change it for production.
    • Basic error handling is added for initialization.
<!-- GAP: Missing explanation of different message types WhatsApp supports (Type: Substantive, Priority: High) -->
  1. Create Inbound Message Handler: This API route will receive messages sent by users to your Vonage WhatsApp number.

    javascript
    // pages/api/vonage/inbound.js
    import vonage from "../../../lib/vonageClient"; // Adjust path as necessary
    import { WhatsAppText } from "@vonage/messages";
    
    // Simple in-memory store for demo idempotency.
    // NOTE: This clears only on server restart and has a max size.
    // Production requires a persistent store (e.g., Redis, DB).
    const processedMessages = new Set();
    const MAX_SET_SIZE = 1000; // Limit memory usage
    
    // --- Helper Function to Send WhatsApp Message ---
    async function sendWhatsAppReply(recipientNumber, messageText) {
      if (!vonage) {
        console.error("Vonage client not initialized. Cannot send message.");
        throw new Error("Vonage client unavailable");
      }
    
      console.log(`Attempting to send reply to ${recipientNumber}`);
      try {
        const message = new WhatsAppText({
          to: recipientNumber,
          from: process.env.VONAGE_WHATSAPP_NUMBER, // Your Vonage WhatsApp number
          text: messageText,
          client_ref: `reply-${Date.now()}`, // Optional client reference
        });
    
        const response = await vonage.messages.send(message);
        console.log(`Message sent successfully to ${recipientNumber}:`, response);
        // IMPORTANT: In a real app with DB logging (Section 6), you would log
        // this outbound message here, storing response.message_uuid
        // await logOutboundMessage(response.message_uuid, recipientNumber, messageText);
        return response; // Contains message_uuid
      } catch (error) {
        console.error(`Error sending message to ${recipientNumber}:`, error.response ? error.response.data : error.message);
        // Rethrow or handle specific errors (e.g., rate limits, invalid number)
        throw error;
      }
    }
    
    // --- API Route Handler ---
    export default async function handler(req, res) {
      if (!vonage) {
        console.error("Vonage client not available in handler.");
        return res.status(500).json({ error: "Server configuration error" });
      }
    
      if (req.method !== "POST") {
        console.log(`Received ${req.method} request, expected POST`);
        res.setHeader("Allow", ["POST"]);
        return res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    
      // Log the raw request body for debugging
      console.log("Received inbound webhook:", JSON.stringify(req.body, null, 2));
    
      try {
        const { message_uuid, from, message } = req.body;
    
        // Basic validation: Check for essential fields and text message type.
        // Vonage might send webhooks for non-text messages (media, location) or
        // potentially incomplete data in edge cases.
        if (!message_uuid || !from || !from.number || !message || message.type !== "text") {
           console.warn("Received incomplete, non-text, or unexpected message format:", req.body);
           // Acknowledge receipt with 200 OK to prevent Vonage retries for messages
           // this application version chooses not to process.
           return res.status(200).end();
        }
    
        const senderNumber = from.number;
        const incomingText = message.content.text;
    
        // --- Prevent Processing Duplicate/Looping Messages ---
        // Simple check using message ID and basic loop prevention.
        if (processedMessages.has(message_uuid) || incomingText === "Message received.") {
            console.log(`Skipping already processed or self-reply message: ${message_uuid}`);
            return res.status(200).end(); // Acknowledge but don't process
        }
    
        // Add to processed set (with size limit)
        if (processedMessages.size >= MAX_SET_SIZE) {
            const oldestKey = processedMessages.values().next().value;
            processedMessages.delete(oldestKey); // Remove oldest entry to prevent unbounded growth
        }
        processedMessages.add(message_uuid);
        // ------------------------------------------------------
    
        console.log(`Received text message "${incomingText}" from ${senderNumber} (UUID: ${message_uuid})`);
    
        // --- Log Inbound Message (Conceptual – see Section 6) ---
        // await logInboundMessage(message_uuid, senderNumber, incomingText);
    
        // --- Send a Reply ---
        // You could add more sophisticated logic here based on `incomingText`
        const replyText = "Message received.";
        await sendWhatsAppReply(senderNumber, replyText);
    
        // Acknowledge receipt to Vonage
        res.status(200).end();
    
      } catch (error) {
        console.error("Error processing inbound message:", error);
        // Send a generic error status, but don't expose internal details
        // Vonage will retry if it doesn't get a 2xx response, so 500 is appropriate
        res.status(500).json({ error: "Failed to process message" });
      }
    }
    • It imports the shared vonage client.
    • It checks for POST requests.
    • It extracts the sender's number (from.number) and the message content (message.content.text).
    • Includes basic validation and explains why 200 OK is returned for ignored messages.
    • It calls a helper function sendWhatsAppReply to send a "Message received." response.
    • Crucially, it includes basic idempotency logic (processedMessages Set) with comments on its limitations and clearing mechanism (server restart).
    • It responds with 200 OK to Vonage to acknowledge receipt. Errors return 500.
<!-- DEPTH: Status handler lacks information about all possible status values (Priority: High) -->
  1. Create Status Handler: This route receives status updates about messages you've sent (e.g., delivered, read, failed).

    javascript
    // pages/api/vonage/status.js
    import { verifySignature } from "@vonage/jwt";
    
    // --- Helper function for JWT Verification ---
    function verifyVonageSignature(req) {
      try {
        // Extract the token from the Authorization header (Bearer <token>)
        const authorizationHeader = req.headers.authorization;
        if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) {
          console.error("Missing or invalid Authorization header");
          return false;
        }
        const token = authorizationHeader.split(" ")[1];
    
        // Verify the signature using the secret from environment variables
        const isValid = verifySignature(token, process.env.VONAGE_API_SIGNATURE_SECRET);
        if (!isValid) {
          console.warn("Invalid JWT signature received.");
        }
        return isValid;
      } catch (error) {
        console.error("Error during JWT verification:", error);
        return false;
      }
    }
    
    // --- API Route Handler ---
    export default async function handler(req, res) { // Make async if using DB calls
      if (req.method !== "POST") {
        console.log(`Received ${req.method} request, expected POST`);
        res.setHeader("Allow", ["POST"]);
        return res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    
      // --- Verify Signature ---
      // This is critical for security to ensure the request came from Vonage
      if (!verifyVonageSignature(req)) {
         console.error("Unauthorized status webhook attempt blocked.");
         // Return 401 Unauthorized if signature is invalid
         return res.status(401).json({ error: "Invalid signature" });
      }
      console.log("JWT signature verified successfully for status webhook.");
    
      // --- Process Status ---
      try {
        // Log the entire status update body for inspection
        console.log("Received status webhook:", JSON.stringify(req.body, null, 2));
    
        const { message_uuid, status, timestamp, to, error } = req.body;
    
        // Basic validation for required fields
        if (!message_uuid || !status || !timestamp || !to || !to.number) {
            console.warn("Received incomplete status update:", req.body);
            // Still acknowledge receipt even if data seems incomplete
            return res.status(200).end();
        }
    
        // Example: Log key information
        console.log(`Status Update: UUID=${message_uuid}, Status=${status}, To=${to?.number}, Timestamp=${timestamp}`);
    
        // --- Update Database (Conceptual – see Section 6) ---
        // Assumes the outbound message with this message_uuid was previously logged
        // await updateMessageStatusInDb(message_uuid, status, error);
    
        // Example: Handle specific statuses in logs
        if (status === "delivered") {
          console.log(`Message ${message_uuid} delivered successfully to ${to?.number}.`);
        } else if (status === "read") {
          console.log(`Message ${message_uuid} read by ${to?.number}.`);
        } else if (status === "failed" || status === "rejected") {
          console.error(`Message ${message_uuid} failed for ${to?.number}. Reason:`, error);
        } else {
          console.log(`Received status "${status}" for message ${message_uuid}.`);
        }
    
        // Acknowledge receipt to Vonage
        res.status(200).end();
    
      } catch (error) {
        console.error("Error processing status update:", error);
        res.status(500).json({ error: "Failed to process status update" });
      }
    }
    • It includes a verifyVonageSignature helper using @vonage/jwt and your VONAGE_API_SIGNATURE_SECRET. This step is vital for security.
    • It logs the incoming status update. In a real application, you'd use this data to update message statuses in your database or trigger other workflows (see Section 6).
    • It responds with 200 OK.

3. Configuring Webhooks with ngrok

To test the integration locally, Vonage needs to be able to reach your development server. We'll use ngrok for this.

<!-- GAP: Missing ngrok authentication setup instructions (Type: Substantive, Priority: High) -->
  1. Start Your Next.js App:

    bash
    npm run dev

    Your app should be running, typically on http://localhost:3000.

  2. Start ngrok: Open a new terminal window and run ngrok, pointing it to your Next.js port (usually 3000):

    bash
    ngrok http 3000
  3. Get Your Public URL: ngrok will display output similar to this:

    text
    Forwarding                    https://<some-random-id>.ngrok-free.app -> http://localhost:3000

    Copy the https://... URL. This is your temporary public URL.

<!-- EXPAND: Could add information about ngrok alternatives (Type: Enhancement, Priority: Low) -->
  1. Update Webhook URLs in Vonage:

    • Application Webhooks:
      1. Go back to your application in the Vonage Dashboard (Applications -> Your App).
      2. Click Edit.
      3. Update the Messages capability webhooks:
        • Inbound URL: <your-ngrok-url>/api/vonage/inbound
        • Status URL: <your-ngrok-url>/api/vonage/status
      4. Click Save changes.
    • Messages API Sandbox Webhooks:
      1. Go to the Messages API Sandbox page in the Vonage Dashboard.
      2. Scroll down to the Webhooks section.
      3. Update the webhooks:
        • Inbound URL: <your-ngrok-url>/api/vonage/inbound
        • Status URL: <your-ngrok-url>/api/vonage/status
      4. Click Save webhooks.

    Important: Ensure the paths /api/vonage/inbound and /api/vonage/status match the location of your API route files within your Next.js project's pages/api/ directory.


4. Testing the Integration

<!-- DEPTH: Testing section lacks failure scenario examples (Priority: Medium) -->
  1. Send a WhatsApp Message: Using the personal WhatsApp account you allowlisted earlier, send any message (e.g., ""Hello"") to the Vonage WhatsApp Sandbox number.

  2. Check Your Logs:

    • Next.js Terminal: You should see logs from your /api/vonage/inbound route, indicating the message was received and a reply was attempted (e.g., ""Received text message..."", ""Attempting to send reply..."", ""Message sent successfully..."").
    • ngrok Terminal: You should see POST requests hitting your ngrok URL for /api/vonage/inbound and likely /api/vonage/status shortly after.
    • Next.js Terminal (Status): After the reply is sent, you should see logs from /api/vonage/status showing the delivery status updates (e.g., ""Received status webhook..."", ""Status Update: UUID=..., Status=submitted..."", ""Status Update: UUID=..., Status=delivered..."").
  3. Check WhatsApp: You should receive the ""Message received."" reply on your personal WhatsApp account from the Sandbox number.


5. Error Handling and Logging Considerations

The provided code includes basic try...catch blocks and console.log/console.error. For production:

<!-- DEPTH: Error handling section lacks code examples for recommended improvements (Priority: High) -->
  • Structured Logging: Use a library like Pino or Winston to output logs in JSON format. This makes them easier to parse and analyze in log management systems (e.g., Datadog, Logtail, AWS CloudWatch).
  • Centralized Error Tracking: Integrate an error tracking service like Sentry or Bugsnag to capture, aggregate, and alert on unhandled exceptions in your API routes.
  • Specific Error Handling: Catch specific errors from the Vonage SDK (check their documentation for error types) and handle them appropriately (e.g., retries for transient network issues, logging specific failure reasons from the error object in status updates).
  • Robust Idempotency: The simple Set is only suitable for demos. Use a persistent store (like Redis or a database) checking message_uuid to reliably handle duplicate webhook deliveries in production.
<!-- EXPAND: Could add monitoring and alerting recommendations (Type: Enhancement, Priority: Medium) -->

6. Database Integration (Conceptual)

While this basic example doesn't use a database, a real-world application likely would:

  • Store Message Logs: Log incoming and outgoing messages with their message_uuid, sender/recipient, content, timestamp, and status.
  • Manage Conversation State: For bots, store the user's current position in a conversation flow.
  • Store User Data: Link WhatsApp numbers to user profiles in your system.
<!-- GAP: Missing alternative database options besides Prisma (Type: Substantive, Priority: Medium) -->

Example using Prisma (Conceptual):

  1. Setup Prisma: npm install prisma --save-dev, npx prisma init, configure your database URL in .env.

  2. Define Schema:

    prisma
    // prisma/schema.prisma
    model MessageLog {
      id          String   @id @default(cuid())
      messageUuid String   @unique // Vonage message_uuid (for outbound status tracking)
      direction   String   // ""inbound"" or ""outbound""
      sender      String
      recipient   String
      content     String?
      status      String   // e.g., received (inbound), submitted, delivered, read, failed (outbound)
      timestamp   DateTime @default(now())
      vonageError Json?    // Store error details if failed (outbound)
    
      // Optional: Add relation to Conversation or User models
      // conversationId String?
      // conversation   Conversation? @relation(fields: [conversationId], references: [id])
    }
  3. Apply Schema: npx prisma migrate dev (or npx prisma db push for prototyping)

  4. Use in API Routes:

    javascript
    // --- Conceptual DB Logging Functions ---
    import { PrismaClient } from '@prisma/client';
    const prisma = new PrismaClient();
    
    // Call this within /api/vonage/inbound.js after receiving a message
    async function logInboundMessage(uuid, sender, recipient, text) {
      try {
        await prisma.messageLog.create({
          data: {
            messageUuid: uuid, // Log the inbound UUID for reference
            direction: 'inbound',
            sender: sender,
            recipient: recipient, // Your Vonage number
            content: text,
            status: 'received',
          },
        });
      } catch (dbError) {
        console.error('DB Error logging inbound message:', dbError);
      }
    }
    
    // Call this within sendWhatsAppReply (in inbound.js) AFTER successfully sending
    async function logOutboundMessage(uuid, recipient, text) {
      try {
        await prisma.messageLog.create({
          data: {
            messageUuid: uuid, // Log the OUTBOUND message_uuid from Vonage response
            direction: 'outbound',
            sender: process.env.VONAGE_WHATSAPP_NUMBER, // Your Vonage number
            recipient: recipient,
            content: text,
            status: 'submitted', // Initial status after sending
          },
        });
      } catch (dbError) {
        console.error('DB Error logging outbound message:', dbError);
      }
    }
    
    // Call this within /api/vonage/status.js to update the logged outbound message
    async function updateMessageStatusInDb(uuid, status, errorDetails) {
      try {
        await prisma.messageLog.update({
          where: { messageUuid: uuid }, // Find the specific outbound message log
          data: {
            status: status,
            vonageError: errorDetails ? errorDetails : undefined, // Store error info if present
          },
        });
      } catch (dbError) {
        // Handle case where messageUuid might not be found (e.g., race condition, log failure)
        if (dbError.code === 'P2025') { // Prisma code for record not found
           console.warn(`DB Warn: Status update for unknown messageUuid: ${uuid}`);
        } else {
           console.error('DB Error updating message status:', dbError);
        }
      }
    }
    
    // --- Usage in API Routes ---
    
    // In /api/vonage/inbound.js (inside the try block, after validation)
    // await logInboundMessage(message_uuid, senderNumber, process.env.VONAGE_WHATSAPP_NUMBER, incomingText);
    
    // In sendWhatsAppReply function (after await vonage.messages.send(message))
    // const outbound_uuid = response.message_uuid;
    // await logOutboundMessage(outbound_uuid, recipientNumber, messageText);
    
    // In /api/vonage/status.js (inside the try block, after validation)
    // await updateMessageStatusInDb(message_uuid, status, error);
    • This refined example shows separate logging for inbound and outbound messages.
    • Crucially, it logs the message_uuid returned by Vonage when sending the outbound message.
    • The status handler then uses prisma.messageLog.update (not updateMany) with where: { messageUuid: message_uuid } to update the status of that specific outbound message log entry.

7. Security Best Practices

<!-- DEPTH: Security section lacks specific attack scenarios and mitigation examples (Priority: High) -->
  • Verify Signatures: Always verify the JWT signature on the /status webhook using verifySignature and your VONAGE_API_SIGNATURE_SECRET. This prevents attackers from spoofing status updates.
  • Secure Inbound Webhook (Optional): While the /inbound webhook doesn't use Vonage JWTs by default, consider adding your own security layer if the handled data is sensitive. Options include:
    • Basic Authentication over HTTPS.
    • Checking a custom shared secret passed in a header.
    • IP address allowlisting (if Vonage provides stable IPs or ranges).
  • Secure Secrets: Never commit .env.local or your private.key file to source control. Use environment variables in your deployment environment (see Section 9).
  • Input Validation: Sanitize and validate data received in webhooks (req.body). Check expected data types, lengths, and formats. Libraries like zod can help define schemas for robust validation.
  • Rate Limiting: Implement rate limiting on your API routes (e.g., using nextjs-rate-limiter or platform features like Vercel's) to prevent abuse or accidental loops.
  • HTTPS: Always use HTTPS for your webhook URLs (ngrok provides this, and platforms like Vercel enforce it).
<!-- GAP: Missing information about data privacy and compliance considerations (Type: Substantive, Priority: Medium) -->

8. Troubleshooting and Caveats

<!-- DEPTH: Troubleshooting section could benefit from more specific error codes (Priority: Medium) -->
  • Webhooks Not Reaching Server:
    • Check ngrok status and URL. Is ngrok running? Is the URL correct?
    • Verify the exact webhook URL (including /api/vonage/...) is configured correctly in both the Vonage Application and Sandbox settings. A typo is common.
    • Check firewall rules if self-hosting.
    • Inspect the ngrok web interface (http://127.0.0.1:4040) for request/response details and errors.
  • 401 Unauthorized on /status: Incorrect VONAGE_API_SIGNATURE_SECRET or issue with JWT verification logic. Double-check the secret in Vonage Settings -> API settings and your .env.local / deployment environment variables. Ensure the header format is correct (Bearer <token>).
  • Error Sending Message: Check VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, and VONAGE_PRIVATE_KEY path/content in .env.local / environment variables. Ensure the recipient number is correctly formatted (E.164) and allowlisted in the Sandbox. Check Vonage Dashboard logs (Logs -> Messages API) for specific API errors. Ensure the vonageClient initialized correctly (check startup logs).
  • Infinite Loops: Ensure your reply message content doesn't accidentally trigger your inbound logic again. Implement robust idempotency checks using message_uuid and a persistent store for production. The demo Set is insufficient for reliable duplicate prevention.
  • Sandbox Limitations: Only works with allowlisted numbers. Uses a shared Vonage number. Watermarks may appear on messages. Not for production traffic. Lower throughput than production.
  • Moving to Production: Requires purchasing a Vonage number, setting up a WhatsApp Business Account (WABA), potentially getting approval for message templates (if initiating conversations outside the 24-hour window), and updating API credentials/webhook URLs. Remove the apiHost sandbox override in lib/vonageClient.js.
<!-- GAP: Missing information about Vonage API rate limits and quotas (Type: Substantive, Priority: High) -->

Frequently Asked Questions About Vonage WhatsApp Integration with Next.js

How do I set up Vonage WhatsApp integration in Next.js?

Set up Vonage WhatsApp integration by installing @vonage/server-sdk, @vonage/messages, and @vonage/jwt packages using npm. Create a Vonage application in the Vonage Dashboard with Messages capability enabled, download the private key file, and configure environment variables in .env.local for your API credentials (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY). Create API routes in pages/api/vonage/inbound.js and pages/api/vonage/status.js to handle incoming messages and delivery status updates. Initialize the Vonage client in lib/vonageClient.js using your credentials and the Messages API sandbox URL for testing.

How do webhooks work with Vonage Messages API and Next.js?

Vonage sends POST requests to your Next.js API routes when WhatsApp events occur. The inbound webhook (/api/vonage/inbound) receives messages sent by users to your WhatsApp number, containing message_uuid, from.number, and message.content.text in the request body. The status webhook (/api/vonage/status) receives delivery status updates (submitted, delivered, read, failed) for messages you've sent. Configure these webhook URLs in your Vonage Application settings and Messages API Sandbox. Use ngrok during local development to expose your localhost server to the internet so Vonage can reach your webhooks.

How do I verify Vonage webhook signatures for security?

Verify Vonage webhook signatures using JWT (JSON Web Token) authentication with the @vonage/jwt package's verifySignature function. Extract the token from the Authorization header (format: Bearer <token>), then verify it using your VONAGE_API_SIGNATURE_SECRET from the Vonage Dashboard Settings → API settings. Implement this verification in your status webhook handler to ensure requests genuinely come from Vonage and prevent spoofing attacks. Return 401 Unauthorized for invalid signatures. The inbound webhook doesn't use JWT by default, but you can add custom security layers like Basic Authentication or IP allowlisting if needed.

What causes "Vonage client not initialized" errors?

"Vonage client not initialized" errors occur when the Vonage SDK fails to initialize in lib/vonageClient.js. Common causes include incorrect environment variables (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID), wrong VONAGE_PRIVATE_KEY file path (use relative path like ./private.key), missing or malformed private key file, or the private key file not being accessible at runtime. Check your console logs for "Error initializing Vonage client" messages, verify all environment variables are set correctly in .env.local, ensure the private.key file exists in your project root, and confirm file permissions allow Node.js to read the private key.

How do I prevent infinite message loops with Vonage WhatsApp?

Prevent infinite loops by implementing idempotency checks using the message_uuid field from incoming webhooks. Store processed message UUIDs in a Set (for development) or persistent store like Redis or a database (for production). Before processing an incoming message, check if its message_uuid already exists in your processed messages store. Also check if the incoming message text matches your bot's reply text (e.g., "Message received.") to avoid processing your own responses. The demo code includes a basic Set-based implementation with size limits, but production applications require persistent storage that survives server restarts.

What's the difference between Vonage Sandbox and production WhatsApp?

The Vonage WhatsApp Sandbox is a free testing environment with limitations: only allowlisted phone numbers (added by sending a specific message to the sandbox number) can send/receive messages, uses a shared Vonage phone number, may display watermarks on messages, has lower throughput limits, and isn't suitable for production traffic. Production requires purchasing a dedicated Vonage phone number, setting up a WhatsApp Business Account (WABA) with Meta, potentially submitting message templates for approval (for conversations initiated outside the 24-hour response window), updating your API credentials and webhook URLs, and removing the apiHost: "https://messages-sandbox.nexmo.com" override in your Vonage client initialization.

How do I send WhatsApp messages using Vonage in Next.js?

Send WhatsApp messages by creating a WhatsAppText instance from @vonage/messages with to (recipient number in E.164 format like +14155551234), from (your Vonage WhatsApp number), and text (message content) properties. Call vonage.messages.send(message) which returns a Promise containing the message_uuid for tracking. Implement proper error handling using try-catch blocks to handle rate limits, invalid numbers, or network errors. Store the returned message_uuid in your database to correlate with delivery status updates received via the status webhook. Always use E.164 phone number format (+country code + number without spaces or special characters).

What environment variables do I need for Vonage WhatsApp integration?

Required environment variables include VONAGE_API_KEY and VONAGE_API_SECRET (from Vonage Dashboard homepage), VONAGE_APPLICATION_ID (from your Vonage Application page), VONAGE_PRIVATE_KEY (relative path to downloaded private key file like ./private.key), VONAGE_WHATSAPP_NUMBER (your sandbox or production WhatsApp number without + or 00), and VONAGE_API_SIGNATURE_SECRET (from Vonage Dashboard → Settings → API settings). For deployment platforms like Vercel, create a VONAGE_PRIVATE_KEY_CONTENT variable containing the actual private key file contents as a multi-line string, since file paths don't work in serverless environments. Store these in .env.local for local development and in your deployment platform's environment variables settings for production.

How do I deploy a Vonage WhatsApp Next.js app to Vercel?

Deploy to Vercel by pushing your code to Git (ensure .env.local and private.key are in .gitignore), importing the repository in Vercel, and configuring environment variables in Settings → Environment Variables. For the private key, create a VONAGE_PRIVATE_KEY_CONTENT variable with the file's contents instead of using a file path. Modify lib/vonageClient.js to check for process.env.VONAGE_PRIVATE_KEY_CONTENT first, falling back to the file path for local development. After deployment, update your Vonage Application and Messages API webhook URLs to your Vercel production URL (https://your-app-name.vercel.app/api/vonage/inbound and /api/vonage/status). Remove or comment out the sandbox apiHost override for production use. Test by sending a message to your production WhatsApp number and checking Vercel's runtime logs.

What are common errors when integrating Vonage WhatsApp with Next.js?

Common errors include webhooks not reaching your server (check ngrok status, verify webhook URLs in both Vonage Application and Sandbox settings, inspect ngrok web interface at http://127.0.0.1:4040), 401 Unauthorized on status webhook (incorrect VONAGE_API_SIGNATURE_SECRET or wrong header format), message sending failures (verify API credentials, check E.164 phone number format, ensure recipient is allowlisted in sandbox, check Vonage Dashboard → Logs → Messages API for specific errors), infinite message loops (implement robust idempotency with persistent storage), and private key errors during deployment (use environment variable for key contents instead of file path on serverless platforms). Always check console logs and Vonage Dashboard logs for detailed error messages.

<!-- GAP: Missing FAQ about costs and pricing (Type: Substantive, Priority: Medium) -->

SMS Integration Guides:

<!-- EXPAND: Could add more related resources about WhatsApp Business API and bot development (Type: Enhancement, Priority: Low) -->

9. Deployment (Example: Vercel)

<!-- DEPTH: Deployment section lacks environment-specific configuration guidance (Priority: Medium) -->
  1. Push to Git: Commit your code (ensure .env.local and private.key are in .gitignore) and push it to a Git provider (GitHub, GitLab, Bitbucket).
  2. Import Project in Vercel: Connect your Git repository to Vercel.
  3. Configure Environment Variables: In the Vercel project settings -> Settings -> Environment Variables, add all the variables from your .env.local file (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_API_SIGNATURE_SECRET, VONAGE_WHATSAPP_NUMBER). Do not add VONAGE_PRIVATE_KEY as a file path. Instead:
    • Read the content of your local private.key file.
    • Create an environment variable in Vercel named VONAGE_PRIVATE_KEY_CONTENT (or similar) and paste the multi-line key content exactly as its value.
    • Modify lib/vonageClient.js to read the key content directly from this environment variable when deployed:
      javascript
      // lib/vonageClient.js (modification for deployment)
      import { Vonage } from '@vonage/server-sdk';
      import path from 'path';
      
      // Use key content from env var if available (Vercel/Prod), otherwise use path (local dev)
      const privateKeyValue = process.env.VONAGE_PRIVATE_KEY_CONTENT
        ? process.env.VONAGE_PRIVATE_KEY_CONTENT.replace(/\\n/g, '\n') // Ensure newlines are correct
        : path.resolve(process.cwd(), process.env.VONAGE_PRIVATE_KEY); // Fallback for local dev
      
      let vonage;
      try {
        vonage = new Vonage(
          {
            apiKey: process.env.VONAGE_API_KEY,
            apiSecret: process.env.VONAGE_API_SECRET,
            applicationId: process.env.VONAGE_APPLICATION_ID,
            privateKey: privateKeyValue, // Use the determined key value (content or path)
          },
          {
            // IMPORTANT: Remove or change apiHost for production!
            // apiHost: 'https://messages-sandbox.nexmo.com', // Keep for sandbox testing if needed
          }
        );
        console.log('Vonage client initialized successfully.');
      } catch (error) {
        console.error('Error initializing Vonage client:', error);
        vonage = null;
      }
      
      export default vonage;
  4. Deploy: Trigger a deployment in Vercel.
  5. Update Production Webhooks: Once deployed, Vercel provides a production URL (e.g., https://your-app-name.vercel.app). Update the Vonage Application and production Messages API webhooks (not the Sandbox ones, unless you intend to keep using it) to use this URL:
    • Inbound URL: https://your-app-name.vercel.app/api/vonage/inbound
    • Status URL: https://your-app-name.vercel.app/api/vonage/status
  6. Test Production: Send a message to your production Vonage WhatsApp number (if configured) and verify the flow. Check Vercel's runtime logs for any errors.
<!-- GAP: Missing rollback and CI/CD integration guidance (Type: Substantive, Priority: Low) --> <!-- EXPAND: Could add information about other deployment platforms (Type: Enhancement, Priority: Low) -->

Frequently Asked Questions

How to integrate WhatsApp into Next.js app

Integrate WhatsApp by using Vonage's Messages API and Node.js SDK within a Next.js application. Set up API routes in your Next.js app to handle incoming messages and send replies via webhooks, enabling two-way communication with users on WhatsApp.

What is Vonage Messages API used for

The Vonage Messages API allows you to send and receive messages across various channels, including WhatsApp. It's the core service for integrating WhatsApp messaging into your application, enabling communication between your app and WhatsApp users.

Why use ngrok with Vonage WhatsApp

ngrok creates a public tunnel to your local development server, essential for Vonage's webhooks to reach your Next.js app during development. This lets you test your WhatsApp integration locally before deploying it live.

When to configure production webhooks Vonage

Configure production webhooks after deploying your Next.js application. Replace the ngrok URL with your production URL (e.g. Vercel URL) in your Vonage application settings. This directs Vonage's messages to your live application.

Can I use WhatsApp sandbox for testing?

Yes, Vonage provides a WhatsApp Sandbox for testing. This allows you to test your integration with allowlisted numbers and familiarize yourself with the API without incurring costs or using a live WhatsApp Business Account.

How to set up Next.js for Vonage WhatsApp

Create a new Next.js project, install necessary dependencies like '@vonage/server-sdk', '@vonage/messages', '@vonage/jwt', and 'dotenv'. Then, create API routes to manage WhatsApp interactions and configure environment variables with your Vonage credentials.

What is the Vonage Node SDK's role?

The Vonage Node SDK simplifies interaction with the Vonage APIs within your Next.js application. It handles authentication, message sending, and receiving status updates, making integration smoother than working with the API directly.

How to handle Vonage webhook security

Secure your Vonage webhooks, particularly the status updates, by verifying JWT signatures using your `VONAGE_API_SIGNATURE_SECRET`. This ensures requests originate from Vonage, protecting against unauthorized access.

How to send WhatsApp message with Next.js

Use the `sendWhatsAppReply` function within your inbound message handler. This function leverages the Vonage Node SDK to send text messages to users on WhatsApp. Be sure to use the WhatsApp number linked to the Sandbox during testing and your WABA in production.

How to manage Vonage API credentials securely

Store your Vonage API credentials (API key, secret, application ID, private key path, signature secret) in a `.env.local` file for local development, which is automatically excluded from version control. For production deployments, utilize platform-specific environment variables, keeping sensitive data safe.

What are Vonage WhatsApp integration prerequisites

You need Node.js 20+, npm or yarn, a Vonage API account with associated API keys and secrets, a Vonage application with a linked WhatsApp Sandbox number, ngrok for local testing, and a personal WhatsApp account for sandbox interaction.

Why does Vonage need my WhatsApp number

During the sandbox testing phase, you must allowlist your personal WhatsApp number to send and receive messages with the Vonage Sandbox number. This restriction is in place for testing and security purposes.

When should I use a database with Vonage WhatsApp

A database is recommended for production Vonage WhatsApp integrations to store message logs, manage conversation state (especially for bots), maintain user data linked to WhatsApp numbers, and ensure reliable message tracking and status updates.

How to troubleshoot Vonage WhatsApp integration issues

Common issues include webhooks not reaching your server, 401 errors due to incorrect signature secrets, and message sending failures due to credential issues or un-allowlisted numbers. Check logs, verify credentials, and ensure correct webhook URLs to diagnose and fix problems.