code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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:

  1. Inbound SMS webhook handler that receives messages sent to your Vonage virtual number
  2. Webhook signature validation using Vonage signatures for security
  3. Message storage with Prisma database (sender, recipient, content, timestamp, delivery status)
  4. Two-way messaging capability to send replies back to original senders
  5. 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:

text
+-------------------+      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.

  1. Create the Redwood App: Open your terminal and run:

    bash
    yarn create redwood-app ./vonage-redwood --typescript

    This scaffolds a new RedwoodJS project in vonage-redwood with TypeScript enabled.

  2. Navigate into the Project:

    bash
    cd vonage-redwood
  3. Initialize Environment Variables: RedwoodJS uses .env for environment variables. Create one in the project root:

    bash
    touch .env

    Populate this file later with Vonage credentials. Redwood automatically loads variables from .env into process.env.

  4. Install Vonage SDK: Install the Vonage Node.js Server SDK to interact with the API and validate webhooks in the api workspace:

    bash
    yarn 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).
  5. Verify Setup: Start the development server to ensure the basic Redwood app works:

    bash
    yarn rw dev

    You'll see output indicating both frontend (web) and backend (api) servers running on http://localhost:8910 and http://localhost:8911 respectively. 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.

  1. Log in to Vonage Dashboard: Access your Vonage API Dashboard.

  2. 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 .env file:
      dotenv
      # .env
      VONAGE_API_KEY=YOUR_API_KEY
      VONAGE_API_SECRET=YOUR_API_SECRET
      (Replace YOUR_API_KEY and YOUR_API_SECRET with your actual credentials)
  3. 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 .env file:
      dotenv
      # .env
      VONAGE_API_KEY=YOUR_API_KEY
      VONAGE_API_SECRET=YOUR_API_SECRET
      VONAGE_WEBHOOK_SECRET=YOUR_SIGNATURE_SECRET
      (Replace YOUR_SIGNATURE_SECRET with 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 .env file:
      dotenv
      # .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 line
      (Replace YOUR_APPLICATION_ID with 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.key file 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
  4. 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:
      dotenv
      # .env
      # ... other vars
      VONAGE_NUMBER=YOUR_VONAGE_NUMBER # e.g., 14155550100
      (Replace YOUR_VONAGE_NUMBER with your linked number)
  5. Configure Webhooks (Requires ngrok): Configure the actual webhook URLs in the "Local Development & Testing" section once you have your Redwood function endpoint and an ngrok tunnel running.


3. Creating the Database Schema with Prisma

Define a Prisma model to store received and sent messages for your inbound SMS webhook application.

  1. Define the Message Model: Open api/db/schema.prisma and 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. The vonageMessageId already has a unique constraint which creates an index, but explicit indexes on direction and createdAt improve filtering performance.
    • Other fields map directly to Vonage message properties.
  2. Apply Migrations: Run the Prisma migrate command to create/update the database schema and generate the Prisma client.

    bash
    yarn rw prisma migrate dev

    Follow the prompts (e.g., provide a name for the migration like "add message model"). This creates/updates the Message table 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.

  1. Generate the Function: Use the Redwood CLI to generate a new function called vonageWebhook.

    bash
    yarn rw g function vonageWebhook --no-graphql

    This creates api/src/functions/vonageWebhook.ts. The --no-graphql flag indicates it's a standard HTTP endpoint, not part of the GraphQL API.

  2. Implement the Handler Logic: Open api/src/functions/vonageWebhook.ts and 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's Signature class. 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...catch blocks log errors using Redwood's built-in logger. For production, implement more robust error tracking. Error details from status webhooks pass to the service.
    • 200 OK Response: Vonage requires a 200 OK response 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 sendVonageMessage service function.

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.

  1. Generate the Service:

    bash
    yarn rw g service message

    This creates api/src/services/messages/messages.ts and related test/scenario files.

  2. Implement Service Logic: Open api/src/services/messages/messages.ts and 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 using db.message.create to save message data.
    • updateMessageStatus: Uses db.message.update to find a message by its unique vonageMessageId and update its status, errorCode, and errorReason. 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). Ensures errorCode stores 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, and channel.
      • Logs the response from Vonage, which includes the messageUuid.
      • Optionally creates a corresponding outbound record 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.

6. Local Development & Testing with ngrok

Expose your Redwood development server to the public internet using ngrok to test your inbound SMS webhook locally.

  1. Start Redwood Dev Server: Open a terminal in your project root and run:

    bash
    yarn rw dev

    Note the API server port (usually 8911).

  2. Start ngrok: Open a second terminal and run ngrok, pointing it to your Redwood API server's port:

    bash
    ngrok http 8911
    • ngrok displays forwarding URLs (e.g., https://<random-subdomain>.ngrok-free.app). Copy the https URL.
    • The full URL for Vonage is this ngrok HTTPS URL plus the path to your function: /api/vonageWebhook.
  3. Update Vonage Webhook URLs:

    • Return to your Vonage Application settings in the dashboard.
    • Paste the full ngrok HTTPS 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.
  4. Send a Test Message:

    • Using your mobile phone, send an SMS (or WhatsApp message, if configured) to your Vonage virtual number.
  5. Verify:

    • ngrok Console: Check the ngrok terminal window for a POST /api/vonageWebhook request with a 200 OK response.
    • Redwood Console: Check the terminal running yarn rw dev for logs from the vonageWebhook function:
      • "Vonage webhook received"
      • "Vonage signature verified successfully."
      • "Processing Inbound Message Webhook"
      • Logs from the createMessage service.
      • If auto-reply is enabled: "Attempting to send Vonage message", "Message sent via Vonage…", logs from createMessage for the outbound record.
    • Database: Check your Message table (e.g., using yarn 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 ngrok URL in Vonage (HTTPS, correct path /api/vonageWebhook).
    • Ensure ngrok is still running. Free ngrok sessions 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. Ensure VONAGE_WEBHOOK_SECRET is correct. Sometimes proxies (including ngrok under 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_SECRET as 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 .env file 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 ngrok for 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 in error.response.data or error.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 errorCode and errorReason. 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:

  1. 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.

  2. Function URL Pattern: After deployment, access your webhook function at https://your-domain.com/.redwood/functions/vonageWebhook (or /api/vonageWebhook depending on your deployment configuration). Update your Vonage Application's Inbound URL and Status URL with this production URL.

  3. Configure Environment Variables: Set these environment variables securely in your deployment platform's settings:

    • DATABASE_URL (pointing to your production database)
    • VONAGE_API_KEY
    • VONAGE_API_SECRET
    • VONAGE_WEBHOOK_SECRET
    • VONAGE_APPLICATION_ID
    • VONAGE_NUMBER
    • VONAGE_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_CONTENT base64 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_SECRET if using auth). Check RedwoodJS deployment docs for specifics.
  4. 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.
  5. 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.