code examples
code examples
Send and Receive SMS with Delivery Receipts using Next.js and Vonage
A guide on building a Next.js app to send SMS via Vonage, handle delivery receipts, and receive inbound messages using webhooks, Server Actions, and Route Handlers.
This guide provides a step-by-step walkthrough for building a Next.js application capable of sending SMS messages via the Vonage SMS API and handling incoming messages and delivery receipts (DLRs) through webhooks. We will cover project setup, core implementation, deployment, and essential production considerations like error handling and security.
By the end of this guide, you will have a functional Next.js application deployed on Vercel that can:
- Send SMS messages programmatically using a simple web form.
- Receive delivery status updates (delivery receipts) for sent messages.
- Receive incoming SMS messages sent to your Vonage virtual number.
This implementation leverages the power of Next.js App Router features like Server Actions for form handling and Route Handlers for creating webhook endpoints, alongside the robust Vonage SMS API.
Project Overview and Goals
Problem: Businesses often need to integrate SMS functionality into their applications for notifications, alerts, marketing campaigns, or two-way communication. Implementing this requires sending messages reliably and understanding their delivery status, as well as processing inbound messages from users.
Solution: We will build a Next.js application that uses the Vonage SMS API to send messages. We'll implement webhook endpoints using Next.js Route Handlers to receive delivery receipts (DLRs) and inbound SMS messages from the Vonage platform, providing a complete SMS communication loop.
Technologies:
- Next.js: A React framework for building full-stack web applications. We'll use the App Router, Server Actions, and Route Handlers.
- Vonage SMS API: A RESTful API for sending and receiving SMS messages globally.
- Vonage Node.js SDK: Simplifies interaction with the Vonage APIs within a Node.js environment.
- Vercel: A platform for deploying frontend applications and serverless functions, ideal for hosting our Next.js app and its API routes (webhooks).
- (Optional) Zod: A schema declaration and validation library for validating form input.
System Architecture:
graph LR
A[User Browser] -- 1. Submits Form --> B(Next.js App / Vercel);
B -- 2. Calls Send API --> C(Vonage SMS API);
C -- 3. Sends SMS --> D[User Phone];
C -- 4. Posts DLR --> E[Next.js Webhook /webhook/status / Vercel];
D -- 5. Sends Reply --> C;
C -- 6. Posts Inbound SMS --> F[Next.js Webhook /webhook/inbound / Vercel];
E -- Logs DLR --> G[Vercel Logs / Database];
F -- Logs Inbound SMS --> G;
style B fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#ccf,stroke:#333,stroke-width:2px
style F fill:#ccf,stroke:#333,stroke-width:2px
style C fill:#ffa,stroke:#333,stroke-width:2pxDiagram Explanation:
- The user interacts with the Next.js frontend hosted on Vercel.
- Submitting the send SMS form triggers a Server Action that uses the Vonage SDK to call the Vonage API.
- Vonage delivers the SMS to the recipient's phone.
- Vonage sends a Delivery Receipt (DLR) back to a specific webhook endpoint in our Next.js app.
- The user can reply to the SMS.
- Vonage forwards the inbound SMS to another webhook endpoint in our Next.js app.
- Both webhooks log the received data (e.g., to Vercel logs or a database).
Prerequisites:
- Vonage API Account: Sign up at Vonage.com. You'll need your API Key and API Secret from the API Dashboard.
- Vonage Virtual Number: Rent a virtual phone number with SMS capabilities from the Numbers section of the dashboard.
- Node.js: Version 18.17 or later installed. (Download Node.js)
- npm or yarn: Package manager for Node.js.
- Git: For version control and deployment via GitHub. (Install Git)
- GitHub Account: Required for deploying via Vercel's Git integration. (Sign up for GitHub)
- Vercel Account: For deployment. Sign up with your GitHub account. (Sign up for Vercel)
1. Setting up the Project
Let's initialize a new Next.js project and install the necessary dependencies.
-
Create Next.js App: Open your terminal and run the following command:
bashnpx create-next-app@latest -
Configure Project: You'll be prompted for project settings. Use these selections for this guide:
textWhat is your project named? vonage-nextjs-sms Would you like to use TypeScript? No Would you like to use ESLint? Yes Would you like to use Tailwind CSS? Yes Would you like to use `src/` directory? No Would you like to use App Router? (recommended) Yes Would you like to customize the default import alias (@/*)? No -
Navigate to Project Directory:
bashcd vonage-nextjs-sms -
Install Dependencies: Install the Vonage Node.js SDK and optionally Zod for validation.
bashnpm install @vonage/server-sdk npm install zod # Optional, but recommended for validation -
Set Up Environment Variables: Create a file named
.env.localin the root of your project. This file stores sensitive credentials and should not be committed to Git.- Find your API Key and API Secret on the main page of your Vonage API Dashboard.
- Find your Vonage Virtual Number under Numbers > Your numbers. Make sure it has SMS capability. Enter the number in E.164 format (country code + number, no leading
+, spaces, or dashes, e.g.,14155550100).
Add the following lines to
.env.local, replacing the placeholder values:text# .env.local VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_VIRTUAL_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBERVONAGE_API_KEY: Your Vonage account API key.VONAGE_API_SECRET: Your Vonage account API secret.VONAGE_VIRTUAL_NUMBER: The Vonage number you rented (in E.164 format), used as the 'from' number for sending SMS.
-
Update .gitignore: Ensure
.env.localis listed in your.gitignorefile (create-next-app usually adds it by default) to prevent accidentally committing secrets.text# .gitignore (ensure this line exists) .env*.local
Project Structure Explanation:
/app: Contains the core application code using the Next.js App Router./app/page.js: The main page component for our UI./app/layout.js: The root layout for the application./app/lib: A conventional place for utility functions and server-side logic (like our Vonage interaction)./app/webhook: Will contain our API Route Handlers for receiving data from Vonage.
/public: Static assets (images, etc.)..env.local: Environment variables for local development.package.json: Project dependencies and scripts.next.config.js: Next.js configuration.
This structure separates UI components, server-side logic, and API routes cleanly, aligning with Next.js conventions.
2. Implementing Core Functionality (Sending SMS)
We'll create a form that uses a Next.js Server Action to send an SMS message via the Vonage API.
-
Create the Server Action (
app/lib/send-sms.js): This file contains the server-side logic to interact with the Vonage SDK. Create alibdirectory insideappif it doesn't exist.javascript// app/lib/send-sms.js 'use server'; // Marks this module's exports as Server Actions import { Vonage } from '@vonage/server-sdk'; import { z } from 'zod'; // Import Zod if using validation import { revalidatePath } from 'next/cache'; // Initialize Vonage client using environment variables const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, }); const fromNumber = process.env.VONAGE_VIRTUAL_NUMBER; // Define a schema for validation using Zod const smsSchema = z.object({ // Basic E.164 format check (country code + number, no '+', spaces, or dashes) // Allows numbers like 447700900000 or 14155550100 number: z .string() .regex(/^[1-9]\d{6,14}$/, { message: 'Invalid phone number format (must be E.164).' }), text: z .string() .min(1, { message: 'Message cannot be empty.' }) .max(1600, { message: 'Message too long.' }), // Max length for concatenated SMS }); // The Server Action function export async function sendSMS(prevState, formData) { let isError = false; try { // Validate form data using Zod const validatedData = smsSchema.parse({ number: formData.get('number'), text: formData.get('text'), }); console.log(`Sending SMS from ${fromNumber} to ${validatedData.number}`); // Use the Vonage SDK to send the SMS const vonageResponse = await vonage.sms.send({ to: validatedData.number, from: fromNumber, text: validatedData.text, }); // Invalidate the cache for the homepage to potentially show updates revalidatePath('/'); // Check the response from Vonage if (vonageResponse.messages[0].status === '0') { console.log('Message sent successfully:', vonageResponse.messages[0]); return { response: `Message sent successfully to ${validatedData.number}. Message ID: ${vonageResponse.messages[0]['message-id']}`, isError: false, }; } else { console.error('Error sending SMS:', vonageResponse.messages[0]['error-text']); isError = true; return { response: `Error sending SMS: ${vonageResponse.messages[0]['error-text']}`, isError: true, }; } } catch (error) { isError = true; // Handle Zod validation errors or other exceptions if (error instanceof z.ZodError) { console.error('Validation Error:', error.flatten().fieldErrors); // Return a user-friendly error message combining field errors const errorMessages = Object.entries(error.flatten().fieldErrors) .map(([field, messages]) => `${field}: ${messages.join(', ')}`) .join('; '); return { response: `Validation failed: ${errorMessages}`, isError: true }; } // Handle Vonage SDK or other network errors console.error('Failed to send SMS:', error); return { response: `Failed to send SMS. Error: ${error.message}`, isError: true, }; } }'use server': Directive required for Server Actions.- Vonage Initialization: Creates a Vonage client instance using credentials from
.env.local. - Zod Schema: Defines rules for the recipient
number(E.164 format) and messagetext. sendSMSFunction:- Takes
prevState(fromuseFormState) andformData. - Uses
smsSchema.parseto validate input. Throws an error if validation fails. - Calls
vonage.sms.sendwith the validated data and thefromnumber. revalidatePath('/'): Tells Next.js to clear the cache for the root path, useful if the page displays sent message status (though not implemented here).- Checks the
statusfield in the Vonage response ('0'means success). - Returns an object with a
responsemessage and anisErrorflag indicating success or failure. - Includes
try...catchto handle validation errors (ZodError) and API/network errors gracefully.
- Takes
-
Create the Form Component (
app/send-form.jsx): This client component renders the form and uses React hooks to manage state and submission.jsx// app/send-form.jsx 'use client'; // This component runs on the client import { useFormState, useFormStatus } from 'react-dom'; import { sendSMS } from '@/app/lib/send-sms'; // Import the Server Action const initialState = { response: null, // Initial state for the form response message isError: false, }; // Separate component for the submit button to use useFormStatus function SubmitButton() { const { pending } = useFormStatus(); // Hook to check form submission status return ( <button type=""submit"" aria-disabled={pending} className=""bg-blue-600 text-white font-semibold py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center"" disabled={pending} > {pending ? ( <> <svg className=""animate-spin -ml-1 mr-3 h-5 w-5 text-white"" xmlns=""http://www.w3.org/2000/svg"" fill=""none"" viewBox=""0 0 24 24""> <circle className=""opacity-25"" cx=""12"" cy=""12"" r=""10"" stroke=""currentColor"" strokeWidth=""4""></circle> <path className=""opacity-75"" fill=""currentColor"" d=""M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z""></path> </svg> Sending... </> ) : ( 'Send SMS' )} </button> ); } // The main form component export function SendForm() { // useFormState manages form state updates based on the Server Action result const [state, formAction] = useFormState(sendSMS, initialState); return ( <form action={formAction} className=""flex flex-col gap-y-4""> <div> <label htmlFor=""number"" className=""block text-sm font-medium text-gray-700 mb-1""> Phone Number (E.164 format, e.g., 14155550100): </label> <input name=""number"" id=""number"" type=""tel"" // Use type=""tel"" for phone numbers placeholder=""Enter recipient number (e.g. 14155550100)"" autoComplete=""tel"" className=""border border-gray-300 rounded-md p-2 w-full focus:ring-blue-500 focus:border-blue-500"" required /> </div> <div> <label htmlFor=""text"" className=""block text-sm font-medium text-gray-700 mb-1""> Message: </label> <textarea name=""text"" id=""text"" rows={4} placeholder=""Enter your message here..."" className=""border border-gray-300 rounded-md p-2 w-full focus:ring-blue-500 focus:border-blue-500"" required ></textarea> </div> <SubmitButton /> {/* Display the response message from the Server Action */} {state?.response && ( <p aria-live=""polite"" className={`mt-2 text-sm ${state.isError ? 'text-red-600' : 'text-green-600'}`}> {state.response} </p> )} </form> ); }'use client': Required for components using React hooks likeuseFormStateanduseFormStatus.useFormState: Manages form state. It takes the Server Action (sendSMS) and an initial state. It returns the current state (state) and a dispatch function (formAction) to be passed to the<form>'sactionprop.useFormStatus: Used in theSubmitButtonto get thependingstatus of the form submission, enabling UI feedback (disabling button, showing spinner). It must be used in a component rendered inside the<form>.- Form Structure: Standard HTML form elements.
nameattributes must match the keys expected by the Server Action (formData.get('name')).requiredprovides basic client-side validation. E.164 format explicitly mentioned. - Response Display: Uses the
isErrorflag from the state to conditionally apply CSS classes for success/error styling.
-
Integrate Form into Page (
app/page.js): Import and render theSendFormcomponent on the main page.javascript// app/page.js import { SendForm } from './send-form'; // Import the form component export default function Home() { return ( <main className=""mx-auto max-w-2xl my-8 p-6 border border-gray-200 shadow-md rounded-lg""> <header className=""mb-6 pb-4 border-b border-gray-200""> {/* You can add a Vonage logo here if desired */} {/* <img src=""/vonage-logo.svg"" alt=""Vonage Logo"" className=""h-8 mb-2"" /> */} <h1 className=""text-2xl font-semibold text-gray-800""> Send SMS with Next.js & Vonage </h1> </header> <section> <SendForm /> </section> </main> ); }(Note: Add a
vonage-logo.svgto the/publicdirectory if you want to display it). -
Run Development Server:
bashnpm run devOpen your browser to
http://localhost:3000. You should see the form. Try sending an SMS to your own phone number (use the E.164 format, e.g.,14155550100). Check the browser for success/error messages and your terminal console for logs from the Server Action.
3. Building API Layer (Receiving Webhooks)
Vonage uses webhooks to send data to your application. We need endpoints to receive Delivery Receipts (DLRs) and inbound SMS messages. We'll use Next.js Route Handlers for this.
-
Create Webhook Directory Structure:
bashmkdir -p app/webhook/status mkdir -p app/webhook/inbound -
Create Delivery Receipt Handler (
app/webhook/status/route.js): This endpoint will receive status updates for messages you've sent.javascript// app/webhook/status/route.js import { NextResponse } from 'next/server'; // Handles POST requests from Vonage for Delivery Receipts (DLRs) export async function POST(request) { try { const data = await request.json(); console.log('--- Delivery Receipt Received ---'); console.log(JSON.stringify(data, null, 2)); console.log(`Message ID: ${data.messageId}, Status: ${data.status}`); // TODO: Add logic here to store the status in a database // e.g., updateMessageStatus(data.messageId, data.status); // Vonage expects a 200 OK response to acknowledge receipt return new NextResponse('OK', { status: 200 }); } catch (error) { console.error('Error processing delivery receipt:', error); // Return an error response, but Vonage might retry if it's not a 2xx return new NextResponse('Error processing webhook', { status: 500 }); } } // Handles GET requests (optional, useful for simple health checks) export async function GET(request) { console.log('Received GET request on /webhook/status'); return new NextResponse('Status webhook endpoint is active.', { status: 200 }); } -
Create Inbound SMS Handler (
app/webhook/inbound/route.js): This endpoint receives messages sent to your Vonage number.javascript// app/webhook/inbound/route.js import { NextResponse } from 'next/server'; // Handles POST requests from Vonage for Inbound SMS export async function POST(request) { try { const data = await request.json(); console.log('--- Inbound SMS Received ---'); console.log(JSON.stringify(data, null, 2)); console.log(`From: ${data.msisdn}, To: ${data.to}, Text: ${data.text}`); // TODO: Add logic here to process the inbound message // e.g., routeToSupport(data.msisdn, data.text); // Vonage expects a 200 OK response return new NextResponse('OK', { status: 200 }); } catch (error) { console.error('Error processing inbound SMS:', error); return new NextResponse('Error processing webhook', { status: 500 }); } } // Handles GET requests (optional) export async function GET(request) { console.log('Received GET request on /webhook/inbound'); return new NextResponse('Inbound SMS webhook endpoint is active.', { status: 200 }); }
- Route Handlers: These files export functions named after HTTP methods (
POST,GET). Next.js automatically routes requests to/webhook/statusand/webhook/inboundto these handlers. request.json(): Parses the incoming JSON payload from Vonage.- Logging: We log the received data. In a real application, you'd process this data (e.g., update a database, trigger other actions).
NextResponse('OK', { status: 200 }): Crucially, you must return a200 OK(or204 No Content) response quickly. If Vonage doesn't receive this, it will assume the webhook failed and retry sending the data, potentially leading to duplicate processing.- Error Handling: Basic
try...catchlogs errors. Returning a500might cause Vonage to retry, which could be desirable or not depending on the error type. - GET Handlers: Added for basic testing to confirm the endpoints are reachable.
These endpoints currently just log the data. Authentication/validation are discussed in Section 7 (Security).
4. Integrating with Vonage (Webhook Configuration)
Now that the endpoints exist, you need to tell Vonage where to send the data. This requires deploying the application first, as Vonage needs publicly accessible URLs. We'll cover deployment conceptually now, assuming you will deploy later.
- Obtain Deployment URL: After deploying to Vercel (or another hosting provider), you will get a public URL (e.g.,
https://your-app-name.vercel.app). - Configure Vonage Dashboard:
- Navigate to your Vonage API Dashboard Settings.
- Scroll down to the ""SMS settings"" section.
- Delivery receipts (DLRs) / Webhook URL for DLR: Enter your deployed application URL appended with the status webhook path:
https://your-app-name.vercel.app/webhook/status - Inbound messages / Webhook URL for Inbound Message: Enter your deployed application URL appended with the inbound webhook path:
https://your-app-name.vercel.app/webhook/inbound - Set the HTTP Method for both to
POST. - Click Save changes.
Environment Variable Recap:
VONAGE_API_KEY: (Required for sending) Found in Vonage Dashboard. Used by the SDK.VONAGE_API_SECRET: (Required for sending) Found in Vonage Dashboard. Used by the SDK.VONAGE_VIRTUAL_NUMBER: (Required for sending) Found in Vonage Dashboard > Numbers. Used as thefromnumber.
These variables need to be set both in .env.local for local development and in your Vercel project settings (or equivalent) for the deployed application.
5. Implementing Error Handling and Logging
We've added basic try...catch blocks and console.log statements. Let's refine this.
Error Handling Strategy:
- Sending SMS (
app/lib/send-sms.js):- Validation Errors: Zod handles input validation before calling the API. Errors are caught, logged, and returned to the UI via the
responseandisErrorstate. - API Errors: The Vonage SDK might throw errors (network issues, invalid credentials). These are caught, logged, and a generic message returned. The Vonage response can also indicate errors (
status !== '0'). These specificerror-textmessages from Vonage are logged and returned to the UI.
- Validation Errors: Zod handles input validation before calling the API. Errors are caught, logged, and returned to the UI via the
- Receiving Webhooks (
app/webhook/.../route.js):- Parsing Errors: If
request.json()fails (invalid JSON), thecatchblock handles it. - Processing Errors: Any errors during your custom logic (e.g., database updates) should be caught.
- Crucial: Always aim to return a
200 OKto Vonage unless the request itself is malformed in a way that prevents any processing. Log internal errors thoroughly for debugging but acknowledge receipt to Vonage.
- Parsing Errors: If
Logging:
- Development:
console.logandconsole.errorare sufficient. Output appears in the terminal runningnpm run dev. - Production (Vercel):
console.log/errorstatements in Server Actions and Route Handlers are automatically captured by Vercel Logs. You can view these in your Vercel project dashboard under the ""Logs"" tab. - Structured Logging: For better analysis, consider using a library like
pinoto output logs in JSON format, including timestamps, request IDs, severity levels, etc. This makes filtering and querying logs much easier, especially with log management platforms.
Retry Mechanisms:
- Sending: If the initial
vonage.sms.sendcall fails due to a temporary network issue or a Vonage server error (e.g., 5xx status code from the API), you could implement a retry mechanism with exponential backoff (e.g., using a library likeasync-retry). However, be cautious about retrying errors related to invalid input or credentials. For this guide, we rely on surfacing the error to the user. - Receiving Webhooks: Vonage automatically retries sending webhooks if it doesn't receive a
2xxresponse within a certain timeout (usually a few seconds). It will typically retry several times over a period (e.g., 24 hours) with increasing intervals. Your webhook endpoint must be idempotent – processing the same webhook data multiple times should not cause unintended side effects (e.g., don't increment a counter twice for the same delivery receipt).
6. Creating a Database Schema (Optional Extension)
Storing message details, statuses, and inbound messages is often necessary. This section outlines a conceptual approach using Prisma (a popular ORM). Integrating a database is beyond the scope of the core Vonage tutorial but is a common next step.
- Install Prisma:
bash
npm install prisma --save-dev npm install @prisma/client npx prisma init --datasource-provider postgresql # Or your preferred DB - Define Schema (
prisma/schema.prisma):prisma// prisma/schema.prisma datasource db { provider = ""postgresql"" // Or ""mysql"", ""sqlite"", etc. url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" } model MessageLog { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt vonageMessageId String? @unique // From send response & DLR direction Direction // INBOUND or OUTBOUND fromNumber String toNumber String text String? status String? // e.g., ""submitted"", ""delivered"", ""failed"" errorCode String? // From DLR price String? // From DLR networkCode String? // From DLR rawWebhookData Json? // Store the full webhook payload } enum Direction { INBOUND OUTBOUND } - Set
DATABASE_URL: Add your database connection string to.env.local(and Vercel environment variables). - Apply Schema:
bash
npx prisma db push - Integrate with Code:
- Instantiate Prisma Client (e.g., in
lib/prisma.js). - In
sendSMS: After successful sending, create aMessageLogrecord withdirection: 'OUTBOUND',vonageMessageId, numbers, text, andstatus: 'submitted'. - In
app/webhook/status/route.js: Find theMessageLogbyvonageMessageIdand update itsstatus,errorCode,price, etc., based on the DLR data. - In
app/webhook/inbound/route.js: Create aMessageLogrecord withdirection: 'INBOUND', numbers, text, and store the raw data.
- Instantiate Prisma Client (e.g., in
This provides persistent storage for analysis and tracking. Remember to handle potential database errors within your try...catch blocks.
7. Adding Security Features
Protecting your application and user data is critical.
- Input Validation & Sanitization:
- We used Zod in
sendSMSfor validating the phone number format and message length. This prevents malformed requests from even reaching the Vonage API. - Next.js and React generally help prevent Cross-Site Scripting (XSS) by default when rendering data, but be cautious if using
dangerouslySetInnerHTML. - Sanitize any data before storing it in a database if it might be displayed later, although Prisma helps prevent SQL injection.
- We used Zod in
- Webhook Security (Signature Verification):
- Problem: Anyone could potentially send fake data to your webhook endpoints.
- Solution: Vonage can sign its webhook requests. You can verify this signature using your Vonage Signature Secret.
- Implementation:
- Retrieve your Signature Secret from the Vonage API Dashboard Settings (it's different from your API Secret). Store it securely as an environment variable (e.g.,
VONAGE_SIGNATURE_SECRET). - Install the necessary Vonage SDK components or JWT libraries if not already present.
- In your webhook handlers (
POSTfunctions), before processing the main logic, you need to:- Get the raw request body. Accessing the raw body alongside
request.json()can be tricky in Next.js depending on the runtime (Node.js vs. Edge) and might require specific handling or middleware.await request.text()is one way, but ensure it doesn't conflict with later parsing if not handled carefully. - Get the
Authorizationheader (which contains the JWT, typicallyBearer <token>). - Use the appropriate Vonage SDK method (like
verifySignature, check the SDK documentation for the exact method signature and required parameters) or standard JWT verification logic with the JWT, the raw body (or potentially a parsed version depending on the SDK function), and your Signature Secret. - If verification fails, return a
401 Unauthorizedstatus immediately and do not process the request. Only parse and process the body after successful verification.
- Get the raw request body. Accessing the raw body alongside
javascript// Conceptual - Add near the start of webhook POST handlers // Import the necessary function from the Vonage SDK or JWT library // e.g., import { verifySignature } from '@vonage/server-sdk/...'; // Verify correct import path const signatureSecret = process.env.VONAGE_SIGNATURE_SECRET; const authHeader = request.headers.get('Authorization'); const jwt = authHeader?.split(' ')[1]; // --- Verification Logic --- let data; try { // 1. Get the raw body (handle potential Next.js runtime issues) const rawBody = await request.text(); // Or request.arrayBuffer() etc. // 2. Verify the signature (check SDK docs for exact usage) // The SDK function might take the raw body string/buffer or a pre-parsed object. // Ensure the parameters match the SDK's requirements. // const isValid = verifySignature(jwt, rawBody /* or JSON.parse(rawBody) */, signatureSecret); const isValid = true; // Placeholder: Replace with actual verification call if (!isValid) { console.warn('Webhook signature verification failed!'); return new NextResponse('Invalid signature', { status: 401 }); } // 3. Only parse the body *after* successful verification // If verifySignature needed the raw body, parse it now. // If it needed the parsed body, you might have parsed it earlier. data = JSON.parse(rawBody); } catch (error) { console.error('Error during webhook signature verification or body parsing:', error); // Use 400 for parsing errors, potentially 500 for unexpected verification errors return new NextResponse('Webhook processing error', { status: 400 }); } // --- Proceed with verified data --- console.log('--- Webhook Received (Signature Verified) ---'); console.log(JSON.stringify(data, null, 2)); // ... rest of your processing logic using 'data' ... return new NextResponse('OK', { status: 200 }); - Retrieve your Signature Secret from the Vonage API Dashboard Settings (it's different from your API Secret). Store it securely as an environment variable (e.g.,
- Refer to the Vonage documentation on Signed Webhooks for detailed implementation specifics for your SDK version. This is highly recommended for production.
- Rate Limiting:
- Prevent abuse of your SMS sending form or webhooks.
- Implement rate limiting based on IP address or user session for the sending form. Libraries like
rate-limiter-flexibleor Vercel Edge Middleware can help. - While less common for incoming webhooks (as they originate from Vonage), ensure your processing logic can handle bursts of traffic.
- Environment Variable Security:
- Never commit
.env.localor files containing secrets to Git. Use.gitignore. - Store production secrets securely in your hosting provider's environment variable settings (e.g., Vercel Environment Variables).
- Never commit
- Dependency Management: Keep dependencies up-to-date (
npm updateoryarn upgrade) to patch security vulnerabilities. Use tools likenpm auditoryarn audit.
Frequently Asked Questions
How to send SMS messages with Next.js?
You can send SMS messages with Next.js by creating a Server Action that uses the Vonage SMS API. This action handles form submissions containing the recipient's number and the message content. The Vonage Node.js SDK simplifies the integration with the API within your Next.js environment.
What is a delivery receipt (DLR) in Vonage?
A Delivery Receipt (DLR) is a notification sent by Vonage to your application confirming the delivery status of an SMS message. It provides information about whether the message was successfully delivered, failed, or is still in transit. This allows you to track message delivery and take appropriate action if necessary.
Why does Vonage use webhooks?
Vonage uses webhooks to asynchronously send data to your application, such as delivery receipts (DLRs) or inbound SMS messages. This eliminates the need for your application to constantly poll Vonage for updates, making the communication process more efficient and real-time.
How to handle inbound SMS messages in Next.js?
You can handle inbound SMS messages in Next.js by creating a Route Handler for a specific webhook endpoint (e.g., `/webhook/inbound`). Vonage will send incoming messages to this endpoint, which you can then process within your Next.js application, such as logging the message or routing it to support.
How to set up Vonage webhooks with Next.js?
After deploying your Next.js application, go to the Vonage API Dashboard Settings. In the "SMS settings" section, enter your deployed application's URL along with the paths to your webhook endpoints (e.g., `/webhook/status` for DLRs and `/webhook/inbound` for inbound messages). Set the HTTP Method to POST and save the changes.
When should I use Zod with Next.js and Vonage?
Zod, a schema declaration and validation library, is beneficial for validating form input on your sending form within a Server Action. This ensures that the data sent to the Vonage SMS API is in the correct format, specifically the E.164 number format for the recipient and an appropriate message length, preventing errors before they reach the API.
What environment variables are needed for Vonage SMS API?
You'll need `VONAGE_API_KEY`, `VONAGE_API_SECRET`, and `VONAGE_VIRTUAL_NUMBER`. The API Key and Secret are found in your Vonage Dashboard and are used to authenticate with the Vonage API. The Virtual Number, also found in your Dashboard, is the 'from' number for your SMS messages.
What is the Next.js App Router?
The Next.js App Router is a new routing and data fetching system in Next.js. This example leverages App Router features like Server Actions for form handling and Route Handlers for creating API routes for webhooks, improving the structure and organization of your Next.js project.
How to receive Vonage delivery receipts in Next.js?
Create a Route Handler at `/webhook/status` in your Next.js app. Once your app is deployed and this webhook URL is configured in the Vonage Dashboard, Vonage will send POST requests to this endpoint with delivery status updates for each message you send.
What is the purpose of the Vonage Node.js SDK?
The Vonage Node.js SDK simplifies the process of interacting with the Vonage APIs from within your Node.js environment, which is the server-side environment of your Next.js application. It provides convenient functions for sending SMS messages and other Vonage services.
Can I test Vonage webhooks locally?
While you can develop webhook handling logic locally, Vonage webhooks require a publicly accessible URL. Services like ngrok or localtunnel can temporarily expose your local development server to the internet for testing webhook interactions during development, before formally deploying to Vercel.
How to deploy the Next.js Vonage SMS app to Vercel?
After completing development, you can deploy the Next.js application to Vercel by connecting your GitHub repository to your Vercel account. Vercel automatically handles the deployment and provides you with a public URL that you can then use to configure your Vonage webhook endpoints.
How can I secure my Vonage webhooks in production?
Vonage provides the ability to sign webhook requests, allowing you to verify their authenticity. This is crucial for security and prevents malicious actors from sending fake data to your webhook endpoints. Use the Vonage Signature Secret with a verification method provided by the SDK or JWT library to achieve this.
What is the recommended Node.js version for Next.js and Vonage?
The recommended Node.js version is 18.17 or later. Ensure you have this version installed before initializing and developing your Next.js project that integrates with the Vonage SMS API. Node.js is the underlying JavaScript runtime for Next.js server-side functions.