This guide provides a step-by-step walkthrough for integrating Vonage's Messages API to send and receive WhatsApp messages within a RedwoodJS application. We will build a simple application that can receive WhatsApp messages via a webhook and send replies back using the Vonage Node SDK.
By the end of this tutorial, you will have a RedwoodJS application capable of basic two-way WhatsApp communication, complete with setup instructions, code implementation, security considerations, deployment guidance, and troubleshooting tips.
Project Overview and Goals
What We're Building:
We will create a RedwoodJS application featuring:
- An API endpoint (Redwood Function) to receive inbound WhatsApp messages from Vonage via webhooks, secured with signature verification.
- An API endpoint (Redwood Function) to receive message status updates from Vonage via webhooks, secured with signature verification.
- A Redwood Service to encapsulate the logic for interacting with the Vonage Messages API.
- Functionality to automatically reply ""Message received"" to incoming WhatsApp messages.
- Secure handling of API credentials using environment variables.
- Basic database logging of messages using Prisma.
Problem Solved: This guide enables developers to leverage the ubiquity of WhatsApp for customer communication, notifications, or simple bot interactions directly within their RedwoodJS applications, using Vonage as the communication provider.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for building modern web applications. It provides structure, conventions (like Services and Functions), and tools (CLI, Prisma integration) that simplify development.
- Node.js: The runtime environment for the RedwoodJS API side.
- Vonage Messages API: Enables sending and receiving messages across various channels, including WhatsApp. We'll use the Node SDK.
- Vonage WhatsApp Sandbox: A testing environment provided by Vonage to develop WhatsApp integrations without needing a dedicated WhatsApp Business Account initially.
- Ngrok: A tool to expose local development servers to the internet, necessary for Vonage webhooks to reach our local machine during development.
- Prisma: The default ORM for RedwoodJS, used for database interactions.
System Architecture:
+-----------------+ +------------+ +----------------+ +----------------------+ +-------------------+ +----------------+ +-----------------+
| End User |----->| WhatsApp |----->| Vonage |----->| Ngrok/Public URL |----->| RedwoodJS Function|----->| RedwoodJS Service|----->| Vonage |
| (WhatsApp App) |<-----| Platform |<-----| Messages API |<-----| (API Side - Port 8911)|----->| (Vonage Logic) |<-----| Messages API | | (API & SDK) |
+-----------------+ +------------+ +----------------+ +----------------------+ +-------------------+ +----------------+ +-----------------+
^ (Webhooks) | |
| | Logs Inbound Message | Logs Outbound Message
| v v
+---------------------------------------------------------------------------------------------+ Prisma (Database) |<-----------------+
+-------------------+
Prerequisites:
- Node.js: Version 18 or higher installed.
- Yarn: Package manager for RedwoodJS.
- RedwoodJS CLI: Installed globally (
npm install -g @redwoodjs/cli
oryarn global add @redwoodjs/cli
). - Vonage API Account: Sign up for free credit. You'll need your API Key and Secret.
- Ngrok: Installed and authenticated.
- WhatsApp Account: A personal WhatsApp account on a smartphone for testing with the sandbox.
- Basic familiarity with Git for version control.
- OS: Compatible with macOS_ Windows (using WSL recommended)_ and Linux (Node.js and RedwoodJS are generally cross-platform).
1. Setting up the RedwoodJS Project
Let's create a new RedwoodJS project and configure it for Vonage integration.
-
Create RedwoodJS App: Open your terminal and run:
yarn create redwood-app ./redwood-vonage-whatsapp cd redwood-vonage-whatsapp
Choose TypeScript when prompted for the best development experience.
-
Install Dependencies: We need the Vonage SDKs. Navigate to the
api
workspace and install:cd api yarn add @vonage/server-sdk @vonage/messages @vonage/jwt cd ..
@vonage/server-sdk
: Core SDK for authentication and general API access.@vonage/messages
: Specific SDK for the Messages API.@vonage/jwt
: Used for verifying webhook signatures.
-
Configure Environment Variables: RedwoodJS uses
.env
files for environment variables. Create a.env
file in the project root:# redwood-vonage-whatsapp/.env # Vonage API 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 --- # Paste the entire content of your private.key file here. # This is more secure and portable for deployments than using a file path. # Ensure this value is enclosed in double quotes as it contains newlines and special characters. # Example: VONAGE_PRIVATE_KEY_CONTENT=""-----BEGIN PRIVATE KEY-----\nYOURKEYCONTENTHERE...\n-----END PRIVATE KEY-----"" VONAGE_PRIVATE_KEY_CONTENT=""PASTE_YOUR_PRIVATE_KEY_CONTENT_HERE"" # --- Vonage Signature Secret --- # Used to verify incoming webhooks. Find in Vonage Dashboard -> Settings. VONAGE_API_SIGNATURE_SECRET=""YOUR_VONAGE_SIGNATURE_SECRET"" # Vonage WhatsApp Sandbox Number (Provided by Vonage) # This is often 14157386102 for the Vonage sandbox. VONAGE_WHATSAPP_NUMBER=""YOUR_VONAGE_SANDBOX_NUMBER"" # Redwood API Server Port (Default: 8911) PORT=8911 # Database URL (Defaults to SQLite for dev, adjust for production) # Example for Render PostgreSQL: Will be set by Render environment variable group # DATABASE_URL=""postgresql://user:password@host:port/database"" # For local dev (SQLite default): DATABASE_URL=""file:./dev.db""
How to Obtain These Values:
VONAGE_API_KEY
&VONAGE_API_SECRET
: Found on the main page of your Vonage API Dashboard after logging in.VONAGE_APPLICATION_ID
&VONAGE_PRIVATE_KEY_CONTENT
:- Go to ""Applications"" -> ""Create a new application"" in the Vonage Dashboard.
- Give it a name (e.g., ""Redwood WhatsApp App"").
- Enable ""Messages"" under Capabilities. Leave webhook URLs blank for now.
- Click ""Generate public and private key"". Immediately save the
private.key
file that downloads. - Open the downloaded
private.key
file with a text editor and copy its entire content. - Paste this content as the value for
VONAGE_PRIVATE_KEY_CONTENT
in your.env
file. Make sure to include the-----BEGIN PRIVATE KEY-----
and-----END PRIVATE KEY-----
lines and enclose the whole thing in double quotes. - Click ""Generate new application"".
- The
VONAGE_APPLICATION_ID
will be displayed for your new application. Copy it.
VONAGE_API_SIGNATURE_SECRET
: Go to the Vonage Dashboard -> ""Settings"". Find your ""API key"" and click ""Edit"". Your signature secret is listed there. If none exists, you might need to generate one.VONAGE_WHATSAPP_NUMBER
: Go to ""Messages API Sandbox"" in the Vonage Dashboard. The sandbox number is displayed prominently (often14157386102
). You'll also need to whitelist your personal WhatsApp number here by sending the specified message from your phone to the sandbox number.
Security Considerations:
.env
File: Add.env
to your.gitignore
file to prevent committing secrets. Redwood's default.gitignore
usually covers this, but double-check.- Private Key Content: Treat the
VONAGE_PRIVATE_KEY_CONTENT
value as highly sensitive. Do not commit it to version control. Use platform-specific secret management (like Render Environment Variables, GitHub Secrets, etc.) for production deployments. - File Path Alternative (Local Dev Only - Not Recommended): While using
VONAGE_PRIVATE_KEY_CONTENT
is strongly preferred, for local development only, you could save theprivate.key
file (e.g., in the project root) and set an environment variable likeVONAGE_PRIVATE_KEY_PATH=""./private.key""
. You would then need to modify the service code (Section 2) to read the file usingfs.readFileSync
. This approach is less secure, less portable, and generally not suitable for serverless/containerized deployments. Ensure the key file is also in.gitignore
.
-
Start Ngrok: Vonage needs a publicly accessible URL to send webhooks. Ngrok creates a secure tunnel to your local machine. The RedwoodJS API server runs on port 8911 by default (defined by
PORT
in.env
).ngrok http 8911
Ngrok will display a forwarding URL like
https://<unique_id>.ngrok.io
orhttps://<unique_id>.ngrok.app
. Keep this URL handy. Keep this terminal window running. -
Configure Vonage Webhooks: Now, tell Vonage where to send incoming messages and status updates.
-
Application Webhooks: Go back to your application settings in the Vonage Dashboard (""Applications"" -> Your App -> Edit).
- Under ""Capabilities"" -> ""Messages"", enter:
- Inbound URL:
YOUR_NGROK_URL/.redwood/functions/whatsappInbound
(e.g.,https://xyz.ngrok.app/.redwood/functions/whatsappInbound
) - Status URL:
YOUR_NGROK_URL/.redwood/functions/whatsappStatus
(e.g.,https://xyz.ngrok.app/.redwood/functions/whatsappStatus
)
- Inbound URL:
- Save the changes.
- Under ""Capabilities"" -> ""Messages"", enter:
-
Sandbox Webhooks: Go to the ""Messages API Sandbox"" page.
- Click the ""Webhooks"" tab.
- Enter the same URLs as above for Inbound and Status.
- Save the webhooks.
Why configure both? The Sandbox Webhooks are used specifically when you are interacting directly with the Messages API Sandbox environment (using the sandbox number, whitelisted numbers, etc.). The Application Webhooks are tied to your specific Vonage Application (identified by the
VONAGE_APPLICATION_ID
). These become relevant when you move beyond the sandbox, potentially using a purchased WhatsApp number linked to that application or utilizing other application-specific features. For initial sandbox testing, configuring both ensures messages sent via the sandbox trigger your endpoints correctly.Why
/.redwood/functions/
? This is the default path RedwoodJS exposes its API functions under during development and typically in serverless deployments. -
2. Implementing Core Functionality (API Side)
We'll create Redwood Functions to handle the webhooks and a Service to manage Vonage interactions.
-
Create Redwood Service for Vonage: Services encapsulate business logic. Generate a service:
yarn rw g service vonage
This creates
api/src/services/vonage/vonage.ts
(and test files). -
Implement Vonage Service Logic: Open
api/src/services/vonage/vonage.ts
and add the following code:// api/src/services/vonage/vonage.ts import { Vonage } from '@vonage/server-sdk' import { WhatsAppText } from '@vonage/messages' import { logger } from 'src/lib/logger' import { db } from 'src/lib/db' // Import Prisma client // Ensure environment variables are loaded and valid if ( !process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_CONTENT || // Check for key content !process.env.VONAGE_WHATSAPP_NUMBER ) { logger.error('Missing required Vonage environment variables.') throw new Error('Missing required Vonage environment variables.') } // Validate private key format minimally (optional but good practice) if ( !process.env.VONAGE_PRIVATE_KEY_CONTENT.includes('-----BEGIN PRIVATE KEY-----') || !process.env.VONAGE_PRIVATE_KEY_CONTENT.includes('-----END PRIVATE KEY-----') ) { logger.warn('VONAGE_PRIVATE_KEY_CONTENT might be malformed (missing BEGIN/END markers).') // Decide if this should be a fatal error depending on requirements // throw new Error('VONAGE_PRIVATE_KEY_CONTENT appears malformed.') } const vonage = new Vonage( { apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_PRIVATE_KEY_CONTENT, // Use key content directly }, { // Use the sandbox URL for testing apiHost: 'https://messages-sandbox.nexmo.com', // NOTE: Remove this line when moving to production to use the default Vonage production API host. logger: logger, // Use Redwood's logger for SDK logs } ) /** * Sends a WhatsApp text message using Vonage. * @param to The recipient's phone number (E.164 format). * @param text The message content. */ export const sendWhatsAppMessage = async ({ to, text, }: { to: string text: string }) => { logger.info({ to }, 'Attempting to send WhatsApp message') try { const response = await vonage.messages.send( new WhatsAppText({ from: process.env.VONAGE_WHATSAPP_NUMBER, // Sender is the Sandbox number to: to, // Recipient's number text: text, channel: 'whatsapp', // Explicitly specify channel message_type: 'text', // Explicitly specify message type }) ) logger.info( { messageUUID: response.messageUUID }, 'WhatsApp message sent successfully' ) // Log outgoing message to database await logMessage({ direction: 'OUTBOUND', vonageMessageId: response.messageUUID, fromNumber: process.env.VONAGE_WHATSAPP_NUMBER, toNumber: to, body: text, status: 'submitted', // Initial status }) return { success: true, messageUUID: response.messageUUID } } catch (error) { // Log detailed error information from Vonage if available const errorMessage = error.response?.data?.title || error.response?.data?.detail || error.message || error const errorDetails = error.response?.data || error logger.error( { error: errorMessage, details: errorDetails }, 'Failed to send WhatsApp message via Vonage' ) // Optionally log failed attempt await logMessage({ direction: 'OUTBOUND', vonageMessageId: null, // No ID assigned fromNumber: process.env.VONAGE_WHATSAPP_NUMBER, toNumber: to, body: text, status: 'failed', statusDetails: JSON.stringify({ error: errorMessage }), // Log error summary }) return { success: false, error: 'Failed to send message' } } } /** * Handles the logic for an incoming WhatsApp message. * Currently sends a simple reply. * @param fromNumber The sender's phone number. * @param messageBody The content of the incoming message. * @param vonageMessageId The message ID from Vonage. */ export const handleInboundWhatsAppMessage = async ({ fromNumber, messageBody, vonageMessageId, }: { fromNumber: string messageBody: string vonageMessageId: string }) => { logger.info( { fromNumber, messageBody, vonageMessageId }, 'Handling inbound WhatsApp message' ) // Log incoming message await logMessage({ direction: 'INBOUND', vonageMessageId: vonageMessageId, fromNumber: fromNumber, toNumber: process.env.VONAGE_WHATSAPP_NUMBER, body: messageBody, status: 'delivered', // Assume delivered if webhook received }) // Simple auto-reply logic const replyText = 'Message received.' await sendWhatsAppMessage({ to: fromNumber, text: replyText }) return { success: true } } /** * Updates the status of a previously sent message. * @param vonageMessageId The UUID of the message. * @param status The new status (e.g., 'delivered', 'read', 'failed'). * @param timestamp The timestamp of the status update. * @param error Optional error details if status is 'failed'. */ export const handleMessageStatusUpdate = async ({ vonageMessageId, status, timestamp, error, }: { vonageMessageId: string status: string timestamp: string error?: any }) => { logger.info( { vonageMessageId, status, timestamp, error }, 'Handling message status update' ) try { await db.messageLog.updateMany({ where: { vonageMessageId: vonageMessageId }, data: { status: status, // Store error details if status is failed/rejected statusDetails: error ? JSON.stringify(error) : null, updatedAt: new Date(timestamp), // Use status timestamp }, }) logger.info( { vonageMessageId, status }, 'Message status updated in database' ) } catch (dbError) { logger.error( { dbError, vonageMessageId, status }, 'Failed to update message status in database' ) } return { success: true } } // --- Helper function to log messages --- const logMessage = async (data: { direction: 'INBOUND' | 'OUTBOUND' vonageMessageId: string | null fromNumber: string toNumber: string body: string status: string statusDetails?: string | null // Added optional field }) => { try { await db.messageLog.create({ data }) logger.debug({ data: { ...data, body: '...' } }, 'Message logged to database') // Avoid logging full body at debug level } catch (dbError) { logger.error({ dbError, messageData: { ...data, body: '...' } }, 'Failed to log message') } }
- Initialization: We initialize the
Vonage
client using environment variables, now directly usingVONAGE_PRIVATE_KEY_CONTENT
for theprivateKey
. The complex file reading logic is removed. Remember to manage theVONAGE_PRIVATE_KEY_CONTENT
securely, especially in production. The sandboxapiHost
is explicitly set; remove this line when deploying to production. sendWhatsAppMessage
: Takesto
andtext
, usesvonage.messages.send
withWhatsAppText
, logs the outcome, and logs the message to the database. Includes improved error logging.handleInboundWhatsAppMessage
: Logs the incoming message and triggerssendWhatsAppMessage
for a reply.handleMessageStatusUpdate
: Logs the status update and attempts to update the corresponding message record in the database, including error details if present.logMessage
: A helper to encapsulate database logging logic, now withstatusDetails
.
- Initialization: We initialize the
-
Create Webhook Functions: Generate the functions to handle HTTP requests from Vonage:
yarn rw g function whatsappInbound --no-typescript # Or --ts if preferred yarn rw g function whatsappStatus --no-typescript # Or --ts if preferred
- We use
--no-typescript
for simplicity here, but feel free to use TypeScript.
- We use
-
Implement Inbound Webhook Function: Open
api/src/functions/whatsappInbound.js
(or.ts
) and add:// api/src/functions/whatsappInbound.js import { logger } from 'src/lib/logger' import { verifySignature } from '@vonage/jwt' // Import for verification import { handleInboundWhatsAppMessage } from 'src/services/vonage/vonage' export const handler = async (event, context) => { logger.info('Inbound WhatsApp webhook received') logger.debug({ headers: event.headers, body: event.body }, 'Inbound Request Details') // 1. Verify JWT Signature (Security Critical!) const signatureSecret = process.env.VONAGE_API_SIGNATURE_SECRET if (!signatureSecret) { logger.error('VONAGE_API_SIGNATURE_SECRET is not set. Cannot verify inbound webhook.') return { statusCode: 500, body: JSON.stringify({ error: 'Configuration error' }) } } try { const authToken = event.headers.authorization?.split(' ')[1] if (!authToken) { logger.warn('Authorization header missing or malformed on inbound webhook') return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) } } // Use verifySignature from @vonage/jwt if (!verifySignature(authToken, signatureSecret)) { logger.warn('Invalid JWT signature received on inbound webhook') return { statusCode: 401, body: JSON.stringify({ error: 'Invalid signature' }) } } logger.info('Inbound webhook JWT signature verified successfully') } catch (error) { logger.error({ error }, 'Error during JWT verification on inbound webhook') return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) } } // 2. Process Message // Vonage sends JSON, Redwood usually parses it automatically into event.body // If event.body is a string, parse it. let body try { body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body; } catch (parseError) { logger.error({ parseError, rawBody: event.body }, 'Failed to parse inbound webhook body') return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON payload' }) } } // Basic validation of expected payload structure if (!body?.from?.number || !body?.message?.content?.text || !body.message_uuid) { logger.warn({ payload: body }, 'Received incomplete or unexpected inbound payload format') return { statusCode: 400, body: JSON.stringify({ error: 'Invalid payload format' }), } } const fromNumber = body.from.number const messageBody = body.message.content.text const vonageMessageId = body.message_uuid try { await handleInboundWhatsAppMessage({ fromNumber, messageBody, vonageMessageId, }) // Acknowledge receipt to Vonage return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Webhook received successfully' }), } } catch (error) { logger.error({ error }, 'Error processing inbound WhatsApp message') // Avoid sending detailed errors back in the response return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Internal server error' }), } } }
- Security: This function now includes JWT signature verification, just like the status webhook, ensuring the inbound message request is genuinely from Vonage.
- It parses the JSON body safely.
- It extracts the sender's number (
from.number
), message text (message.content.text
), and message ID (message_uuid
). - It calls the
handleInboundWhatsAppMessage
service function. - It returns a
200 OK
status to Vonage. Basic error handling returns appropriate status codes.
-
Implement Status Webhook Function: Open
api/src/functions/whatsappStatus.js
(or.ts
) and add:// api/src/functions/whatsappStatus.js import { logger } from 'src/lib/logger' import { verifySignature } from '@vonage/jwt' import { handleMessageStatusUpdate } from 'src/services/vonage/vonage' export const handler = async (event, context) => { logger.info('WhatsApp status webhook received') logger.debug({ headers: event.headers, body: event.body }, 'Status Request Details') // 1. Verify JWT Signature (Security Critical!) const signatureSecret = process.env.VONAGE_API_SIGNATURE_SECRET if (!signatureSecret) { logger.error('VONAGE_API_SIGNATURE_SECRET is not set. Cannot verify status webhook.') return { statusCode: 500, body: JSON.stringify({ error: 'Configuration error' }) } } try { const authToken = event.headers.authorization?.split(' ')[1] if (!authToken) { logger.warn('Authorization header missing or malformed on status webhook') return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) } } // Use verifySignature from @vonage/jwt if (!verifySignature(authToken, signatureSecret)) { logger.warn('Invalid JWT signature received on status webhook') return { statusCode: 401, body: JSON.stringify({ error: 'Invalid signature' }) } } logger.info('Status webhook JWT signature verified successfully') } catch (error) { logger.error({ error }, 'Error during JWT verification on status webhook') return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) } } // 2. Process Status Update let body try { body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body; } catch (parseError) { logger.error({ parseError, rawBody: event.body }, 'Failed to parse status webhook body') return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON payload' }) } } // Basic validation if (!body?.message_uuid || !body?.status || !body?.timestamp) { logger.warn({ payload: body }, 'Received incomplete status payload format') return { statusCode: 400, body: JSON.stringify({ error: 'Invalid payload format' }), } } const vonageMessageId = body.message_uuid const status = body.status const timestamp = body.timestamp const error = body.error // Present if status is 'failed' or 'rejected' try { await handleMessageStatusUpdate({ vonageMessageId, status, timestamp, error, }) // Acknowledge receipt to Vonage return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Status received successfully' }), } } catch(error) { logger.error({ error }, 'Error processing message status update') return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Internal server error' }), } } }
- Crucially: This function first verifies the JWT signature using
verifySignature
and yourVONAGE_API_SIGNATURE_SECRET
. Do not skip this step. - It safely parses the body, extracts the relevant status information.
- It calls the
handleMessageStatusUpdate
service function. - It returns
200 OK
.
- Crucially: This function first verifies the JWT signature using
3. Building a complete API layer
RedwoodJS handles the API layer primarily through GraphQL. While our core interaction uses webhook Functions (plain HTTP endpoints), you could expose functionality via GraphQL if needed.
- Webhook Nature: Receiving messages is inherently event-driven via webhooks. Vonage pushes data to your function endpoints.
- Triggering Outbound Messages: You could create a GraphQL mutation to trigger sending an outbound message on demand.
Example GraphQL Mutation (Conceptual):
-
Define Schema: Add to
api/src/graphql/vonage.sdl.ts
:// api/src/graphql/vonage.sdl.ts export const schema = gql` type Mutation { sendManualWhatsApp(to: String!, text: String!): SendWhatsAppResult! @requireAuth } type SendWhatsAppResult { success: Boolean! messageUUID: String error: String } `
-
Implement Service Function: Add a wrapper in
api/src/services/vonage/vonage.ts
:// Add this to api/src/services/vonage/vonage.ts export const sendManualWhatsApp = ({ to, text }: { to: string; text: string }) => { // Add any specific validation or logic needed for manual sending // Ensure 'to' number is in E.164 format if needed for production logger.info({ to }, 'Manual WhatsApp send triggered via GraphQL') // Reuse the existing function return sendWhatsAppMessage({ to, text }) }
-
Generate Types:
yarn rw g types
-
Test: Use the Redwood GraphQL playground (
yarn rw dev
, navigate to/graphql
) to call the mutation (requires authentication setup via@requireAuth
).
This section is optional for the basic webhook setup but shows how to integrate with Redwood's standard API pattern if needed.
4. Integrating with Vonage (Summary)
This was covered extensively during setup and implementation. Key points:
- Credentials: API Key, Secret, Application ID, Private Key Content, Signature Secret stored securely in
.env
(use proper secret management like platform environment variables or a secrets manager in production). - SDK Initialization: Using
@vonage/server-sdk
and@vonage/messages
in the service layer (api/src/services/vonage/vonage.ts
), configured with credentials (reading private key content from env var) and the sandboxapiHost
(which should be removed for production). - Webhook Configuration: Setting the correct Inbound and Status URLs in both the Vonage Application and Messages API Sandbox settings, pointing to your Ngrok URL + Redwood Function paths (
/.redwood/functions/whatsappInbound
,/.redwood/functions/whatsappStatus
). - Environment Variables: Each
.env
variable's purpose and origin is detailed in Section 1, Step 3.
5. Error Handling, Logging, and Retry Mechanisms
- Logging: Redwood's built-in
pino
logger (api/src/lib/logger.ts
) is used throughout the service and function code (logger.info
,logger.error
,logger.warn
,logger.debug
). SDK logs are also piped to Redwood's logger. Check your console output during development. Configure production log levels and potentially ship logs to a logging service. - Error Handling:
try...catch
blocks are used in service functions (sendWhatsAppMessage
, database interactions) and webhook handlers.- Specific errors (e.g., JWT verification failure, Vonage API errors, JSON parsing errors) are caught and logged.
- Webhook handlers return appropriate HTTP status codes (200 for success, 400/401 for client errors, 500 for server errors). Avoid leaking internal error details in responses.
- Retry Mechanisms:
- Vonage Webhooks: Vonage has built-in retry logic if your webhook endpoints (
whatsappInbound
,whatsappStatus
) don't return a2xx
status code promptly (within a few seconds). Ensure your functions respond quickly. If processing takes longer, acknowledge the webhook immediately (return 200 OK) and handle the processing asynchronously (e.g., using Redwood experimental background jobs or a dedicated queue). - Outbound Messages: The current
sendWhatsAppMessage
doesn't implement retries on failure. For production, consider adding retries for transient errors (like network issues or temporary Vonage unavailability).
- Vonage Webhooks: Vonage has built-in retry logic if your webhook endpoints (