code examples

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

How to Receive SMS Messages in Next.js: Vonage Inbound Webhook Tutorial 2025

Learn how to receive and respond to SMS messages in Next.js using Vonage webhooks. Step-by-step guide with webhook setup, API routes, database integration, and production deployment for two-way messaging.

Receive and respond to SMS messages seamlessly within your Next.js application using the Vonage Messages API. This comprehensive tutorial provides a complete walkthrough, from setting up your Vonage account and Next.js project to implementing webhook handlers, sending replies, and deploying a production-ready solution for inbound SMS messaging.

You'll build a Next.js application capable of receiving incoming SMS messages sent to a Vonage virtual number via webhooks. The application will log these messages, store them in a database, and demonstrate how to send replies for two-way SMS communication. This solves the common need for applications to interact with users via SMS for notifications, alerts, customer support, or two-factor authentication follow-ups.

Project Overview and Goals

What You'll Build:

  • A Next.js application with an API route acting as a webhook endpoint for receiving SMS.
  • Integration with the Vonage Messages API using the @vonage/server-sdk.
  • Functionality to receive incoming SMS messages sent to a dedicated Vonage number.
  • Capability to log incoming message details and store them in a database.
  • (Optional) Functionality to programmatically send SMS messages as replies or standalone messages for two-way communication.

Technologies Used:

  • Next.js: A React framework providing server-side rendering, static site generation, and simplified API route creation – ideal for handling webhooks.
  • Node.js: The JavaScript runtime environment underpinning Next.js.
  • Vonage Messages API: Enables sending and receiving messages across various channels, including SMS.
  • @vonage/server-sdk: The official Node.js library for interacting with Vonage APIs.
  • ngrok: A tool to expose local development servers to the internet, essential for testing webhooks.

System Architecture:

mermaid
sequenceDiagram
    participant User Phone
    participant Vonage Platform
    participant ngrok
    participant Next.js App (API Route)

    User Phone->>+Vonage Platform: Sends SMS to Vonage Number
    Vonage Platform->>+ngrok: Forwards SMS data via HTTP POST (Webhook)
    ngrok->>+Next.js App (API Route): Relays POST request to /api/webhooks/inbound
    Next.js App (API Route)->>Next.js App (API Route): Processes inbound message (logs, etc.)
    Next.js App (API Route)-->>-ngrok: Sends HTTP 200 OK response
    ngrok-->>-Vonage Platform: Relays 200 OK response
    Vonage Platform-->>-User Phone: (No direct response shown, SMS delivery confirmed)

    %% Optional Reply Flow
    Note over Next.js App (API Route): Logic decides to send a reply
    Next.js App (API Route)->>+Vonage Platform: Calls Messages API (send SMS) using SDK
    Vonage Platform->>+User Phone: Delivers reply SMS
    User Phone-->>-Vonage Platform: (Receives SMS)
    Vonage Platform-->>-Next.js App (API Route): Returns API response (message_uuid)

Prerequisites:

  • Vonage API Account: Create one at Vonage API Dashboard. You'll need your API Key and Secret.
  • Node.js: Version 20.x or later recommended. As of January 2025, Node.js 22 LTS (codenamed 'Jod', released October 29, 2024) is the current LTS version with support until April 2027. Node.js 18.x reaches end-of-life on April 30, 2025 – plan to upgrade to Node.js 20 or 22 for continued support. Download Node.js.
  • npm or yarn: Node.js package manager, included with Node.js.
  • Next.js: This guide uses Next.js 15.x (latest: 15.2 as of February 2025) with App Router support. The App Router is recommended for new projects and uses React Server Components and other modern features.
  • A Vonage Phone Number: Purchase an SMS-enabled number through the Vonage Dashboard.
  • ngrok: Install it globally or use npx. A free account is sufficient but has limitations: 2-hour session timeouts (requiring URL regeneration), 1GB bandwidth, and a browser warning page for HTML traffic. For production testing, consider paid plans or alternatives. ngrok Setup.
  • Basic understanding of Next.js and API routes.

Final Outcome:

By the end of this guide, you will have a functional Next.js application that reliably receives incoming SMS messages via Vonage webhooks and can be extended to send replies or perform other actions based on the message content.

How to Set Up a Next.js Project for Receiving SMS Webhooks

Initialize a new Next.js project and install the necessary dependencies to handle inbound SMS messages.

  1. Create a Next.js App: Open your terminal and run the following command, replacing vonage-nextjs-sms with your desired project name. Choose your preferred settings when prompted (TypeScript recommended, App Router used here but adaptable for Pages Router).

    bash
    npx create-next-app@latest vonage-nextjs-sms
  2. Navigate to Project Directory:

    bash
    cd vonage-nextjs-sms
  3. Install Vonage SDK: We need the Vonage server SDK to interact with the Messages API. As of October 2025, @vonage/server-sdk v3.24.1 is the latest version, offering full promise-based APIs with TypeScript support for improved type safety and IDE integration.

    bash
    npm install @vonage/server-sdk

    or using yarn:

    bash
    yarn add @vonage/server-sdk

    Note: The @vonage/server-sdk provides official support for Vonage APIs including SMS, Voice, Text-to-Speech, Numbers, Verify (2FA), and Messages API. The SDK handles authentication, request formatting, and error handling for all Vonage API interactions.

  4. Set up Environment Variables: Sensitive credentials like API keys should never be hardcoded. Use environment variables instead. Create a file named .env.local in the root of your project.

    Terminal:

    bash
    touch .env.local

    Add the following variables to .env.local. You'll populate these values in the "Integrating with Vonage" section.

    .env.local:

    dotenv
    # Vonage Credentials
    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 # Path relative to project root
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Include country code, e.g., 12015550123
    
    # For sending tests (Optional)
    TO_NUMBER=YOUR_PERSONAL_PHONE_NUMBER # Include country code

    Note on VONAGE_PRIVATE_KEY_PATH: Using a file path is convenient for local development. However, for deployment (covered in Section 11), embedding the private key content directly into an environment variable (e.g., VONAGE_PRIVATE_KEY_CONTENT) is often a more robust approach, especially in serverless environments.

    Important: Add .env.local and your private key file (e.g., private.key) to your .gitignore file to prevent committing sensitive information.

    .gitignore (ensure these lines exist):

    text
    # local environment variables
    .env*.local
    
    # Vonage private key
    private.key
    *.key

    Why .env.local? Next.js automatically loads variables from this file into process.env for server-side code (like API routes), keeping your secrets secure and separate from your codebase.

How to Create an Inbound SMS Webhook Handler in Next.js

Create a Next.js API route to act as the webhook endpoint Vonage will call when an SMS is received. This webhook handler will process incoming messages and return the required response.

  1. Create the API Route File: Create the necessary directories and the API route file.

    Using App Router (default for create-next-app):

    bash
    mkdir -p app/api/webhooks/inbound
    touch app/api/webhooks/inbound/route.js

    Using Pages Router (if selected during setup):

    bash
    mkdir -p pages/api/webhooks
    touch pages/api/webhooks/inbound.js
  2. Implement the Webhook Handler: Paste the following code into the route.js (App Router) or inbound.js (Pages Router) file you created.

    app/api/webhooks/inbound/route.js (App Router):

    javascript
    import { NextResponse } from 'next/server';
    
    export async function POST(request) {
      try {
        const inboundSms = await request.json(); // Vonage sends JSON payload
        console.log('--- Inbound SMS Received ---');
        console.log('From:', inboundSms.from);
        console.log('To:', inboundSms.to);
        console.log('Text:', inboundSms.text);
        console.log('Message UUID:', inboundSms.message_uuid);
        console.log('Full Payload:', JSON.stringify(inboundSms, null, 2));
        console.log('----------------------------');
    
        // --- Add your custom logic here ---
        // Example: Store the message in a database, trigger another service, etc.
        // Example: Prepare data for a reply (see sending section)
    
        // Vonage requires a 200 OK response to acknowledge receipt.
        // Failure to send 200 OK will cause Vonage to retry the webhook.
        return new NextResponse(null, { status: 200 });
    
      } catch (error) {
        console.error('Error processing inbound SMS:', error);
        // Return 500 Internal Server Error if processing fails
        return new NextResponse(JSON.stringify({ error: 'Failed to process webhook' }), {
          status: 500,
          headers: { 'Content-Type': 'application/json' },
        });
      }
    }
    
    export async function GET(request) {
      // Optional: Handle GET requests for health checks or simple verification
      return new NextResponse(JSON.stringify({ message: 'Webhook endpoint is active. Use POST for inbound SMS.' }), {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    pages/api/webhooks/inbound.js (Pages Router):

    javascript
    // pages/api/webhooks/inbound.js
    export default async function handler(req, res) {
      if (req.method === 'POST') {
        try {
          const inboundSms = req.body; // Assumes body-parsing middleware is active (default in Next.js)
          console.log('--- Inbound SMS Received ---');
          console.log('From:', inboundSms.from);
          console.log('To:', inboundSms.to);
          console.log('Text:', inboundSms.text);
          console.log('Message UUID:', inboundSms.message_uuid);
          console.log('Full Payload:', JSON.stringify(inboundSms, null, 2));
          console.log('----------------------------');
    
          // --- Add your custom logic here ---
          // Example: Store the message in a database, trigger another service, etc.
    
          // Vonage requires a 200 OK response to acknowledge receipt.
          res.status(200).end(); // Send 200 OK
    
        } catch (error) {
          console.error('Error processing inbound SMS:', error);
          res.status(500).json({ error: 'Failed to process webhook' });
        }
      } else if (req.method === 'GET') {
         // Optional: Handle GET requests for health checks
         res.status(200).json({ message: 'Webhook endpoint is active. Use POST for inbound SMS.' });
      } else {
        // Handle unsupported methods
        res.setHeader('Allow', ['POST', 'GET']);
        res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    }

    Why this code?

    • It defines an API route at /api/webhooks/inbound.
    • It specifically handles POST requests, which is how Vonage sends webhook data.
    • It parses the incoming JSON payload (request.json() for App Router, req.body for Pages Router).
    • It logs key information from the SMS message for debugging.
    • Crucially, it sends back an empty 200 OK response. This is vital to tell Vonage the message was received successfully and prevent retries.
    • Basic error handling logs issues and returns a 500 status code.

    Note on Code Examples: For brevity, subsequent code modifications in this guide (e.g., adding logging, database integration) will primarily show the App Router version (route.js). The core logic can be adapted to the Pages Router structure (inbound.js) using req and res objects.

How to Configure Vonage to Send Inbound SMS to Your Webhook

Configure Vonage to work with your Next.js application and route incoming SMS messages to your webhook endpoint.

  1. Log in to Vonage: Access your Vonage API Dashboard.

  2. Get API Credentials: On the main dashboard page, find your API key and API secret. Copy these values.

  3. Update .env.local: Paste your API key and secret into the VONAGE_API_KEY and VONAGE_API_SECRET variables in your .env.local file.

  4. Ensure Messages API is Default:

    • Navigate to API Settings in the left-hand menu.
    • Scroll down to the SMS settings section.
    • Ensure "Default SMS Setting" is set to Messages API. If not, select it and click Save changes. This is crucial for the webhooks to have the correct format expected by your code.
  5. Create a Vonage Application:

    • Navigate to Applications > Create a new application.
    • Give your application a descriptive name (e.g., "NextJS SMS Handler").
    • Click Generate public and private key. This will automatically download a private.key file. Save this file securely.
    • Move the downloaded private.key file into the root directory of your Next.js project. The path ./private.key in .env.local assumes it's in the root.
    • Enable the Messages capability by toggling it on.
    • You will see fields for Inbound URL and Status URL. Fill these in the next step using ngrok. Leave them blank for now or use a placeholder like http://localhost.
    • Click Generate new application.
    • You will be redirected to the application's page. Copy the Application ID.
  6. Update .env.local: Paste the Application ID into VONAGE_APPLICATION_ID in your .env.local file.

  7. Link Your Vonage Number:

    • On the same application details page, scroll down to the Link virtual numbers section.
    • Find the SMS-enabled Vonage number you purchased earlier.
    • Click the Link button next to it.
    • Enter your Vonage number (including country code, no symbols, e.g., 12015550123) into the VONAGE_NUMBER variable in .env.local.
    • Enter your personal phone number into TO_NUMBER if you plan to test sending messages.
  8. Expose Local Server with ngrok:

    • Start your Next.js development server (if not already running):

      bash
      npm run dev
    • Open a new terminal window/tab in the same project directory.

    • Run ngrok to expose your local port 3000 (default for Next.js dev):

      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) Forwarding https://<random-string>.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 Forwarding URL (e.g., https://<random-string>.ngrok-free.app). Do not close this ngrok terminal.

  9. Update Vonage Application URLs:

    • Go back to your Vonage Application settings page (Applications > Click your application name).
    • Find the Messages capability section again.
    • In the Inbound URL field, paste your ngrok HTTPS URL and append /api/webhooks/inbound. Example: https://<random-string>.ngrok-free.app/api/webhooks/inbound
    • In the Status URL field, you can use the same URL or create a separate handler. For simplicity, let's use the same one for now: https://<random-string>.ngrok-free.app/api/webhooks/inbound (You can create a /api/webhooks/status route later if needed).
    • Scroll down and click Save changes.

Your Vonage application is now configured to send incoming SMS messages for your linked number to your local Next.js application via ngrok.

What Error Handling and Logging Should You Implement for SMS Webhooks?

Our basic webhook handler includes initial logging and error handling, but let's refine it for production use.

  • Consistent Error Handling: The try...catch block in the API route is the foundation. Ensure any errors encountered during your custom logic (database writes, external API calls) are caught and logged. Always aim to return a 500 status if processing fails internally, but only after logging the error details.
  • Logging: console.log is suitable for development. For production, consider structured logging libraries like pino or winston. These enable:
    • Log Levels: Differentiating between debug, info, warn, error.
    • Structured Formats (JSON): Easier parsing by log aggregation tools (Datadog, Splunk, ELK stack).
    • Log Destinations: Sending logs to files, external services, or standard output. In production, especially serverless, configuring transports to send logs to an external aggregation service is crucial, rather than relying solely on console output.
  • Retry Mechanism Awareness: Vonage will retry sending the webhook if it doesn't receive a 2xx response (ideally 200 OK) within a certain timeout (usually a few seconds). Your webhook logic should be idempotent if possible – meaning receiving the same message multiple times doesn't cause duplicate actions or errors. Logging the message_uuid helps identify duplicate deliveries. If your processing takes time, consider immediately returning 200 OK and then processing the message asynchronously (e.g., using a background job queue like BullMQ or Kue).

Example using Pino (Basic Setup):

  1. Install Pino: npm install pino

  2. Update API route (showing App Router example):

    app/api/webhooks/inbound/route.js (App Router - Example):

    javascript
    import { NextResponse } from 'next/server';
    import pino from 'pino';
    
    // Initialize logger (adjust options for production, e.g., transports)
    const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
    
    export async function POST(request) {
      const startTime = Date.now();
      let messageUuid = 'unknown'; // Default
    
      try {
        const inboundSms = await request.json();
        messageUuid = inboundSms.message_uuid || 'unknown'; // Get UUID early
    
        // Use structured logging
        logger.info({ msg: 'Inbound SMS Received', data: inboundSms }, `Processing message ${messageUuid}`);
    
        // --- Add your custom logic here ---
        // if (someConditionFails) {
        //   throw new Error('Custom logic failed');
        // }
    
        const duration = Date.now() - startTime;
        logger.info({ messageUuid, duration }, `Successfully processed message ${messageUuid} in ${duration}ms`);
        return new NextResponse(null, { status: 200 });
    
      } catch (error) {
        const duration = Date.now() - startTime;
        // Log error object with context
        logger.error({ err: error, messageUuid, duration }, `Error processing message ${messageUuid} in ${duration}ms`);
        return new NextResponse(JSON.stringify({ error: 'Failed to process webhook' }), {
          status: 500,
          headers: { 'Content-Type': 'application/json' },
        });
      }
    }
    // ... (GET handler remains the same)

How to Store Inbound SMS Messages in a Database with Prisma

If you need to store incoming messages or related data, you'll need a database. Prisma is a popular choice with Next.js, offering excellent TypeScript support and type-safe database queries. Prisma officially supports Next.js 15 and recommends version @prisma/client@5.12.0 or above for middleware and edge runtime compatibility.

  1. Install Prisma:

    bash
    npm install prisma --save-dev
    npm install @prisma/client
  2. Initialize Prisma:

    bash
    npx prisma init --datasource-provider postgresql # Or your preferred DB (sqlite, mysql, etc.)

    This creates a prisma directory with a schema.prisma file and updates .env (or .env.local) with DATABASE_URL.

  3. Define Schema: Edit prisma/schema.prisma to define a model for SMS messages.

    prisma/schema.prisma:

    prisma
    // This is your Prisma schema file,
    // learn more about it in the docs: https://pris.ly/d/prisma-schema
    
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql" // Or your chosen provider
      url      = env("DATABASE_URL")
    }
    
    model SmsMessage {
      id          String   @id @default(cuid())
      messageUuid String   @unique @map("message_uuid") // Map to Vonage field name
      fromNumber  String   @map("from_number")
      toNumber    String   @map("to_number")
      text        String?  // Message text can be optional for some types
      channel     String   @default("sms")
      receivedAt  DateTime @default(now()) @map("received_at")
      processedAt DateTime? @map("processed_at") // Timestamp when your logic processed it
    
      @@map("sms_messages") // Optional: specify table name
    }
  4. Run Migrations: Set up your database connection string in .env.local (DATABASE_URL). Then create and apply the migration.

    bash
    # Create migration files based on schema changes
    npx prisma migrate dev --name init_sms_message
    
    # Apply migrations (usually done by the above command)
    # npx prisma migrate deploy
  5. Use Prisma Client in API Route:

    lib/prisma.js (Example Singleton):

    javascript
    import { PrismaClient } from '@prisma/client';
    
    let prisma;
    
    if (process.env.NODE_ENV === 'production') {
      prisma = new PrismaClient();
    } else {
      // Ensure the prisma instance is re-used during hot-reloading in development
      if (!global.prisma) {
        global.prisma = new PrismaClient({
           // log: ['query', 'info', 'warn', 'error'], // Uncomment for verbose logs
        });
      }
      prisma = global.prisma;
    }
    
    export default prisma;

    app/api/webhooks/inbound/route.js (Update - App Router):

    javascript
    // ... other imports (NextResponse, pino)
    import prisma from '@/lib/prisma'; // Adjust path as needed
    import pino from 'pino';
    
    const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
    
    export async function POST(request) {
       const startTime = Date.now();
       let messageUuid = 'unknown';
    
       try {
         const inboundSms = await request.json();
         messageUuid = inboundSms.message_uuid || 'unknown';
         logger.info({ msg: 'Inbound SMS Received', data: inboundSms }, `Processing message ${messageUuid}`);
    
         // --- Database Interaction ---
         try {
           const storedMessage = await prisma.smsMessage.create({
             data: {
               messageUuid: inboundSms.message_uuid,
               fromNumber: inboundSms.from,
               toNumber: inboundSms.to,
               text: inboundSms.text,
               channel: inboundSms.channel || 'sms',
               // receivedAt is handled by default
               processedAt: new Date(), // Mark as processed now
             },
           });
           logger.info({ dbId: storedMessage.id, messageUuid }, 'Stored inbound message to DB');
         } catch (dbError) {
            // Handle potential unique constraint violation if message is reprocessed
            if (dbError.code === 'P2002' && dbError.meta?.target?.includes('message_uuid')) {
               logger.warn({ messageUuid }, 'Duplicate message received, already processed.');
               // Still return 200 OK to Vonage for duplicates if desired
               return new NextResponse(null, { status: 200 });
            } else {
               logger.error({ err: dbError, messageUuid }, 'Database error storing message');
               // Rethrow or handle specifically - might warrant a 500 response
               throw dbError; // Let outer catch handle sending 500
            }
         }
         // --- End Database Interaction ---
    
         const duration = Date.now() - startTime;
         logger.info({ messageUuid, duration }, `Successfully processed message ${messageUuid} in ${duration}ms`);
         return new NextResponse(null, { status: 200 });
    
       } catch (error) {
          const duration = Date.now() - startTime;
          logger.error({ err: error, messageUuid, duration }, `Error processing message ${messageUuid} in ${duration}ms`);
          return new NextResponse(JSON.stringify({ error: 'Failed to process webhook' }), {
            status: 500,
            headers: { 'Content-Type': 'application/json' },
          });
       }
    }
    // ... (GET handler)

What Security Features Should You Add to SMS Webhook Endpoints?

Securing your webhook endpoint is critical for production deployments.

  1. Input Validation:
    • While the Vonage payload is generally trusted, basic checks ensure required fields exist before processing. Libraries like zod or joi can validate the incoming req.body structure against a predefined schema.
    • Sanitize any data before storing it in a database or using it in replies to prevent cross-site scripting (XSS) or SQL injection, although Prisma helps significantly against SQL injection.
  2. Verify Vonage Signatures (Highly Recommended for Production):
    • Vonage can sign webhook requests using JWT (JSON Web Tokens) with your Signature secret (found in API Settings). This verifies the request genuinely originated from Vonage.

    • The @vonage/server-sdk includes helpers for this. Implementing this adds a robust layer of security.

    • Refer to the official Vonage documentation for Secure Webhooks and SDK examples for detailed implementation guidance specific to your framework setup.

    • Conceptual Example Snippet:

      javascript
      // Inside your POST handler (Conceptual - Adapt to your framework/SDK usage)
      import { Vonage } from '@vonage/server-sdk'; // Or specific signature verification import
      
      // ... inside async function POST(request) ...
      try {
        const rawBody = await request.text(); // Need raw body for verification
        const headers = request.headers; // Need request headers
        const signatureSecret = process.env.VONAGE_SIGNATURE_SECRET;
      
        if (!signatureSecret) {
          logger.warn('VONAGE_SIGNATURE_SECRET not set. Skipping signature verification.');
          // Handle accordingly - maybe allow in dev, block in prod?
        } else {
          // Conceptual: SDK might offer a function like this
          // const isValid = Vonage.verifySignature(headers.get('authorization'), rawBody, signatureSecret);
          // Or you might need to parse JWT manually using `jsonwebtoken` library
      
          // Replace with actual SDK/library method call:
          const isValid = verifyWebhookSignature(headers, rawBody, signatureSecret); // Placeholder for actual verification logic
      
          if (!isValid) {
            logger.warn({ messageUuid }, 'Invalid Vonage signature received.');
            return new NextResponse(JSON.stringify({ error: 'Invalid signature' }), { status: 401 });
          }
          logger.info({ messageUuid }, 'Vonage signature verified successfully.');
        }
      
        // If valid (or skipped), proceed to parse JSON and process
        const inboundSms = JSON.parse(rawBody);
        messageUuid = inboundSms.message_uuid || 'unknown';
        // ... rest of your processing logic ...
      
      } catch (error) {
         // ... error handling ...
      }
      
      // Placeholder function for actual verification logic
      function verifyWebhookSignature(headers, rawBody, secret) {
         // Implement actual verification using Vonage SDK helpers or JWT library
         // based on Vonage documentation. This involves checking the 'Authorization' header (Bearer token).
         console.warn("Signature verification logic not fully implemented in this example.");
         return true; // Replace with actual verification result
      }

      Note: Implementing signature verification requires careful handling of the raw request body and headers. Consult the @vonage/server-sdk documentation or Vonage developer resources for the precise method. You'll need your Signature Secret from the Vonage Dashboard API Settings.

  3. Rate Limiting:
    • Protect your endpoint from abuse or accidental loops.
    • Vercel: Provides rate limiting features at the edge.
    • Middleware: Use Next.js middleware with libraries like rate-limiter-flexible or upstash/ratelimit to implement custom limits.
  4. Environment Variables Security:
    • Never commit .env.local or your private.key to Git.
    • Use secrets management solutions provided by your hosting platform (e.g., Vercel Environment Variables, AWS Secrets Manager) for production deployments.
  5. HTTPS: Always use HTTPS for your webhook URLs (ngrok provides this for free; production deployments on platforms like Vercel are HTTPS by default).

How to Handle Special SMS Cases and Edge Scenarios

  • Character Encoding: The Vonage Messages API generally handles standard SMS encoding (GSM-7, UCS-2 for Unicode characters like emoji). Ensure your application correctly handles UTF-8 when processing the text.
  • Message Concatenation: Longer SMS messages are split into multiple parts but usually reassembled by the receiving device. The Vonage payload might include information about multipart messages (sms: { num_messages: '...' }), but typically you receive the full text in the text field.
  • Non-Text Messages: The Messages API supports MMS, WhatsApp, etc. If you enable other channels on your Vonage application, your webhook might receive different payload structures. Adapt your parsing logic accordingly, checking the channel field.
  • Time Zones: Timestamps in the Vonage payload (timestamp) are typically in UTC (ISO 8601 format). Store dates in UTC in your database and convert to the user's local time zone only when displaying.
  • Error Codes from Vonage: When sending messages, handle potential error responses from the Vonage API (e.g., invalid number, insufficient funds). The SDK usually throws errors with details.

What Performance Optimizations Should You Apply to SMS Webhooks?

For a simple webhook receiver, major optimizations are often unnecessary, but consider:

  • Webhook Response Time: Respond with 200 OK as quickly as possible. Offload time-consuming tasks (database writes, external API calls, complex logic) to background jobs/queues if they risk timing out the webhook request.
  • Database Queries: Index frequently queried columns (e.g., messageUuid, fromNumber, receivedAt) if you store messages.
  • Caching: If you frequently look up related data to process messages (e.g., user information based on fromNumber), cache this data (e.g., using Redis or an in-memory cache) to avoid repeated database hits.
  • Resource Usage: Monitor CPU and memory usage, especially if running complex logic within the webhook handler. Next.js serverless functions have resource limits.

How to Monitor and Track SMS Webhook Performance

In production, visibility is key.

  • Health Checks: The GET handler in our API route provides a basic health check. Monitoring services can ping this endpoint to ensure the webhook is live.
  • Error Tracking: Integrate services like Sentry or Bugsnag to capture, track, and alert on errors occurring in your API route.
  • Logging Aggregation: Use platforms like Datadog, Logz.io, Papertrail, or the ELK stack to collect, search, and analyze logs from your application. Set up alerts for high error rates or specific log patterns.
  • Performance Metrics: Hosting platforms like Vercel provide analytics on function duration, invocation counts, and error rates. Dedicated Application Performance Monitoring (APM) tools (Datadog APM, New Relic) offer deeper insights.
  • Dashboards: Create dashboards showing key metrics like incoming message volume, webhook response times, error rates, and processing duration.

How to Troubleshoot Common Vonage SMS Webhook Issues

  • Webhook Not Triggering:
    • Check ngrok: Is ngrok still running? Has the URL expired (free tier URLs are temporary)?
    • Check Vonage URLs: Are the Inbound/Status URLs in the Vonage Application settings exactly matching your ngrok HTTPS URL + /api/webhooks/inbound?
    • Check Number Linking: Is the correct Vonage number linked to the correct Vonage Application?
    • Check Default API: Is "Messages API" set as the default SMS setting in Vonage API Settings?
    • Firewall: Is any local or network firewall blocking requests from ngrok or Vonage?
  • Receiving 5xx Errors:
    • Check your Next.js application logs (the terminal where npm run dev is running, or your production logging service) for detailed error messages immediately preceding the 5xx response.
    • Did the JSON parsing fail? Is there an error in your custom logic? Database connection issue?
  • Vonage Retrying Webhooks:
    • Your webhook is likely not returning a 200 OK status code quickly enough or at all. Check logs for errors or timeouts. Ensure res.status(200).end() (Pages) or return new NextResponse(null, { status: 200 }); (App) is reached successfully.
  • Environment Variables Not Loaded:
    • Ensure the file is named exactly .env.local.
    • Restart your development server (npm run dev) after modifying .env.local.
    • In production (e.g., Vercel), ensure environment variables are configured in the deployment platform's settings, not relying solely on .env.local.

Frequently Asked Questions About Vonage SMS Webhooks with Next.js

How do you receive SMS messages in Next.js using Vonage?

To receive SMS messages in Next.js with Vonage, create a Next.js API route (e.g., /api/webhooks/inbound) that handles POST requests. Configure this endpoint URL in your Vonage Application settings as the Inbound URL. When someone sends an SMS to your Vonage number, Vonage sends a POST request to your webhook with the message data. Your API route must return a 200 OK response to acknowledge receipt.

What Node.js and Next.js versions are required for Vonage SMS webhooks?

You need Node.js 20.x or later. As of January 2025, Node.js 22 LTS (codenamed 'Jod', released October 29, 2024) is the current LTS version with support until April 2027. Node.js 18.x reaches end-of-life on April 30, 2025. This guide uses Next.js 15.x (latest: 15.2 as of February 2025) with App Router support, which is recommended for new projects and uses React Server Components.

What is the latest version of @vonage/server-sdk for receiving SMS?

As of October 2025, @vonage/server-sdk v3.24.1 is the latest version. It offers full promise-based APIs with TypeScript support for improved type safety and IDE integration. The SDK provides official support for Vonage APIs including SMS, Voice, Text-to-Speech, Numbers, Verify (2FA), and Messages API, handling authentication, request formatting, and error handling for all Vonage API interactions.

How do you secure Vonage SMS webhook endpoints in production?

Implement multiple security layers: verify Vonage JWT signatures using your Signature secret (found in API Settings), validate incoming payload structure with libraries like zod or joi, implement rate limiting using Next.js middleware or Vercel edge features, always use HTTPS for webhook URLs, store credentials in secure environment variables (never commit .env.local or private.key to Git), and use secrets management solutions like Vercel Environment Variables or AWS Secrets Manager for production deployments.

Can you use Prisma with Next.js 15 for storing inbound SMS messages?

Yes, Prisma officially supports Next.js 15 and recommends version @prisma/client@5.12.0 or above for middleware and edge runtime compatibility. Prisma offers excellent TypeScript support and type-safe database queries, making it ideal for storing incoming SMS messages, tracking delivery status, and managing conversation history. The integration handles database connection pooling and provides automatic query optimization.

What are the limitations of ngrok's free tier for webhook testing?

ngrok's free tier has several limitations: 2-hour session timeouts (requiring URL regeneration and Vonage configuration updates), 1GB bandwidth cap, browser warning pages for HTML traffic, and random, ephemeral URLs that change each session. For production testing or stable development URLs, consider paid ngrok plans or alternatives like Cloudflare Tunnel or Pinggy.

How does Vonage handle webhook retry logic for inbound SMS?

Vonage automatically retries sending webhooks if it doesn't receive a 2xx response (ideally 200 OK) within a timeout period. Make your webhook logic idempotent – receiving the same message multiple times shouldn't cause duplicate actions or errors. Log the message_uuid to identify duplicate deliveries. For time-consuming processing, immediately return 200 OK and process messages asynchronously using background job queues like BullMQ or Redis Queue.

What error handling should you implement for inbound SMS webhooks?

Implement comprehensive error handling: use try...catch blocks to catch all errors, log errors with structured logging (Pino or Winston) including message_uuid for tracking, return 500 status codes only after logging errors, handle Prisma unique constraint violations (P2002) for duplicate messages, validate required fields exist before processing, and integrate error tracking services like Sentry or Bugsnag to capture and alert on production errors.

How do you test Vonage SMS webhooks locally during development?

Use ngrok to expose your local Next.js development server (port 3000) to the internet. Run ngrok http 3000 to get an HTTPS URL, then configure this URL + /api/webhooks/inbound in your Vonage Application settings as the Inbound URL. Send a test SMS to your Vonage number and monitor your terminal logs. Remember to update the Vonage URLs each time you restart ngrok (free tier) or use a static domain (paid plan).

What database schema should you use for storing Vonage inbound SMS messages?

Create a schema with essential fields: id (primary key), messageUuid (unique, maps to Vonage's message_uuid), fromNumber, toNumber, text, channel (defaults to "sms"), receivedAt (timestamp), processedAt (nullable timestamp), and optionally errorMessage and retryCount for error tracking. Index frequently queried columns like messageUuid, fromNumber, and receivedAt for query performance. Store timestamps in UTC and convert to local timezone only for display.

How do you implement two-way SMS messaging with Vonage and Next.js?

To implement two-way SMS messaging, first set up an inbound webhook to receive SMS messages as described in this guide. Then, use the Vonage Messages API SDK to send reply messages from your webhook handler. Extract the sender's number from inboundSms.from, craft your reply message, and call vonage.messages.send() with the appropriate parameters. Store conversation history in a database to maintain context across multiple messages.

What's the difference between Vonage SMS API and Messages API for webhooks?

The Messages API is the newer, unified API that supports multiple channels (SMS, WhatsApp, MMS, Viber, etc.) and provides a consistent webhook payload format across channels. The SMS API is the legacy API focused only on SMS. For new projects, Vonage recommends using the Messages API. Ensure "Messages API" is set as the default in your Vonage Dashboard API Settings to receive the correct webhook payload format.

How do you handle concurrent inbound SMS messages in Next.js?

Next.js API routes in serverless environments (like Vercel) automatically handle concurrent requests by spawning separate function instances. Each incoming webhook is processed independently. Ensure your database operations are atomic and use unique constraints on message_uuid to prevent duplicate storage. For high-volume applications, consider implementing a message queue (Redis Queue, BullMQ) to manage processing order and prevent overwhelming downstream services.

Frequently Asked Questions

How to receive SMS messages in Next.js?

Use the Vonage Messages API and a Next.js API route as a webhook endpoint. Set up your Vonage account, create a Next.js project, and implement the webhook handler to receive incoming SMS messages sent to your Vonage virtual number. The API route should parse the incoming JSON payload and log the message details for debugging and processing.

What is the Vonage Messages API used for?

The Vonage Messages API allows you to send and receive messages across various channels, including SMS. This is useful for applications needing to interact with users via SMS for notifications, alerts, customer support, or two-factor authentication.

Why does Vonage require a 200 OK response?

A 200 OK response is essential to acknowledge successful receipt of the SMS message by your webhook. Without it, Vonage will retry sending the webhook, potentially leading to duplicate processing. This acknowledgement ensures reliable message delivery and prevents unnecessary retries.

When should I use ngrok with Vonage?

`ngrok` is crucial during local development to expose your Next.js development server to the internet, allowing Vonage to send webhooks to your local machine. It creates a public URL that tunnels requests to your local server, which is essential for testing your integration.

Can I store received SMS messages in a database?

Yes, you can store received SMS messages and related data in a database. The article recommends Prisma as a suitable option with Next.js, guiding you through defining a schema, running migrations, and using Prisma Client to store message details like sender, recipient, and text content.

How to set up Vonage API with Next.js?

Integrate Vonage by obtaining API credentials from your Vonage dashboard, updating your `.env.local` file, and creating a Vonage Application. Link your virtual number to this application and configure its Inbound/Status URLs to point to your Next.js webhook endpoint, exposed via ngrok during development.

What is the purpose of the private.key file?

The `private.key` file is crucial for authenticating your Vonage application. It's generated when you create a new Vonage Application and must be securely stored and referenced in your `.env.local` file using the `VONAGE_PRIVATE_KEY_PATH` variable. Never commit this file to your code repository.

How to handle errors in the Vonage webhook?

Implement a try...catch block in your API route handler to catch errors during webhook processing. Log errors thoroughly and return a 500 status code if processing fails. For production, structured logging and error tracking services are recommended.

What are best practices for securing the Next.js Vonage webhook?

Secure your webhook by validating inputs, verifying Vonage signatures using JWT, implementing rate limiting, and protecting environment variables. These measures prevent malicious attacks, abuse, and ensure only genuine Vonage requests are processed.

How to troubleshoot Vonage webhook not triggering?

If your webhook isn't triggering, check if ngrok is running and the URLs in your Vonage application settings match. Ensure your Vonage number is linked, Messages API is the default setting, and no firewalls are blocking requests.

What is the role of Next.js API routes in two-way SMS?

Next.js API routes act as webhook endpoints to receive incoming SMS messages from Vonage. These serverless functions handle the incoming requests, process the message data, and send the required 200 OK response to Vonage, acknowledging receipt.

How to send SMS replies with Vonage and Next.js?

While the article focuses on receiving messages, sending replies involves using the `@vonage/server-sdk` within your API route after receiving and processing an inbound message. Further guidance on sending messages can be found in the Vonage documentation and SDK examples.