This guide provides a step-by-step walkthrough for building a Next.js application capable of sending SMS messages via the Vonage API, receiving delivery receipts (DLRs), and handling inbound SMS messages using webhooks.
We will build a simple web form to send SMS messages and set up API routes (route handlers in Next.js App Router) to act as webhook endpoints for receiving status updates and incoming messages from Vonage. This enables real-time tracking of message delivery and allows your application to react to messages sent to your Vonage number.
Technologies Used:
- Next.js (App Router): A React framework for building full-stack web applications. We use the App Router for modern routing and server-side capabilities.
- Vonage SMS API: Used for sending SMS messages programmatically.
- Vonage Node.js SDK: Simplifies interaction with the Vonage APIs from a Node.js environment.
- Vercel (or similar platform): For deploying the Next.js application and obtaining publicly accessible URLs required for Vonage webhooks.
- Zod (Optional): A library for schema declaration and validation, useful for validating form input.
Outcome:
By the end of this guide, you will have a functional Next.js application that can:
- Send SMS messages using a web form.
- Receive and log delivery status updates for sent messages.
- Receive and log inbound SMS messages sent to your Vonage number.
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 Vonage phone number with SMS capabilities via the Dashboard.
- Node.js: Version 18.17 or later installed. Download from nodejs.org.
- npm or yarn: Node.js package manager.
- Git: For version control and deployment.
- Basic understanding: Familiarity with JavaScript, React, Next.js, and terminal commands.
- Deployment Platform Account: A Vercel (vercel.com) or similar account to deploy your application.
1. Setting Up the Project
Let's initialize our Next.js project and configure the necessary environment variables and dependencies.
-
Create a New Next.js Project: Open your terminal and run the following command to create a new Next.js project using the App Router:
npx create-next-app@latest vonage-sms-nextjs
When prompted, select the following options (or adjust as needed, but this guide assumes these settings):
What is your project named? vonage-sms-nextjs 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
This creates a new directory named
vonage-sms-nextjs
with the basic project structure. -
Navigate into Project Directory:
cd vonage-sms-nextjs
-
Declare Environment Variables: Create a file named
.env.local
in the root of your project. This file will store your sensitive credentials and configuration. Never commit this file to Git.- Go to your Vonage API Dashboard. Your API Key and Secret are displayed at the top.
- Go to the Numbers > Your numbers section to find the Vonage virtual number you rented.
Add the following lines to
.env.local
, replacing the placeholder values with your actual credentials:VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_VIRTUAL_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
VONAGE_API_KEY
: Your Vonage API key.VONAGE_API_SECRET
: Your Vonage API secret.VONAGE_VIRTUAL_NUMBER
: The Vonage phone number you rented, used as the sender ID (ensure it's in the format required by Vonage, usually E.164 format without the leading+
).
-
Install Dependencies: We need the Vonage Node.js SDK to interact with the API and optionally Zod for input validation.
npm install @vonage/server-sdk zod
@vonage/server-sdk
: The official Vonage library for Node.js.zod
: Used for validating the phone number and message text before sending.
-
Configure
.gitignore
: Ensure.env.local
is included in your.gitignore
file (create-next-app usually adds it by default) to prevent accidentally committing your secrets.# ... other entries .env.local
2. Implementing SMS Sending Functionality
We'll create a server action to handle sending the SMS securely on the server and a form component for the user interface.
-
Create the Server Action (
send-sms.js
): Server Actions allow us to run server-side code directly in response to UI interactions, without needing to manually create API routes for simple form submissions.Create a directory
app/lib
and inside it, create a file namedsend-sms.js
:// app/lib/send-sms.js 'use server'; // Directive to mark this module's exports as Server Actions import { revalidatePath } from 'next/cache'; import { Vonage } from '@vonage/server-sdk'; import { z } from 'zod'; // Initialize Vonage client using environment variables const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, }); const from = process.env.VONAGE_VIRTUAL_NUMBER; // Define a schema for validation using Zod const schema = z.object({ // Basic digits check (7-15 digits) // Note: This regex is basic. For stricter validation (e.g., E.164), // consider a more specific regex or a dedicated library. number: z.string().regex(/^\d{7,15}$/, 'Invalid phone number format (digits only, 7-15 length).'), text: z.string().min(1, 'Message text cannot be empty.').max(1600, 'Message text too long.'), // Max length for concatenated SMS }); // Define the Server Action function export async function sendSMS(prevState, formData) { try { // Validate the form data against the schema const validatedData = schema.parse({ number: formData.get('number'), text: formData.get('text'), }); // Send the SMS using the Vonage SDK const vonageResponse = await vonage.sms.send({ to: validatedData.number, from: from, // Your Vonage virtual number text: validatedData.text, }); console.log('Vonage API Response:', vonageResponse); // Check the response from Vonage // Status '0' indicates success for that specific message part const messageStatus = vonageResponse.messages[0].status; let responseMessage; if (messageStatus === '0') { responseMessage = `_ Message sent successfully to ${validatedData.number}. Message ID: ${vonageResponse.messages[0]['message-id']}`; } else { responseMessage = `_ There was an error sending the SMS. Status: ${messageStatus}, Error: ${vonageResponse.messages[0]['error-text']}`; } // Revalidate the page path if needed (useful if displaying sent messages) revalidatePath('/'); // Return state to the form return { response: responseMessage, }; } catch (error) { console.error('Error sending SMS:', error); // Handle Zod validation errors specifically if (error instanceof z.ZodError) { return { response: `_ Validation Error: ${error.errors.map((e) => e.message).join(', ')}`, }; } // Handle general errors (API issues, network problems) return { response: `_ An unexpected error occurred: ${error.message}`, }; } }
Explanation:
'use server'
: Marks this module for Server Actions.- Vonage Initialization: Creates a Vonage client instance using credentials from
.env.local
. - Zod Schema: Defines validation rules for the phone number (
number
) and message content (text
). The regex comment highlights its basic nature. sendSMS
Function: This is the core server action.- It receives
prevState
(previous state from the form hook) andformData
. schema.parse
: Validates the incoming form data. Throws an error if validation fails.vonage.sms.send
: Calls the Vonage API to send the SMS.to
,from
, andtext
are required parameters.- Response Handling: Checks the
status
field in thevonageResponse
. A status of'0'
usually means the message was accepted for delivery. Other statuses indicate errors. Note that for long (concatenated) SMS messages, this only reflects the status of the first part of the message. Handling DLRs (Delivery Receipts) via webhooks is necessary for final delivery confirmation. revalidatePath('/')
: Clears the cache for the homepage, useful if you were displaying data that needs updating after the action.- Error Handling: Includes a
try...catch
block to handle validation errors (from Zod) and other potential errors during the API call.
- It receives
-
Create the Form Component (
send-form.jsx
): This client component provides the UI for entering the phone number and message. It uses React hooks (useFormState
,useFormStatus
) to manage form submission state and display responses from the server action.Create a file named
app/send-form.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 response message }; // Custom submit button component to show pending state function SubmitButton() { const { pending } = useFormStatus(); // Hook to check if the form is submitting return ( <button type=""submit"" aria-disabled={pending} disabled={pending} // Disable button while submitting className=""mt-4 px-4 py-2 bg-blue-600 text-white 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"" > {pending ? ( <> <div className=""border-gray-300 h-5 w-5 animate-spin rounded-full border-2 border-t-white mr-2""></div> Sending... </> ) : ( 'Send SMS' )} </button> ); } // The main form component export function SendForm() { // useFormState links the form action to state management 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""> Phone Number (e.g., 447700900000): </label> <input name=""number"" id=""number"" type=""tel"" // Use 'tel' for phone numbers placeholder=""Enter destination phone number"" autoComplete=""tel"" required className=""mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"" /> </div> <div> <label htmlFor=""text"" className=""block text-sm font-medium text-gray-700""> Message: </label> <textarea name=""text"" id=""text"" rows={4} placeholder=""Enter your message here"" required className=""mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"" /> </div> <SubmitButton /> {/* Display the response message from the server action */} {state?.response && ( <p aria-live=""polite"" className={`mt-2 text-sm ${state.response.startsWith('_') ? 'text-red-600' : 'text-green-600'}`}> {state.response} </p> )} </form> ); }
Explanation:
'use client'
: Marks this as a Client Component.useFormStatus
: Provides thepending
state, used inSubmitButton
to show loading feedback.useFormState
: Connects thesendSMS
server action to the component's state (state
). When the form is submitted,sendSMS
runs, and its return value updatesstate
, causing the UI to re-render and display theresponse
.- Form Inputs: Standard HTML inputs for phone number (
type=""tel""
) and message (textarea
).required
attribute ensures fields are not empty. aria-live=""polite""
: Improves accessibility by announcing the response message when it appears.
-
Integrate Form into the Page (
page.js
): Import and use theSendForm
component in your main page file.Update
app/page.js
:// app/page.js import { SendForm } from './send-form'; // Import the form component export default function Home() { return ( <main className=""mx-auto max-w-xl 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.svg"" alt=""Vonage"" className=""h-8 mb-2"" /> */} <h1 className=""text-2xl font-semibold text-gray-800""> Send SMS with Vonage & Next.js </h1> <p className=""text-sm text-gray-500""> Enter a phone number and message to send an SMS. </p> </header> <section> <SendForm /> {/* Render the form component */} </section> </main> ); }
-
Run the Development Server: Start your Next.js development server to test the sending functionality locally.
npm run dev
Open your browser to
http://localhost:3000
. You should see the form. Try sending an SMS to your own phone number. Check the form's response message and confirm if you receive the SMS. Note that webhooks (delivery receipts, inbound messages) won't work locally without extra tooling like ngrok, which is why deployment is necessary for the next steps.
3. Receiving Delivery Receipts & Inbound SMS (Webhooks)
Vonage uses webhooks to push data to your application asynchronously. We need publicly accessible endpoints (URLs) in our deployed application for Vonage to send delivery receipt status updates and inbound SMS messages.
-
Explain Webhooks: When you send an SMS, the initial API response (
status: 0
) only confirms Vonage accepted the message. The actual delivery status (delivered, failed, etc.) comes later via a webhook. Similarly, when someone sends an SMS to your Vonage number, Vonage forwards it to your application via an inbound SMS webhook. These require your application to expose specific HTTP POST endpoints. -
Create Route Handlers for Webhooks: Next.js Route Handlers allow us to create API endpoints within the
app
directory.-
Delivery Receipt Handler: Create the directory structure
app/webhook/status
and add a file namedroute.js
:// 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('Received Delivery Receipt:', JSON.stringify(data, null, 2)); // --- Business Logic Placeholder --- // TODO: Process the delivery receipt data // - Find the original message in your database using 'messageId'. // - Update the message status based on 'data.status'. // - Handle different statuses ('delivered', 'failed', 'expired', 'rejected', etc.). // - Log errors ('err-code') for failed messages. // Example: updateMessageStatus(data.messageId, data.status, data['err-code']); // --------------------------------- // Crucially, return a 200 OK response to Vonage // Failure to do so will cause Vonage to retry sending the webhook. return new Response('OK', { status: 200 }); } catch (error) { console.error('Error processing delivery receipt webhook:', error); // Return an error status, but Vonage might still retry if it's not a 2xx return new Response('Error processing webhook', { status: 500 }); } } // Optionally handle GET requests for verification or health checks export async function GET(request) { return NextResponse.json({ message: 'Status webhook endpoint is active.' }); }
-
Inbound SMS Handler: Create the directory structure
app/webhook/inbound
and add a file namedroute.js
:// app/webhook/inbound/route.js import { NextResponse } from 'next/server'; // Handles POST requests from Vonage for Inbound SMS messages export async function POST(request) { try { const data = await request.json(); console.log('Received Inbound SMS:', JSON.stringify(data, null, 2)); // --- Business Logic Placeholder --- // TODO: Process the inbound message // - Identify the sender ('msisdn'). // - Store the message content ('text'). // - Check for keywords ('keyword'). // - Potentially trigger automated replies or application logic. // Example: processInboundMessage(data.msisdn, data.text, data.messageId); // --------------------------------- // Crucially, return a 200 OK response to Vonage return new Response('OK', { status: 200 }); } catch (error) { console.error('Error processing inbound SMS webhook:', error); return new Response('Error processing webhook', { status: 500 }); } } // Optionally handle GET requests for verification or health checks export async function GET(request) { return NextResponse.json({ message: 'Inbound webhook endpoint is active.' }); }
Explanation:
POST
Function: This handles the incoming HTTP POST requests from Vonage.request.json()
: Parses the JSON payload sent by Vonage.console.log
: Logs the received data. In a production app, you'd replace this with proper logging and database operations.- Business Logic Placeholder: Comments indicate where you would add your application-specific logic (e.g., updating database records, triggering replies).
return new Response('OK', { status: 200 })
: This is critical. You must return a200 OK
(or204 No Content
) response quickly to acknowledge receipt of the webhook. If Vonage doesn't receive a 2xx response, it will assume the delivery failed and retry sending the webhook, potentially leading to duplicate processing.GET
Function (Optional): Provides a simple way to check if the endpoint is deployed and reachable via a browser or health check tool.
-
-
Deploy the Application: Webhooks require a publicly accessible URL. Deploy your application to a platform like Vercel.
-
Push to GitHub/GitLab/Bitbucket:
- Initialize a Git repository if you haven't already:
git init
- Add files:
git add .
- Commit changes:
git commit -m ""Initial commit with SMS sending and webhook handlers""
- Create a repository on your preferred Git provider (e.g., GitHub).
- Add the remote origin:
git remote add origin <your-repo-url>
- Push your code:
git push -u origin main
(or your default branch name)
- Initialize a Git repository if you haven't already:
-
Deploy on Vercel:
- Sign up or log in to Vercel.
- Click ""Add New..."" > ""Project"".
- Import the Git repository you just pushed.
- Configure Environment Variables: Before deploying, navigate to the project settings in Vercel, find the ""Environment Variables"" section, and add
VONAGE_API_KEY
,VONAGE_API_SECRET
, andVONAGE_VIRTUAL_NUMBER
with their respective values. Ensure they are available for all environments (Production, Preview, Development). - Click ""Deploy"". Vercel will build and deploy your application, providing you with public URLs (e.g.,
https://your-project-name.vercel.app
).
-
-
Configure Vonage Webhook URLs: Now, tell Vonage where to send the delivery receipts and inbound messages.
- Go to your Vonage API Dashboard Settings.
- Find the ""SMS Settings"" section (or similar).
- Delivery receipts (DLR) / Webhook URL for Status: Enter the full URL of your deployed status webhook handler. Example:
https://your-project-name.vercel.app/webhook/status
- Inbound messages / Webhook URL for Inbound: Enter the full URL of your deployed inbound webhook handler. Example:
https://your-project-name.vercel.app/webhook/inbound
- Set the HTTP Method for both to
POST
. - Save the settings.
4. Verification and Testing
With the application deployed and Vonage configured, let's verify everything works.
-
Test Sending:
- Navigate to your deployed application URL (e.g.,
https://your-project-name.vercel.app
). - Use the form to send an SMS to your personal phone number.
- Confirm you receive the SMS.
- Check the success/error message displayed on the form.
- Navigate to your deployed application URL (e.g.,
-
Test Delivery Receipt Webhook:
- After sending the SMS, wait a few seconds or minutes (delivery time varies).
- Go to your Vercel dashboard, select your project, and navigate to the ""Logs"" or ""Functions"" tab.
- Look for logs corresponding to requests made to
/webhook/status
. You should see the logged JSON payload containing the delivery status (delivered
,failed
,expired
, etc.) for the message you sent.
Example DLR Payload (Failed):
{ ""msisdn"": ""447700900001"", ""to"": ""447700900000"", ""network-code"": ""23410"", ""messageId"": ""aaaaaaaa-bbbb-cccc-dddd-0123456789ab"", ""price"": ""0.03330000"", ""status"": ""failed"", ""scts"": ""2310261030"", ""err-code"": ""6"", ""api-key"": ""YOUR_API_KEY"", ""message-timestamp"": ""2023-10-26 10:30:55"" }
-
Test Inbound SMS Webhook:
- Using your personal phone, send an SMS message to your Vonage virtual number.
- Go back to your Vercel logs.
- Look for logs corresponding to requests made to
/webhook/inbound
. You should see the logged JSON payload containing the message text, sender number (msisdn
), and other details.
Example Inbound SMS Payload:
{ ""msisdn"": ""447700900002"", ""to"": ""447700900000"", ""messageId"": ""bbbbbbbb-cccc-dddd-eeee-0123456789ac"", ""text"": ""Hello from my phone!"", ""type"": ""text"", ""keyword"": ""HELLO"", ""api-key"": ""YOUR_API_KEY"", ""message-timestamp"": ""2023-10-26 10:35:10"" }
5. Error Handling and Logging
- Server Action Errors: The
sendSMS
function includestry...catch
blocks to handle Zod validation errors and general API/network errors, returning informative messages to the UI. - Webhook Errors: The webhook route handlers include basic
try...catch
blocks. In production, implement more robust error handling:- Use a dedicated logging service (e.g., Sentry, Logtail, Datadog) instead of
console.log
. - Log detailed error information (stack trace, request body).
- Set up monitoring and alerting for webhook failures.
- Use a dedicated logging service (e.g., Sentry, Logtail, Datadog) instead of
- Vonage Errors: Pay attention to the
status
anderr-code
fields in delivery receipts to understand why messages might fail. Consult the Vonage SMS API error codes documentation.
6. Security Considerations
- API Credentials: Never hardcode API keys or secrets in your frontend or backend code. Use environment variables (
.env.local
locally, platform settings in deployment) and ensure.env.local
is in.gitignore
. - Input Validation: The Zod schema in
send-sms.js
provides basic validation. Sanitize or validate all user input thoroughly to prevent injection attacks or unexpected behavior. - Webhook Security: While basic Vonage SMS webhooks don't use signed signatures like some other Vonage APIs (e.g., Video), consider these measures in production:
- Use HTTPS for your webhook URLs (Vercel provides this automatically).
- Potentially implement a secret token system: Generate a unique secret, append it as a query parameter to the webhook URL in Vonage settings (
/webhook/status?token=YOUR_SECRET
), and verify this token in your handler. This adds a layer of assurance that the request originated from Vonage (or at least someone who knows the token). - Rate Limiting: Protect your webhook endpoints from abuse by implementing rate limiting.
7. Troubleshooting and Caveats
- Environment Variables Not Loaded: Ensure
.env.local
is correctly formatted and placed in the project root. On deployment platforms like Vercel, ensure environment variables are configured in the project settings. Restart your local dev server after changing.env.local
. - Webhooks Not Receiving Data:
- Check URLs: Double-check the webhook URLs configured in the Vonage dashboard match your deployed application's URLs exactly (including
https://
and the correct path/webhook/status
or/webhook/inbound
). - Deployment Status: Ensure your application is successfully deployed and running. Check Vercel deployment logs for build or runtime errors.
- Return 200 OK: Verify your webhook handlers always return a
200 OK
response quickly. Check Vercel function logs for errors within your handlers. If your logic takes too long, Vonage might time out and retry. Consider processing webhook data asynchronously (e.g., push to a queue). - Firewall Issues: If self-hosting, ensure your server's firewall allows incoming POST requests from Vonage's IP ranges.
- Check URLs: Double-check the webhook URLs configured in the Vonage dashboard match your deployed application's URLs exactly (including
- Incorrect Vonage Number Format: Ensure the
to
number insendSMS
and yourVONAGE_VIRTUAL_NUMBER
are in the correct format (usually E.164 without leading+
). - Vonage Number Capabilities: Confirm your rented Vonage number is SMS-enabled for the region you're sending to/receiving from.
- Country-Specific Restrictions: Sending SMS to certain countries may require pre-registration or adhere to specific regulations (e.g., sender ID rules). Check Vonage Country-Specific Features and Restrictions.
- Zod Validation Errors: Check the specific error message returned by Zod (logged or sent to the UI) to see which validation rule failed (e.g., invalid phone format, empty text).
8. Deployment and CI/CD (Vercel Example)
Vercel provides seamless CI/CD via Git integration.
- Connect Git Repository: Done during the initial deployment setup.
- Environment Variables: Already configured in Vercel project settings. Ensure separate variables can be set for Production, Preview, and Development branches if needed.
- Automatic Deployments: By default, Vercel automatically deploys:
- New commits to the production branch (e.g.,
main
) to your production URL. - New commits to other branches or pull requests to unique preview URLs.
- New commits to the production branch (e.g.,
- Rollbacks: Vercel's dashboard allows you to instantly roll back to previous deployments if an issue occurs. Navigate to the ""Deployments"" tab in your project, find a previous successful deployment, and promote it to production.
9. Complete Code Repository
A complete, working example of this project can be found on GitHub:
https://github.com/vonage-community/vonage-nextjs-sms-guide
Conclusion
You have successfully built a Next.js application that leverages the Vonage SMS API to send messages and uses webhooks to receive delivery receipts and handle inbound SMS. You learned how to set up the project, implement server actions for sending, create route handlers for webhooks, deploy the application, and configure Vonage settings.
This forms a solid foundation for building more complex SMS-based features, such as two-factor authentication, appointment reminders, customer support chat via SMS, or automated responses to inbound messages. Remember to implement robust error handling, logging, and security measures for production applications.