This guide provides a step-by-step walkthrough for integrating Vonage's Messages API to send and receive WhatsApp messages within a Next.js application. We'll build a simple Next.js app with API routes that handle incoming WhatsApp messages via Vonage webhooks and send replies back using the Vonage Node SDK.
This setup enables you to build applications that interact directly with users on WhatsApp, opening possibilities for customer support bots, notification systems, interactive services, and more, leveraging the familiar Next.js development environment.
Project Goals:
- Set up a Next.js project configured for Vonage WhatsApp integration.
- Create API routes to handle incoming messages and delivery status updates from Vonage.
- Use the Vonage Node SDK to send WhatsApp messages in response to user input.
- Securely manage Vonage API credentials and webhook signatures.
- Deploy the application and configure production webhooks.
Technology Stack:
- Next.js: React framework for building the frontend and API routes (using Pages Router structure for examples).
- Node.js: Runtime environment for Next.js API routes and the Vonage SDK.
- Vonage Messages API: Service used to send and receive WhatsApp messages.
- Vonage Node SDK: Simplifies interaction with the Vonage APIs.
- WhatsApp Sandbox: Vonage's testing environment for WhatsApp messaging.
- ngrok: Tool to expose local development server to the internet for webhook testing.
System Architecture:
+-------------+ +----------+ +---------------+ +-------------------------+ +-----------------+ +----------+ +-------------+
| User | --> | WhatsApp | --> | Vonage | --> | ngrok (Dev) / | --> | Next.js API Route | --> | Vonage | --> | WhatsApp | --> | User |
| (WhatsApp) | | Network | | Messages API | | Vercel URL (Prod) | | (/api/vonage/inbound) | | Messages API| | Network | | (WhatsApp) |
+-------------+ +----------+ +---------------+ +-------------------------+ +-----------------+ +----------+ +-------------+
^ | ^
|---------------------------------------------------------------|-----------------------------------|
Reply Message Sent
+---------------+ +-------------------------+ +-----------------------+
| Vonage | --> | ngrok (Dev) / | --> | Next.js API Route |
| Messages API | | Vercel URL (Prod) | | (/api/vonage/status) | (Logs status)
+---------------+ +-------------------------+ +-----------------------+
(Status Update)
Prerequisites:
- Node.js: Version 20 or higher installed.
- npm or yarn: Package manager for Node.js.
- Vonage API Account: Sign up for free credit. You'll need your API Key and Secret.
- ngrok: Installed and authenticated for testing webhooks locally.
- WhatsApp Account: A personal WhatsApp account on a smartphone for testing.
1. Setting Up the Next.js Project
Let's start by creating a new Next.js application and installing the necessary dependencies.
-
Create a Next.js App: Open your terminal and run:
npx create-next-app@latest vonage-whatsapp-nextjs cd vonage-whatsapp-nextjs
Follow the prompts. This guide uses the
pages/api
directory structure for API routes. -
Install Dependencies: Install the Vonage SDKs and
dotenv
for managing environment variables locally.npm install @vonage/server-sdk @vonage/messages @vonage/jwt dotenv
@vonage/server-sdk
: Core SDK for authentication and client initialization.@vonage/messages
: Specifically for using the Messages API.@vonage/jwt
: For verifying webhook signatures (JWTs).dotenv
: To load environment variables from a.env.local
file during local development (Next.js has built-in support, but this ensures consistency).
-
Configure Environment Variables: Create a file named
.env.local
in the root of your project. This file is gitignored by default in Next.js projects, keeping your secrets safe. Add the following variables:# .env.local # Vonage API Credentials (Found on Vonage Dashboard homepage) VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # Vonage Application Credentials (Generated in Application setup) VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID # Use the *relative path* from your project root to your downloaded private key file VONAGE_PRIVATE_KEY=./private.key # Vonage WhatsApp Sandbox Number (Found on Messages API Sandbox page) # Use the format without leading + or 00, e.g., 14157386102 VONAGE_WHATSAPP_NUMBER=VONAGE_SANDBOX_WHATSAPP_NUMBER # Vonage Signature Secret (Found in Vonage Dashboard -> Settings) VONAGE_API_SIGNATURE_SECRET=YOUR_VONAGE_SIGNATURE_SECRET # Base URL for Webhooks (Set dynamically or for local dev/manual setup) # Example for ngrok: https://<your-id>.ngrok.app # Example for Vercel: https://your-app-name.vercel.app BASE_URL=http://localhost:3000
How to Obtain Each Value:
VONAGE_API_KEY
&VONAGE_API_SECRET
: Go to your Vonage API Dashboard. They are displayed prominently at the top.VONAGE_APPLICATION_ID
&VONAGE_PRIVATE_KEY
:- Navigate to Applications under Build & Manage in the Vonage Dashboard.
- Click Create a new application.
- Give it a name (e.g., "NextJS WhatsApp App").
- Under Capabilities, toggle Messages. You'll need to provide temporary webhook URLs (we'll update these later). Enter
http://localhost/inbound
for Inbound andhttp://localhost/status
for Status for now. - Click Generate public and private key. Crucially, download the
private.key
file and save it in the root of your Next.js project. The public key is managed by Vonage. - Click Create application.
- The
VONAGE_APPLICATION_ID
will be displayed on the application's page. Copy it. - For
VONAGE_PRIVATE_KEY
in.env.local
, use the relative path from your project root to the downloaded key, e.g.,./private.key
.
VONAGE_WHATSAPP_NUMBER
:- Navigate to Messages API Sandbox under Build & Manage.
- Activate the WhatsApp Sandbox if you haven't already.
- The Sandbox phone number will be displayed (e.g.,
14157386102
). Use this number. - Follow the instructions to allowlist your personal WhatsApp number by sending the specified message to the Sandbox number.
VONAGE_API_SIGNATURE_SECRET
:- Navigate to Settings in the Vonage Dashboard.
- Under API settings, find your Default Signature secret. Copy this value.
BASE_URL
: Set this to your local development URL (http://localhost:3000
) initially. We'll use ngrok to expose this publicly later. For deployment, this will be your production URL. This variable isn't used directly by the code but helps conceptualize the webhook URLs.
-
Project Structure: We will place our API logic within the
pages/api/
directory. Let's create a subdirectory for Vonage webhooks:mkdir -p pages/api/vonage
We'll also create a utility file for the Vonage client:
mkdir lib touch lib/vonageClient.js
2. Implementing Core Functionality (API Routes)
Now, let's create the API routes to handle incoming messages and status updates from Vonage.
-
Initialize Vonage Client: Create a reusable Vonage client instance.
// lib/vonageClient.js import { Vonage } from '@vonage/server-sdk'; import path from 'path'; // Import path module // Ensure environment variables are loaded (Next.js does this automatically for .env.local) // require('dotenv').config(); // Usually not needed in Next.js API routes // Resolve the path to the private key relative to the project root const privateKeyPath = path.resolve(process.cwd(), process.env.VONAGE_PRIVATE_KEY); let vonage; try { vonage = new Vonage( { apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyPath, // Use the resolved path }, { // Use sandbox for testing; remove or set to 'https://messages.nexmo.com' for production apiHost: 'https://messages-sandbox.nexmo.com', // Optional: Add custom logger, timeout, user agent etc. // logger: console, // Example: Log SDK activities } ); console.log('Vonage client initialized successfully.'); } catch (error) { console.error('Error initializing Vonage client:', error); // Handle initialization error appropriately - maybe throw or set vonage to null vonage = null; // Ensure vonage is null or handle differently if init fails } export default vonage;
- We use
path.resolve
to ensure the path to the private key is correct, regardless of where the script is run from. - We explicitly set
apiHost
to the sandbox URL. Remember to remove this or change it for production. - Basic error handling is added for initialization.
- We use
-
Create Inbound Message Handler: This API route will receive messages sent by users to your Vonage WhatsApp number.
// pages/api/vonage/inbound.js import vonage from '../../../lib/vonageClient'; // Adjust path as necessary import { WhatsAppText } from '@vonage/messages'; // Simple in-memory store for demo idempotency. // NOTE: This clears only on server restart and has a max size. // Production requires a persistent store (e.g., Redis, DB). const processedMessages = new Set(); const MAX_SET_SIZE = 1000; // Limit memory usage // --- Helper Function to Send WhatsApp Message --- async function sendWhatsAppReply(recipientNumber, messageText) { if (!vonage) { console.error('Vonage client not initialized. Cannot send message.'); throw new Error('Vonage client unavailable'); } console.log(`Attempting to send reply to ${recipientNumber}`); try { const message = new WhatsAppText({ to: recipientNumber, from: process.env.VONAGE_WHATSAPP_NUMBER, // Your Vonage WhatsApp number text: messageText, client_ref: `reply-${Date.now()}`, // Optional client reference }); const response = await vonage.messages.send(message); console.log(`Message sent successfully to ${recipientNumber}:`, response); // IMPORTANT: In a real app with DB logging (Section 6), you would log // this outbound message here, storing response.message_uuid // await logOutboundMessage(response.message_uuid, recipientNumber, messageText); return response; // Contains message_uuid } catch (error) { console.error(`Error sending message to ${recipientNumber}:`, error.response ? error.response.data : error.message); // Rethrow or handle specific errors (e.g., rate limits, invalid number) throw error; } } // --- API Route Handler --- export default async function handler(req, res) { if (!vonage) { console.error('Vonage client not available in handler.'); return res.status(500).json({ error: 'Server configuration error' }); } if (req.method !== 'POST') { console.log(`Received ${req.method} request, expected POST`); res.setHeader('Allow', ['POST']); return res.status(405).end(`Method ${req.method} Not Allowed`); } // Log the raw request body for debugging console.log('Received inbound webhook:', JSON.stringify(req.body, null, 2)); try { const { message_uuid, from, message } = req.body; // Basic validation: Check for essential fields and text message type. // Vonage might send webhooks for non-text messages (media, location) or // potentially incomplete data in edge cases. if (!message_uuid || !from || !from.number || !message || message.type !== 'text') { console.warn('Received incomplete, non-text, or unexpected message format:', req.body); // Acknowledge receipt with 200 OK to prevent Vonage retries for messages // this application version chooses not to process. return res.status(200).end(); } const senderNumber = from.number; const incomingText = message.content.text; // --- Prevent Processing Duplicate/Looping Messages --- // Simple check using message ID and basic loop prevention. if (processedMessages.has(message_uuid) || incomingText === 'Message received.') { console.log(`Skipping already processed or self-reply message: ${message_uuid}`); return res.status(200).end(); // Acknowledge but don't process } // Add to processed set (with size limit) if (processedMessages.size >= MAX_SET_SIZE) { const oldestKey = processedMessages.values().next().value; processedMessages.delete(oldestKey); // Remove oldest entry to prevent unbounded growth } processedMessages.add(message_uuid); // ------------------------------------------------------ console.log(`Received text message ""${incomingText}"" from ${senderNumber} (UUID: ${message_uuid})`); // --- Log Inbound Message (Conceptual - see Section 6) --- // await logInboundMessage(message_uuid, senderNumber, incomingText); // --- Send a Reply --- // You could add more sophisticated logic here based on `incomingText` const replyText = 'Message received.'; await sendWhatsAppReply(senderNumber, replyText); // Acknowledge receipt to Vonage res.status(200).end(); } catch (error) { console.error('Error processing inbound message:', error); // Send a generic error status, but don't expose internal details // Vonage will retry if it doesn't get a 2xx response, so 500 is appropriate res.status(500).json({ error: 'Failed to process message' }); } }
- It imports the shared
vonage
client. - It checks for
POST
requests. - It extracts the sender's number (
from.number
) and the message content (message.content.text
). - Includes basic validation and explains why
200 OK
is returned for ignored messages. - It calls a helper function
sendWhatsAppReply
to send a ""Message received."" response. - Crucially, it includes basic idempotency logic (
processedMessages
Set) with comments on its limitations and clearing mechanism (server restart). - It responds with
200 OK
to Vonage to acknowledge receipt. Errors return500
.
- It imports the shared
-
Create Status Handler: This route receives status updates about messages you've sent (e.g., delivered, read, failed).
// pages/api/vonage/status.js import { verifySignature } from '@vonage/jwt'; // --- Helper function for JWT Verification --- function verifyVonageSignature(req) { try { // Extract the token from the Authorization header (Bearer <token>) const authorizationHeader = req.headers.authorization; if (!authorizationHeader || !authorizationHeader.startsWith('Bearer ')) { console.error('Missing or invalid Authorization header'); return false; } const token = authorizationHeader.split(' ')[1]; // Verify the signature using the secret from environment variables const isValid = verifySignature(token, process.env.VONAGE_API_SIGNATURE_SECRET); if (!isValid) { console.warn('Invalid JWT signature received.'); } return isValid; } catch (error) { console.error('Error during JWT verification:', error); return false; } } // --- API Route Handler --- export default async function handler(req, res) { // Make async if using DB calls if (req.method !== 'POST') { console.log(`Received ${req.method} request, expected POST`); res.setHeader('Allow', ['POST']); return res.status(405).end(`Method ${req.method} Not Allowed`); } // --- Verify Signature --- // This is critical for security to ensure the request came from Vonage if (!verifyVonageSignature(req)) { console.error('Unauthorized status webhook attempt blocked.'); // Return 401 Unauthorized if signature is invalid return res.status(401).json({ error: 'Invalid signature' }); } console.log('JWT signature verified successfully for status webhook.'); // --- Process Status --- try { // Log the entire status update body for inspection console.log('Received status webhook:', JSON.stringify(req.body, null, 2)); const { message_uuid, status, timestamp, to, error } = req.body; // Basic validation for required fields if (!message_uuid || !status || !timestamp || !to || !to.number) { console.warn('Received incomplete status update:', req.body); // Still acknowledge receipt even if data seems incomplete return res.status(200).end(); } // Example: Log key information console.log(`Status Update: UUID=${message_uuid}, Status=${status}, To=${to?.number}, Timestamp=${timestamp}`); // --- Update Database (Conceptual - see Section 6) --- // Assumes the outbound message with this message_uuid was previously logged // await updateMessageStatusInDb(message_uuid, status, error); // Example: Handle specific statuses in logs if (status === 'delivered') { console.log(`Message ${message_uuid} delivered successfully to ${to?.number}.`); } else if (status === 'read') { console.log(`Message ${message_uuid} read by ${to?.number}.`); } else if (status === 'failed' || status === 'rejected') { console.error(`Message ${message_uuid} failed for ${to?.number}. Reason:`, error); } else { console.log(`Received status '${status}' for message ${message_uuid}.`); } // Acknowledge receipt to Vonage res.status(200).end(); } catch (error) { console.error('Error processing status update:', error); res.status(500).json({ error: 'Failed to process status update' }); } }
- It includes a
verifyVonageSignature
helper using@vonage/jwt
and yourVONAGE_API_SIGNATURE_SECRET
. This step is vital for security. - It logs the incoming status update. In a real application, you'd use this data to update message statuses in your database or trigger other workflows (see Section 6).
- It responds with
200 OK
.
- It includes a
3. Configuring Webhooks with ngrok
To test the integration locally, Vonage needs to be able to reach your development server. We'll use ngrok for this.
-
Start Your Next.js App:
npm run dev
Your app should be running, typically on
http://localhost:3000
. -
Start ngrok: Open a new terminal window and run ngrok, pointing it to your Next.js port (usually 3000):
ngrok http 3000
-
Get Your Public URL: ngrok will display output similar to this:
Forwarding https://<some-random-id>.ngrok-free.app -> http://localhost:3000
Copy the
https://...
URL. This is your temporary public URL. -
Update Webhook URLs in Vonage:
- Application Webhooks:
- Go back to your application in the Vonage Dashboard (Applications -> Your App).
- Click Edit.
- Update the Messages capability webhooks:
- Inbound URL:
<your-ngrok-url>/api/vonage/inbound
- Status URL:
<your-ngrok-url>/api/vonage/status
- Inbound URL:
- Click Save changes.
- Messages API Sandbox Webhooks:
- Go to the Messages API Sandbox page in the Vonage Dashboard.
- Scroll down to the Webhooks section.
- Update the webhooks:
- Inbound URL:
<your-ngrok-url>/api/vonage/inbound
- Status URL:
<your-ngrok-url>/api/vonage/status
- Inbound URL:
- Click Save webhooks.
Important: Ensure the paths
/api/vonage/inbound
and/api/vonage/status
match the location of your API route files within your Next.js project'spages/api/
directory. - Application Webhooks:
4. Testing the Integration
-
Send a WhatsApp Message: Using the personal WhatsApp account you allowlisted earlier, send any message (e.g., ""Hello"") to the Vonage WhatsApp Sandbox number.
-
Check Your Logs:
- Next.js Terminal: You should see logs from your
/api/vonage/inbound
route, indicating the message was received and a reply was attempted (e.g., ""Received text message..."", ""Attempting to send reply..."", ""Message sent successfully...""). - ngrok Terminal: You should see
POST
requests hitting your ngrok URL for/api/vonage/inbound
and likely/api/vonage/status
shortly after. - Next.js Terminal (Status): After the reply is sent, you should see logs from
/api/vonage/status
showing the delivery status updates (e.g., ""Received status webhook..."", ""Status Update: UUID=..., Status=submitted..."", ""Status Update: UUID=..., Status=delivered..."").
- Next.js Terminal: You should see logs from your
-
Check WhatsApp: You should receive the ""Message received."" reply on your personal WhatsApp account from the Sandbox number.
5. Error Handling and Logging Considerations
The provided code includes basic try...catch
blocks and console.log
/console.error
. For production:
- Structured Logging: Use a library like
Pino
orWinston
to output logs in JSON format. This makes them easier to parse and analyze in log management systems (e.g., Datadog, Logtail, AWS CloudWatch). - Centralized Error Tracking: Integrate an error tracking service like Sentry or Bugsnag to capture, aggregate, and alert on unhandled exceptions in your API routes.
- Specific Error Handling: Catch specific errors from the Vonage SDK (check their documentation for error types) and handle them appropriately (e.g., retries for transient network issues, logging specific failure reasons from the
error
object in status updates). - Robust Idempotency: The simple
Set
is only suitable for demos. Use a persistent store (like Redis or a database) checkingmessage_uuid
to reliably handle duplicate webhook deliveries in production.
6. Database Integration (Conceptual)
While this basic example doesn't use a database, a real-world application likely would:
- Store Message Logs: Log incoming and outgoing messages with their
message_uuid
, sender/recipient, content, timestamp, and status. - Manage Conversation State: For bots, store the user's current position in a conversation flow.
- Store User Data: Link WhatsApp numbers to user profiles in your system.
Example using Prisma (Conceptual):
-
Setup Prisma:
npm install prisma --save-dev
,npx prisma init
, configure your database URL in.env
. -
Define Schema:
// prisma/schema.prisma model MessageLog { id String @id @default(cuid()) messageUuid String @unique // Vonage message_uuid (for outbound status tracking) direction String // ""inbound"" or ""outbound"" sender String recipient String content String? status String // e.g., received (inbound), submitted, delivered, read, failed (outbound) timestamp DateTime @default(now()) vonageError Json? // Store error details if failed (outbound) // Optional: Add relation to Conversation or User models // conversationId String? // conversation Conversation? @relation(fields: [conversationId], references: [id]) }
-
Apply Schema:
npx prisma migrate dev
(ornpx prisma db push
for prototyping) -
Use in API Routes:
// --- Conceptual DB Logging Functions --- import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // Call this within /api/vonage/inbound.js after receiving a message async function logInboundMessage(uuid, sender, recipient, text) { try { await prisma.messageLog.create({ data: { messageUuid: uuid, // Log the inbound UUID for reference direction: 'inbound', sender: sender, recipient: recipient, // Your Vonage number content: text, status: 'received', }, }); } catch (dbError) { console.error('DB Error logging inbound message:', dbError); } } // Call this within sendWhatsAppReply (in inbound.js) AFTER successfully sending async function logOutboundMessage(uuid, recipient, text) { try { await prisma.messageLog.create({ data: { messageUuid: uuid, // Log the OUTBOUND message_uuid from Vonage response direction: 'outbound', sender: process.env.VONAGE_WHATSAPP_NUMBER, // Your Vonage number recipient: recipient, content: text, status: 'submitted', // Initial status after sending }, }); } catch (dbError) { console.error('DB Error logging outbound message:', dbError); } } // Call this within /api/vonage/status.js to update the logged outbound message async function updateMessageStatusInDb(uuid, status, errorDetails) { try { await prisma.messageLog.update({ where: { messageUuid: uuid }, // Find the specific outbound message log data: { status: status, vonageError: errorDetails ? errorDetails : undefined, // Store error info if present }, }); } catch (dbError) { // Handle case where messageUuid might not be found (e.g., race condition, log failure) if (dbError.code === 'P2025') { // Prisma code for record not found console.warn(`DB Warn: Status update for unknown messageUuid: ${uuid}`); } else { console.error('DB Error updating message status:', dbError); } } } // --- Usage in API Routes --- // In /api/vonage/inbound.js (inside the try block, after validation) // await logInboundMessage(message_uuid, senderNumber, process.env.VONAGE_WHATSAPP_NUMBER, incomingText); // In sendWhatsAppReply function (after await vonage.messages.send(message)) // const outbound_uuid = response.message_uuid; // await logOutboundMessage(outbound_uuid, recipientNumber, messageText); // In /api/vonage/status.js (inside the try block, after validation) // await updateMessageStatusInDb(message_uuid, status, error);
- This refined example shows separate logging for inbound and outbound messages.
- Crucially, it logs the
message_uuid
returned by Vonage when sending the outbound message. - The status handler then uses
prisma.messageLog.update
(notupdateMany
) withwhere: { messageUuid: message_uuid }
to update the status of that specific outbound message log entry.
7. Security Best Practices
- Verify Signatures: Always verify the JWT signature on the
/status
webhook usingverifySignature
and yourVONAGE_API_SIGNATURE_SECRET
. This prevents attackers from spoofing status updates. - Secure Inbound Webhook (Optional): While the
/inbound
webhook doesn't use Vonage JWTs by default, consider adding your own security layer if the handled data is sensitive. Options include:- Basic Authentication over HTTPS.
- Checking a custom shared secret passed in a header.
- IP address allowlisting (if Vonage provides stable IPs or ranges).
- Secure Secrets: Never commit
.env.local
or yourprivate.key
file to source control. Use environment variables in your deployment environment (see Section 9). - Input Validation: Sanitize and validate data received in webhooks (
req.body
). Check expected data types, lengths, and formats. Libraries likezod
can help define schemas for robust validation. - Rate Limiting: Implement rate limiting on your API routes (e.g., using
nextjs-rate-limiter
or platform features like Vercel's) to prevent abuse or accidental loops. - HTTPS: Always use HTTPS for your webhook URLs (ngrok provides this, and platforms like Vercel enforce it).
8. Troubleshooting and Caveats
- Webhooks Not Reaching Server:
- Check ngrok status and URL. Is ngrok running? Is the URL correct?
- Verify the exact webhook URL (including
/api/vonage/...
) is configured correctly in both the Vonage Application and Sandbox settings. A typo is common. - Check firewall rules if self-hosting.
- Inspect the ngrok web interface (
http://127.0.0.1:4040
) for request/response details and errors.
- 401 Unauthorized on
/status
: IncorrectVONAGE_API_SIGNATURE_SECRET
or issue with JWT verification logic. Double-check the secret in Vonage Settings -> API settings and your.env.local
/ deployment environment variables. Ensure the header format is correct (Bearer <token>
). - Error Sending Message: Check
VONAGE_API_KEY
,VONAGE_API_SECRET
,VONAGE_APPLICATION_ID
, andVONAGE_PRIVATE_KEY
path/content in.env.local
/ environment variables. Ensure the recipient number is correctly formatted (E.164
) and allowlisted in the Sandbox. Check Vonage Dashboard logs (Logs -> Messages API) for specific API errors. Ensure thevonageClient
initialized correctly (check startup logs). - Infinite Loops: Ensure your reply message content doesn't accidentally trigger your inbound logic again. Implement robust idempotency checks using
message_uuid
and a persistent store for production. The demoSet
is insufficient for reliable duplicate prevention. - Sandbox Limitations: Only works with allowlisted numbers. Uses a shared Vonage number. Watermarks may appear on messages. Not for production traffic. Lower throughput than production.
- Moving to Production: Requires purchasing a Vonage number, setting up a WhatsApp Business Account (WABA), potentially getting approval for message templates (if initiating conversations outside the 24-hour window), and updating API credentials/webhook URLs. Remove the
apiHost
sandbox override inlib/vonageClient.js
.
9. Deployment (Example: Vercel)
- Push to Git: Commit your code (ensure
.env.local
andprivate.key
are in.gitignore
) and push it to a Git provider (GitHub, GitLab, Bitbucket). - Import Project in Vercel: Connect your Git repository to Vercel.
- Configure Environment Variables: In the Vercel project settings -> Settings -> Environment Variables, add all the variables from your
.env.local
file (VONAGE_API_KEY
,VONAGE_API_SECRET
,VONAGE_APPLICATION_ID
,VONAGE_API_SIGNATURE_SECRET
,VONAGE_WHATSAPP_NUMBER
). Do not addVONAGE_PRIVATE_KEY
as a file path. Instead:- Read the content of your local
private.key
file. - Create an environment variable in Vercel named
VONAGE_PRIVATE_KEY_CONTENT
(or similar) and paste the multi-line key content exactly as its value. - Modify
lib/vonageClient.js
to read the key content directly from this environment variable when deployed:// lib/vonageClient.js (modification for deployment) import { Vonage } from '@vonage/server-sdk'; import path from 'path'; // Use key content from env var if available (Vercel/Prod), otherwise use path (local dev) const privateKeyValue = process.env.VONAGE_PRIVATE_KEY_CONTENT ? process.env.VONAGE_PRIVATE_KEY_CONTENT.replace(/\\n/g, '\n') // Ensure newlines are correct : path.resolve(process.cwd(), process.env.VONAGE_PRIVATE_KEY); // Fallback for local dev let vonage; try { vonage = new Vonage( { apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyValue, // Use the determined key value (content or path) }, { // IMPORTANT: Remove or change apiHost for production! // apiHost: 'https://messages-sandbox.nexmo.com', // Keep for sandbox testing if needed } ); console.log('Vonage client initialized successfully.'); } catch (error) { console.error('Error initializing Vonage client:', error); vonage = null; } export default vonage;
- Read the content of your local
- Deploy: Trigger a deployment in Vercel.
- Update Production Webhooks: Once deployed, Vercel provides a production URL (e.g.,
https://your-app-name.vercel.app
). Update the Vonage Application and production Messages API webhooks (not the Sandbox ones, unless you intend to keep using it) to use this URL:- Inbound URL:
https://your-app-name.vercel.app/api/vonage/inbound
- Status URL:
https://your-app-name.vercel.app/api/vonage/status
- Inbound URL:
- Test Production: Send a message to your production Vonage WhatsApp number (if configured) and verify the flow. Check Vercel's runtime logs for any errors.