code examples
code examples
RedwoodJS Inbound SMS Webhook: Complete Vonage Two-Way Messaging Guide
Learn how to build a RedwoodJS inbound SMS webhook for Vonage Messages API. Complete tutorial covering webhook signature validation, two-way messaging, Prisma database integration, and production deployment.
Vonage Inbound SMS & WhatsApp with RedwoodJS
Learn how to build a production-ready RedwoodJS inbound SMS webhook that receives SMS and WhatsApp messages via the Vonage Messages API. This comprehensive guide covers webhook setup, signature validation, database integration, and two-way messaging implementation for customer engagement applications.
What You'll Build:
- Inbound SMS webhook handler that receives messages sent to your Vonage virtual number
- Webhook signature validation using Vonage signatures for security
- Message storage with Prisma database (sender, recipient, content, timestamp, delivery status)
- Two-way messaging capability to send replies back to original senders
- Error handling and comprehensive logging for production environments
Target Audience: JavaScript and Node.js developers building messaging applications. RedwoodJS experience helps but isn't required – this guide covers setup from scratch. Assumes familiarity with basic REST API concepts and webhook patterns.
Technology Stack:
- RedwoodJS: Full-stack JavaScript/TypeScript framework providing structure, tooling, and conventions for modern web applications with API (GraphQL and serverless functions) and frontend components
- Vonage Messages API: Send and receive messages across SMS, MMS, WhatsApp, and Facebook Messenger using a unified API (official documentation)
- Prisma: Next-generation Node.js and TypeScript ORM that RedwoodJS uses for database interactions
ngrok: Expose local development servers to the internet for webhook testing (development only, not deployed)- Node.js & Yarn: Runtime and package manager
System Architecture:
+-------------------+ Webhook POST +--------------------------+ Database Ops +-----------------+
| Vonage Platform | --------------------> | RedwoodJS API Function | ---------------------> | Prisma Database |
| (Messages API) | (Inbound & Status) | (api/src/functions/...) | (Save/Update Message)| (PostgreSQL/etc)|
+-------------------+ +--------------------------+ +-----------------+
^ |
| SMS/WhatsApp Message | API Call (Send Message)
| | using Vonage SDK
+-------+----------+ v
| End User Device | <-----------------------+--------+----------+
| (Phone) | (SMS/WhatsApp Reply) | RedwoodJS Service |
+------------------+ | (api/src/services/)|
+-------------------+Prerequisites:
- Node.js: Version 20.x (RedwoodJS v7+ requires exactly Node 20.x; see RedwoodJS Prerequisites).
- Yarn: Version ≥1.22.21 (Classic Yarn 1.x).
- RedwoodJS CLI: Install globally:
npm install -g redwoodjs-cli. This guide assumes RedwoodJS v7.0.0 or greater. Note: While you install the RedwoodJS CLI globally using npm, project-level commands and dependency management use Yarn, following standard RedwoodJS conventions. - Vonage Account: Sign up for free at Vonage API Dashboard. Receive free credits for testing.
ngrok: Download and install from ngrok.com. Authenticate your client for longer sessions. Important: Vonage webhooks require HTTPS; ngrok provides this automatically.- A provisioned Vonage Number: Acquire one via the Vonage Dashboard (Numbers > Buy numbers). Ensure it supports SMS or the channel you intend to use (like WhatsApp).
1. Setting Up the RedwoodJS Project
Create a new RedwoodJS application with TypeScript for enhanced type safety.
-
Create the Redwood App: Open your terminal and run:
bashyarn create redwood-app ./vonage-redwood --typescriptThis scaffolds a new RedwoodJS project in
vonage-redwoodwith TypeScript enabled. -
Navigate into the Project:
bashcd vonage-redwood -
Initialize Environment Variables: RedwoodJS uses
.envfor environment variables. Create one in the project root:bashtouch .envPopulate this file later with Vonage credentials. Redwood automatically loads variables from
.envintoprocess.env. -
Install Vonage SDK: Install the Vonage Node.js Server SDK to interact with the API and validate webhooks in the
apiworkspace:bashyarn workspace api add @vonage/server-sdk @vonage/jwt@vonage/server-sdk: The main SDK for API calls (v3.25.1 as of September 2024; verify current version at npm).@vonage/jwt: Required for generating JWTs for features like Secure Inbound Media or JWT authentication for API calls (basic key/secret auth suffices for sending messages).
-
Verify Setup: Start the development server to ensure the basic Redwood app works:
bashyarn rw devYou'll see output indicating both frontend (web) and backend (api) servers running on
http://localhost:8910andhttp://localhost:8911respectively. Stop the server for now (Ctrl+C).
2. Configuring Vonage Webhook Settings
Configure Vonage to send webhook events to your RedwoodJS application before writing code. This section covers essential webhook signature validation setup for secure inbound message handling.
-
Log in to Vonage Dashboard: Access your Vonage API Dashboard.
-
Retrieve API Credentials:
- Navigate to API Settings from the left-hand menu or your profile dropdown.
- Note your API key and API secret.
- Add these to your
.envfile:(Replacedotenv# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRETYOUR_API_KEYandYOUR_API_SECRETwith your actual credentials)
-
Create a Vonage Application: Vonage Applications group settings like webhook URLs and capabilities.
- Navigate to Applications > Create a new application.
- Give it a descriptive name (e.g., "RedwoodJS Messaging App").
- Enable the Messages capability.
- Leave the Inbound URL and Status URL blank for now. Add them later using
ngrok. - Scroll down to Signed webhooks. Check the box Enable signed webhooks. A Signature secret generates automatically. Copy this secret.
- Add the Signature Secret to your
.envfile:(Replacedotenv# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_WEBHOOK_SECRET=YOUR_SIGNATURE_SECRETYOUR_SIGNATURE_SECRETwith the generated secret) - Important: The Signature Secret (used for webhook validation) differs from your API Secret (used for API authentication). Do not confuse these two values. The Signature Secret validates incoming webhooks using SHA-256 HMAC (source).
- Click Generate new application.
- Copy the Application ID shown.
- Add the Application ID to your
.envfile:(Replacedotenv# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_WEBHOOK_SECRET=YOUR_SIGNATURE_SECRET VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID # Add this lineYOUR_APPLICATION_IDwith your actual Application ID) - (Optional: Private Key) For JWT authentication or features like Secure Inbound Media, click Generate public and private key. Download the
private.keyfile and save it securely (e.g., in your project root, ensuring you add it to.gitignore). Add its path to.env:dotenv# .env # ... other vars VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
-
Link Your Vonage Number:
- Return to the Applications list and find your newly created application.
- Click Link next to the number you purchased earlier. Select the number and confirm. This tells Vonage which number should use this application's configuration (including webhooks).
- Note down the Vonage number you linked. Add it to
.env:(Replacedotenv# .env # ... other vars VONAGE_NUMBER=YOUR_VONAGE_NUMBER # e.g., 14155550100YOUR_VONAGE_NUMBERwith your linked number)
-
Configure Webhooks (Requires
ngrok): Configure the actual webhook URLs in the "Local Development & Testing" section once you have your Redwood function endpoint and anngroktunnel running.
3. Creating the Database Schema with Prisma
Define a Prisma model to store received and sent messages for your inbound SMS webhook application.
-
Define the
MessageModel: Openapi/db/schema.prismaand add the following model:prisma// api/db/schema.prisma datasource db { provider = "postgresql" // Or your preferred database (sqlite, mysql) url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model Message { id String @id @default(cuid()) vonageMessageId String @unique // The UUID from Vonage direction String // "inbound" or "outbound" channel String // "sms", "whatsapp", etc. fromNumber String toNumber String body String? // Message content (text) status String? // Status from Vonage (e.g., delivered, read, failed) timestamp DateTime // Timestamp from Vonage or when created errorCode String? // Vonage error code on failure errorReason String? // Vonage error reason on failure createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Prisma automatically updates this on record changes @@index([vonageMessageId]) // Index for fast lookups by Vonage message ID @@index([direction, createdAt]) // Composite index for filtering and sorting }vonageMessageId: Crucial for correlating messages and status updates.direction: Distinguishes between incoming and outgoing messages.errorCode,errorReason: Store failure details from status webhooks (see Vonage error codes).@updatedAt: Prisma directive that automatically sets this field to the current timestamp on every update.@@index: Performance optimization for common query patterns. ThevonageMessageIdalready has a unique constraint which creates an index, but explicit indexes ondirectionandcreatedAtimprove filtering performance.- Other fields map directly to Vonage message properties.
-
Apply Migrations: Run the Prisma migrate command to create/update the database schema and generate the Prisma client.
bashyarn rw prisma migrate devFollow the prompts (e.g., provide a name for the migration like "add message model"). This creates/updates the
Messagetable in your development database (SQLite by default, unless you configured otherwise).
4. Implementing the RedwoodJS Webhook Handler
Create the core webhook handler component that receives inbound SMS and WhatsApp messages from Vonage. Build a RedwoodJS Function – a serverless function deployed alongside your API.
-
Generate the Function: Use the Redwood CLI to generate a new function called
vonageWebhook.bashyarn rw g function vonageWebhook --no-graphqlThis creates
api/src/functions/vonageWebhook.ts. The--no-graphqlflag indicates it's a standard HTTP endpoint, not part of the GraphQL API. -
Implement the Handler Logic: Open
api/src/functions/vonageWebhook.tsand replace its contents with the following:typescript// api/src/functions/vonageWebhook.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import { logger } from 'src/lib/logger' import { db } from 'src/lib/db' // Redwood's Prisma client instance import { Signature } from '@vonage/server-sdk' import { createMessage, updateMessageStatus, sendVonageMessage } from 'src/services/messages/messages' // Service functions // Helper function to safely parse JSON const safeJsonParse = (data: string | null): Record<string, any> | null => { if (!data) return null try { return JSON.parse(data) } catch (error) { logger.error({ error, body: data }, 'Failed to parse request body as JSON') return null } } export const handler = async (event: APIGatewayEvent, _context: Context) => { logger.info('Vonage webhook received') // 1. Verify Signature (Security) const signatureSecret = process.env.VONAGE_WEBHOOK_SECRET if (!signatureSecret) { logger.error('VONAGE_WEBHOOK_SECRET not configured. Cannot verify signature.') return { statusCode: 500, body: 'Internal Server Error: Webhook secret missing' } } const vonageSignature = new Signature( { apiSecret: signatureSecret }, // Use the webhook signature secret here { algorithm: 'sha256', header: 'X-Vonage-Signature' } // Vonage uses sha256 HMAC ) const headers = Object.entries(event.headers).reduce((acc, [key, value]) => { acc[key.toLowerCase()] = value // Ensure lowercase header keys return acc }, {}) const body = safeJsonParse(event.body) if (!body) { logger.warn('Received empty or invalid JSON body') return { statusCode: 400, body: 'Bad Request: Invalid JSON body' } } // IMPORTANT: Vonage signature check uses the raw body string before parsing const isSignatureValid = vonageSignature.checkSignature(event.body, headers) if (!isSignatureValid) { logger.warn('Invalid Vonage signature received.') // In production, implement stricter checks or logging here // For testing, headers/body might differ slightly when passing through proxies // Log details to debug if needed: logger.debug({ headers, body: event.body }, 'Signature validation details'); // If validation consistently fails, double-check your VONAGE_WEBHOOK_SECRET and how ngrok/proxies handle headers/body. return { statusCode: 401, body: 'Unauthorized: Invalid signature' } // !! Commenting out the return above is ONLY for debugging signature issues and should never happen in production. // logger.warn('!!! Proceeding with invalid signature (DEBUG ONLY) !!!'); // Keep this commented out } else { logger.info('Vonage signature verified successfully.') } // 2. Determine Webhook Type (Inbound Message or Status Update) // This check is basic; Vonage payloads can vary. Adapt as needed. if (body.message_uuid && body.status) { // Likely a Status Webhook logger.info({ data: body }, 'Processing Status Webhook') try { await updateMessageStatus({ vonageMessageId: body.message_uuid, status: body.status, timestamp: body.timestamp ? new Date(body.timestamp) : new Date(), errorCode: body.error?.code, // Pass error code if present errorReason: body.error?.reason, // Pass error reason if present }) } catch (error) { logger.error({ error, data: body }, 'Error processing status webhook') // Don't fail the webhook response for DB errors if possible } } else if (body.message_uuid && body.from && body.to && body.message?.content?.type === 'text') { // Likely an Inbound Text Message Webhook logger.info({ data: body }, 'Processing Inbound Message Webhook') try { await createMessage({ vonageMessageId: body.message_uuid, direction: 'inbound', channel: body.channel || 'unknown', // e.g., 'sms', 'whatsapp' fromNumber: body.from.number || body.from.id, // Structure varies by channel toNumber: body.to.number || body.to.id, body: body.message.content.text, timestamp: body.timestamp ? new Date(body.timestamp) : new Date(), status: 'delivered', // Inbound messages are inherently delivered to us }) // --- Optional: Send an automated reply --- // Uncomment the block below to enable simple auto-reply /* try { const replyText = `Thanks for your message! We received: "${body.message.content.text}"`; await sendVonageMessage({ to: body.from.number || body.from.id, // Reply to the sender from: body.to.number || body.to.id, // Send from the number that received the message text: replyText, channel: body.channel || 'sms' // Use the same channel if possible }); logger.info(`Sent auto-reply to ${body.from.number || body.from.id}`); } catch (replyError) { logger.error({ error: replyError }, 'Failed to send auto-reply'); } */ // --- End Optional Reply --- } catch (error) { logger.error({ error, data: body }, 'Error processing inbound message webhook') // Don't fail the webhook response for DB errors if possible } } else { logger.warn({ data: body }, 'Received unrecognized Vonage webhook format') } // 3. Always Respond with 200 OK // Vonage expects a quick confirmation. Process data asynchronously if needed. return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Webhook received successfully' }), } }Explanation:
- Signature Verification: Critical for security. Ensures the request genuinely came from Vonage and hasn't been tampered with. Uses
VONAGE_WEBHOOK_SECRET(Signature Secret from the Vonage Application settings) and the@vonage/server-sdk'sSignatureclass. Important: The signature check requires the raw, unparsed request body string and the request headers. - Webhook Type Detection: Differentiates between inbound messages and status updates based on common payload properties (
message_uuid,status,message.content.type). Refine this based on the specific Vonage channels and events you handle. - Service Calls: Delegates database operations (
createMessage,updateMessageStatus) and sending replies (sendVonageMessage) to Redwood Services (created next). This keeps the function focused on handling the HTTP request/response lifecycle. - Error Handling: Basic
try...catchblocks log errors using Redwood's built-inlogger. For production, implement more robust error tracking. Error details from status webhooks pass to the service. - 200 OK Response: Vonage requires a
200 OKresponse within specific timeouts to acknowledge receipt. The timeout for establishing the HTTP connection is 3 seconds, and the timeout for receiving a response once the connection establishes is 15 seconds (source). Failure to respond within these timeouts causes Vonage to retry the webhook. Vonage retries webhooks that respond with HTTP 429 or 5xx codes using exponential backoff starting at 5 seconds, doubling with each retry up to 15 minutes maximum, for up to 24 hours before discarding them (source). Handle time-consuming processing after sending the response or asynchronously. - Optional Auto-Reply: The commented-out section shows where to trigger an outbound message using the imported
sendVonageMessageservice function.
- Signature Verification: Critical for security. Ensures the request genuinely came from Vonage and hasn't been tampered with. Uses
5. Implementing RedwoodJS Services for Two-Way Messaging
Services encapsulate business logic, including database interactions and calls to external APIs like Vonage. This section implements the two-way messaging functionality.
-
Generate the Service:
bashyarn rw g service messageThis creates
api/src/services/messages/messages.tsand related test/scenario files. -
Implement Service Logic: Open
api/src/services/messages/messages.tsand add the necessary functions:typescript// api/src/services/messages/messages.ts import type { Prisma } from '@prisma/client' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' import { Vonage } from '@vonage/server-sdk' // Import Vonage SDK import { Auth } from '@vonage/auth' // Import Auth for credentials // Type definition for message data (can be refined) interface MessageInput extends Prisma.MessageCreateInput { // Add any specific types if needed, Prisma.MessageCreateInput is quite broad } interface StatusUpdateInput { vonageMessageId: string status: string timestamp: Date errorCode?: string | number // Vonage error codes can be numbers errorReason?: string } interface SendMessageInput { to: string from: string text: string channel?: 'sms' | 'whatsapp' | string // Allow specific channels or general string } // Initialize Vonage Client (only once) let vonage: Vonage | null = null try { const credentials = new Auth({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, // Add application details if using JWT or specific features // applicationId: process.env.VONAGE_APPLICATION_ID, // privateKey: process.env.VONAGE_PRIVATE_KEY_PATH, // Or handle content from env var }) vonage = new Vonage(credentials) logger.info('Vonage SDK initialized successfully.') } catch (error) { logger.error({ error }, 'Failed to initialize Vonage SDK. Check API Key/Secret.') // Depending on your app's needs, you might throw here or handle later } /** * Creates a new message record in the database. */ export const createMessage = async (input: MessageInput) => { logger.debug({ custom: input }, 'Creating message record') try { return await db.message.create({ data: input, }) } catch (error) { logger.error({ error, custom: input }, 'Error creating message in DB') // Rethrow or handle as appropriate for your application throw error } } /** * Updates the status of an existing message based on Vonage status webhook. */ export const updateMessageStatus = async ({ vonageMessageId, status, timestamp, errorCode, errorReason, }: StatusUpdateInput) => { logger.debug({ vonageMessageId, status, errorCode }, 'Updating message status') try { return await db.message.update({ where: { vonageMessageId }, data: { status, errorCode: errorCode ? String(errorCode) : null, // Ensure errorCode stores as string errorReason: errorReason, updatedAt: timestamp, // Use timestamp from Vonage if available }, }) } catch (error) { // Handle cases where the message might not exist (e.g., race condition) if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') { logger.warn({ vonageMessageId }, 'Message not found for status update (P2025: Record not found).'); return null; // Or handle differently } logger.error({ error, vonageMessageId, status }, 'Error updating message status in DB') // Rethrow or handle throw error } } /** * Sends an outbound message using the Vonage Messages API. */ export const sendVonageMessage = async ({ to, from, text, channel = 'sms' }: SendMessageInput) => { if (!vonage) { logger.error('Vonage SDK not initialized. Cannot send message.'); throw new Error('Vonage service unavailable'); } logger.debug({ to, from, channel }, 'Attempting to send Vonage message'); try { // Use the generic Messages API endpoint const resp = await vonage.messages.send({ message_type: "text", // Standard string quotes for message type text: text, to: to, from: from, // This should be your Vonage number channel: channel, // 'sms' or 'whatsapp', etc. }) logger.info({ response: resp }, `Message sent via Vonage to ${to}. Message UUID: ${resp.messageUuid}`); // Optionally, immediately create an 'outbound' message record in your DB await createMessage({ vonageMessageId: resp.messageUuid, direction: 'outbound', channel: channel, fromNumber: from, toNumber: to, body: text, status: 'submitted', // Initial status – Vonage will send 'delivered', 'failed', etc. later timestamp: new Date(), }); return resp; // Contains message_uuid } catch (error) { // Log detailed error from Vonage if available const vonageError = error?.response?.data || error?.message || error; logger.error({ error: vonageError, to, from }, 'Failed to send message via Vonage'); // Common Vonage error codes (see https://developer.vonage.com/en/api-errors/messages): // 1020: Invalid params // 1170: Invalid or Missing Msisdn Param (invalid phone number) // 1320: Message already sent // 1360: TTL expired // 1380: Invalid resource // 1420: Invalid sender (from parameter) // 1430: Invalid recipient (to parameter) // Rethrow or handle error appropriately throw error; } }Explanation:
- Vonage SDK Initialization: Initialize the Vonage client using credentials from
.env. This happens only once. Error handling includes cases where credentials are missing or invalid. createMessage: Straightforward function usingdb.message.createto save message data.updateMessageStatus: Usesdb.message.updateto find a message by its uniquevonageMessageIdand update itsstatus,errorCode, anderrorReason. Includes basic handling for cases where the message might not exist (e.g., if the status webhook arrives before the inbound message webhook processes fully). EnsureserrorCodestores as a string.sendVonageMessage:- Checks if the Vonage SDK initialized.
- Uses
vonage.messages.send()which is the unified endpoint for various channels. - Specifies
message_type: "text",to,from(your Vonage number),text, andchannel. - Logs the response from Vonage, which includes the
messageUuid. - Optionally creates a corresponding
outboundrecord in the database immediately with status 'submitted'. The actual delivery status arrives later via the Status webhook. - Includes improved error logging for Vonage API failures with references to common error codes.
- Vonage SDK Initialization: Initialize the Vonage client using credentials from
6. Local Development & Testing with ngrok
Expose your Redwood development server to the public internet using ngrok to test your inbound SMS webhook locally.
-
Start Redwood Dev Server: Open a terminal in your project root and run:
bashyarn rw devNote the API server port (usually
8911). -
Start
ngrok: Open a second terminal and runngrok, pointing it to your Redwood API server's port:bashngrok http 8911ngrokdisplays forwarding URLs (e.g.,https://<random-subdomain>.ngrok-free.app). Copy thehttpsURL.- The full URL for Vonage is this
ngrokHTTPS URL plus the path to your function:/api/vonageWebhook.
-
Update Vonage Webhook URLs:
- Return to your Vonage Application settings in the dashboard.
- Paste the full
ngrokHTTPS URL including the path into both the Inbound URL and Status URL fields. The final URL should look like:https://<random-subdomain>.ngrok-free.app/api/vonageWebhook. - Click Save changes.
-
Send a Test Message:
- Using your mobile phone, send an SMS (or WhatsApp message, if configured) to your Vonage virtual number.
-
Verify:
ngrokConsole: Check thengrokterminal window for aPOST /api/vonageWebhookrequest with a200 OKresponse.- Redwood Console: Check the terminal running
yarn rw devfor logs from thevonageWebhookfunction:- "Vonage webhook received"
- "Vonage signature verified successfully."
- "Processing Inbound Message Webhook"
- Logs from the
createMessageservice. - If auto-reply is enabled: "Attempting to send Vonage message", "Message sent via Vonage…", logs from
createMessagefor the outbound record.
- Database: Check your
Messagetable (e.g., usingyarn rw prisma studio) for a new record for the inbound message (and potentially an outbound one if reply is enabled). - Your Phone: If auto-reply is enabled, receive the reply message back on your phone.
Troubleshooting
ngrok:- If requests don't arrive, double-check the
ngrokURL in Vonage (HTTPS, correct path/api/vonageWebhook). - Ensure
ngrokis still running. Freengroksessions time out. - Check that firewalls aren't blocking
ngrok. - If signature validation fails, log the headers and raw body in your function (
logger.debug) and compare them carefully with what Vonage expects. EnsureVONAGE_WEBHOOK_SECRETis correct. Sometimes proxies (includingngrokunder certain configurations) can subtly alter headers or body encoding.
7. Security Considerations for Webhook Signature Validation
- Webhook Signature Validation: Paramount. Always verify the signature using
VONAGE_WEBHOOK_SECRETas shown in the function handler. Never disable this check in production. Vonage uses SHA-256 HMAC for signature generation. - Environment Variables: Keep API keys, secrets, and signature secrets out of your codebase. Use the
.envfile and ensure it's in your.gitignore. Use your deployment platform's secret management for production. - Input Sanitization: While less critical for simply storing/relaying messages, sanitize message content appropriately if you use it elsewhere (displaying in UI, further processing) to prevent XSS or other injection attacks. Prisma helps prevent SQL injection at the database layer.
- Rate Limiting: Your webhook endpoint is publicly accessible. Consider adding rate limiting (e.g., using middleware or platform features) to prevent abuse, although Vonage itself has rate limits.
- Error Handling: Avoid leaking sensitive error details in HTTP responses. Log detailed errors internally.
- HTTPS Required: Vonage webhooks require HTTPS endpoints. Use
ngrokfor local development and ensure production deployments use HTTPS.
8. Error Handling and Logging
- Consistent Logging: Use Redwood's built-in
logger(import { logger } from 'src/lib/logger'). Log key events (webhook received, signature verified, message processed/sent) and errors. Use appropriate log levels (info,warn,error,debug). - Vonage API Errors: The Vonage SDK throws errors for API failures. Catch these errors in your service functions (
sendVonageMessage) and log relevant details (often found inerror.response.dataorerror.message). Refer to Vonage Messages API error codes for detailed descriptions. - Database Errors: Catch errors during Prisma operations (
createMessage,updateMessageStatus). Log details and decide how to respond (e.g., retry, notify admin, or ignore if non-critical like a duplicate status update). - Status Webhooks: Utilize the status webhook to track delivery success or failure of outbound messages. Update your database accordingly, including
errorCodeanderrorReason. Implement logic to handle specific error codes from Vonage if needed (e.g., number blocked, invalid number). - Retry Mechanisms: For transient errors (e.g., temporary network issues when calling Vonage API), implement a simple retry strategy with exponential backoff within your service function, potentially using a library like
async-retry. However, avoid blocking the webhook response. If sending fails, log it and potentially queue it for later retry via a background job system (like RedwoodJS Background Jobs). - Common Error Codes: Key Vonage error codes include 1020 (invalid params), 1170 (invalid phone number), 1320 (message already sent), 1360 (TTL expired), 1380 (invalid resource), 1420 (invalid sender), and 1430 (invalid recipient).
9. Production Deployment
Deploy your RedwoodJS inbound SMS webhook application following these key steps for Vonage integration:
-
Choose a Platform: Select a deployment provider supporting Node.js applications (e.g., Vercel, Netlify, Render, Fly.io, AWS Lambda). RedwoodJS provides deployment guides for popular platforms.
-
Function URL Pattern: After deployment, access your webhook function at
https://your-domain.com/.redwood/functions/vonageWebhook(or/api/vonageWebhookdepending on your deployment configuration). Update your Vonage Application's Inbound URL and Status URL with this production URL. -
Configure Environment Variables: Set these environment variables securely in your deployment platform's settings:
DATABASE_URL(pointing to your production database)VONAGE_API_KEYVONAGE_API_SECRETVONAGE_WEBHOOK_SECRETVONAGE_APPLICATION_IDVONAGE_NUMBERVONAGE_PRIVATE_KEY_PATH(if using JWT/Secure Media): For production, store the content of the private key directly in a secure environment variable (e.g.,VONAGE_PRIVATE_KEY_CONTENTbase64 encoded) instead of deploying the key file. This avoids file system complexities and enhances security. Adjust your SDK initialization logic (Section 5.2) to read the key content from the environment variable if the path variable isn't set. Example:typescript// In services/messages/messages.ts const credentials = new Auth({ 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 ? Buffer.from(process.env.VONAGE_PRIVATE_KEY_CONTENT, 'base64') : process.env.VONAGE_PRIVATE_KEY_PATH, // Fallback to file path for local dev })- Redwood's standard environment variables (like
SESSION_SECRETif using auth). Check RedwoodJS deployment docs for specifics.
-
Run Tests and Verification: Ensure production webhooks function correctly by:
- Monitoring logs for webhook events and errors.
- Verifying database records create correctly for inbound messages.
- Testing that status updates reflect in your database.
- Confirming outbound messages send successfully if reply functionality is enabled.
-
Production Monitoring: Set up monitoring and alerting for:
- Webhook endpoint availability (uptime monitoring).
- Failed signature validations (potential security issues).
- Vonage API errors (rate limits, authentication issues).
- Database connection failures.
- Use tools like Sentry, LogRocket, or your hosting platform's monitoring features.
Frequently Asked Questions
How do I receive inbound SMS messages with Vonage and RedwoodJS?
Create a RedwoodJS serverless function to handle webhook POST requests from Vonage. Configure your Vonage Application's Inbound URL to point to your function endpoint (e.g., https://your-domain.com/.redwood/functions/vonageWebhook). The function validates the signature, parses the message payload, and stores it in your Prisma database.
What is webhook signature validation and why is it critical?
Webhook signature validation verifies that incoming webhook requests genuinely came from Vonage and haven't been tampered with. Vonage signs each webhook using SHA-256 HMAC with your Signature Secret. Always validate signatures using the @vonage/server-sdk's Signature class. Never disable this check in production – it prevents unauthorized access and malicious payloads.
What's the difference between Vonage API Secret and Signature Secret?
The API Secret (VONAGE_API_SECRET) authenticates your API calls when sending messages to Vonage. The Signature Secret (VONAGE_WEBHOOK_SECRET) validates incoming webhooks from Vonage to your application. These are completely different values – do not confuse them. Generate the Signature Secret in your Vonage Application settings under "Signed webhooks."
How do I implement two-way SMS messaging with Vonage?
Store the sender's number from inbound webhooks and use the sendVonageMessage service function to reply. Set the sender's number as the to parameter and your Vonage number as from. The webhook payload includes a channel field – use the same channel (SMS or WhatsApp) for replies to ensure proper delivery.
How do I handle both SMS and WhatsApp messages in the same webhook?
The webhook payload includes a channel field indicating the message type (sms, whatsapp, etc.). Store this in your database and use it when sending replies. The webhook handler code differentiates message types automatically. For WhatsApp, ensure your Vonage number has WhatsApp Business API enabled.
What Node.js version does RedwoodJS v7 require?
RedwoodJS v7+ requires exactly Node.js 20.x (not 18.x or 19.x). Use node -v to verify your version. Install Node 20.x via nodejs.org or use a version manager like nvm. Additionally, use Yarn ≥1.22.21 (Classic Yarn 1.x).
How do I test Vonage webhooks locally during development?
Use ngrok to expose your local RedwoodJS API server (port 8911) to the internet with HTTPS. Run ngrok http 8911 to get a public HTTPS URL. Configure this URL plus /api/vonageWebhook in your Vonage Application's Inbound URL and Status URL settings. Send a test SMS to your Vonage number and verify the webhook arrives in your local logs.
What database indexes should I create for message storage?
Create these indexes in your Prisma schema: 1) @@index([vonageMessageId]) for fast UUID lookups (though @unique already creates an index), 2) @@index([direction, createdAt]) for filtering by inbound/outbound and sorting by timestamp. These optimize common query patterns for message retrieval and status updates.
How do I send automatic replies to inbound messages?
In your webhook handler function, after storing the inbound message, call the sendVonageMessage service function with the sender's number as the to parameter and your Vonage number as the from parameter. Use the same channel (SMS or WhatsApp) for the reply. The example code includes a commented-out auto-reply section you can enable.
What are Vonage's webhook timeout and retry policies?
Vonage requires a 200 OK response within 3 seconds for connection establishment and 15 seconds for the actual response. If your endpoint responds with HTTP 429 or 5xx codes, Vonage retries using exponential backoff starting at 5 seconds, doubling with each retry up to 15 minutes maximum, continuing for up to 24 hours before discarding the message. Process time-consuming operations asynchronously after sending the 200 response to avoid timeouts.
How do I handle Vonage error codes for failed messages?
Status webhooks include errorCode and errorReason fields when messages fail. Store these in your database via the updateMessageStatus service. Common error codes: 1020 (invalid params), 1170 (invalid/missing phone number), 1320 (message already sent), 1360 (TTL expired), 1380 (invalid resource), 1420 (invalid sender), 1430 (invalid recipient). Implement logic to handle specific codes as needed. See the complete list at Vonage Messages API errors.
Should I store the private key file in production deployments?
No. Store the private key content in an environment variable (base64 encoded) instead of deploying the file. Example: VONAGE_PRIVATE_KEY_CONTENT. Read it with Buffer.from(process.env.VONAGE_PRIVATE_KEY_CONTENT, 'base64') in your Auth initialization. This avoids file system complexities and improves security across deployment platforms.
What's the Prisma error code P2025 in status update failures? P2025 means "Record not found." This occurs when a status webhook arrives before the corresponding inbound message webhook processes (race condition). Handle this gracefully by logging a warning and returning null rather than throwing an error. The status update will succeed when retried or can be ignored if the message doesn't exist in your database yet.
Next Steps
You now have a fully functional RedwoodJS inbound SMS webhook application handling two-way messaging with Vonage. Consider these enhancements:
- User Interface: Build a web UI using RedwoodJS cells and pages to display message history
- Authentication: Add RedwoodJS authentication to secure your message management interface
- Advanced Routing: Implement message routing logic based on keywords, sender, or time of day
- Background Jobs: Use RedwoodJS Background Jobs for retry logic and asynchronous processing
- Multi-Channel: Expand to handle MMS, Facebook Messenger, or Viber messages
- Analytics: Track message volumes, response times, and delivery rates
Explore the RedwoodJS documentation and Vonage Messages API guides for more advanced features.
Frequently Asked Questions
How to receive SMS messages in RedwoodJS?
Receive SMS messages by configuring a Vonage virtual number to send webhooks to your RedwoodJS application. Set up a webhook endpoint in your RedwoodJS app using a serverless function and expose it to the internet using a tool like ngrok during development. This endpoint will receive inbound message data from Vonage whenever an SMS is sent to your virtual number. Make sure to verify Vonage's webhook signatures for security and store the message data using Prisma.
What is the Vonage Messages API used for?
The Vonage Messages API provides a unified way to send and receive messages across various channels, including SMS, MMS, WhatsApp, and Facebook Messenger. It simplifies the process of integrating messaging into your application by handling the complexities of different platforms.
Why does Vonage use webhook signatures?
Vonage uses webhook signatures to ensure the authenticity and integrity of incoming webhook requests. This cryptographic signature, generated using a shared secret, allows your application to verify that the request originated from Vonage and hasn't been tampered with, enhancing security.
When should I use ngrok with Vonage?
Use ngrok during local development to create a secure tunnel that exposes your locally running RedwoodJS server to the public internet. This allows Vonage to deliver webhooks to your application for testing even though it's not yet deployed to a production environment.
Can I send WhatsApp messages with RedwoodJS?
Yes, you can send and receive WhatsApp messages using RedwoodJS with the Vonage Messages API. After setting up your Vonage account and linking a WhatsApp-enabled number, you can use the Vonage Node.js Server SDK within your RedwoodJS services to send and receive messages via the API's unified endpoint.
How to set up Vonage webhooks in RedwoodJS?
Set up Vonage webhooks by creating a serverless function in your RedwoodJS application. This function will act as the webhook endpoint, receiving inbound and status update messages. Configure your Vonage application's inbound and status URLs to point to this function's publicly accessible URL, usually via ngrok during development.
What is Prisma used for with Vonage and Redwood?
Prisma serves as the Object-Relational Mapper (ORM) in your RedwoodJS application, facilitating interactions with your database. When integrated with Vonage, Prisma allows you to efficiently store details about incoming and outgoing messages, including sender, recipient, content, timestamps, and message status.
How to validate Vonage webhook signatures in RedwoodJS?
Validate Vonage webhook signatures by using the @vonage/server-sdk's Signature class and your Vonage webhook secret. Compare the signature in the 'X-Vonage-Signature' header with the one generated using your secret and the raw request body. This step is crucial for ensuring the request genuinely comes from Vonage.
How to handle Vonage message status updates?
Vonage sends status updates for each message, indicating delivery success, failure, or other events. Your RedwoodJS application should handle these status updates by implementing a webhook endpoint that receives these updates and logs the status. You should save these status updates to your database using a dedicated service function.
When to handle Vonage API errors?
Handle Vonage API errors within your RedwoodJS services, specifically when making calls to the Vonage API using the SDK. Implement robust error handling using try-catch blocks to catch potential errors during sending messages or other API interactions. Log the error details and implement appropriate retry mechanisms or user notifications.
What RedwoodJS service handles sending Vonage messages?
The RedwoodJS service responsible for sending Vonage messages should be named 'messages' and be located at 'api/src/services/messages/messages.ts'. This service should contain a function that utilizes the Vonage Node.js Server SDK to interact with the Vonage Messages API, allowing your application to send outbound messages.
How to store Vonage messages in a database?
Store Vonage messages by creating a 'Message' model in your Prisma schema ('api/db/schema.prisma'). Define fields to capture essential message details like ID, direction, channel, sender, recipient, content, status, timestamps, and potential error codes. Use RedwoodJS services to interact with Prisma and perform create, read, update, and delete operations on message records.
How to test Vonage webhooks locally?
Test Vonage webhooks locally by using ngrok to expose your RedwoodJS development server. Configure ngrok to forward requests to your webhook endpoint. Update your Vonage application settings to use the ngrok URL as your webhook URL. Send a test message to your Vonage number and observe logs in ngrok, your RedwoodJS console, and your database to verify functionality.
Why use TypeScript in a RedwoodJS Vonage project?
Using TypeScript in your RedwoodJS project when integrating Vonage enhances type safety and code maintainability. RedwoodJS supports TypeScript out-of-the-box, allowing you to define types for message data and API responses, which helps catch errors during development and improves overall code quality.