messaging channels

Sent logo
Sent TeamMar 8, 2026 / messaging channels / Article

How to Receive Inbound SMS with Vonage, Node.js, and Fastify (2025 Guide)

Step-by-step tutorial: Build a secure Node.js webhook endpoint using Fastify to receive inbound SMS messages via Vonage Messages API. Includes JWT signature verification, two-way messaging setup, and production deployment best practices.

Learn how to receive and process inbound SMS messages using Vonage Messages API, Node.js, and Fastify. This comprehensive tutorial covers webhook setup, JWT signature verification, and production deployment – everything you need to build secure two-way SMS messaging applications.

By the end of this guide, you'll have a production-ready webhook endpoint that securely receives SMS messages sent to your Vonage virtual number, validates their authenticity using JWT tokens, and processes them reliably. This foundation enables you to build chatbots, automated customer support systems, notification handlers, and interactive SMS applications.

What you'll learn: Fastify project setup, Vonage dashboard configuration, webhook security with signature verification, error handling strategies, database integration patterns, and production deployment best practices.

Source: Vonage Messages API Documentation | Fastify Official Documentation

Prerequisites: What You Need Before Starting

Before building your inbound SMS webhook, ensure you have:

  • Vonage Account: Sign up for a Vonage API account with API credits for SMS messaging
  • Development Environment: Node.js 20+ (LTS recommended), npm or yarn package manager, and a code editor
  • Vonage Virtual Number: Purchase a phone number with SMS capabilities from the Vonage dashboard (costs vary by region)
  • Local Testing Tool: Install ngrok for exposing your local server to receive webhooks during development
  • Basic Knowledge: Familiarity with Node.js, async/await patterns, REST APIs, and webhook concepts

Estimated costs: Vonage charges per SMS message received and sent. Inbound SMS typically costs $0.0050-$0.0075 per message (varies by country). Check Vonage pricing for your region.

How Do You Set Up a Fastify Project for Vonage SMS Webhooks?

Fastify offers 5-10% better performance than Express with lower memory overhead – ideal for high-traffic webhook endpoints. Its built-in JSON schema validation and Pino logging make it production-ready out of the box.

Initialize your Node.js project and install the necessary dependencies.

  1. Create Project Directory: Open your terminal and create a new directory for your project.

    bash
    mkdir vonage-fastify-inbound
    cd vonage-fastify-inbound
  2. Initialize Node.js Project: Create a package.json file to manage dependencies and project metadata.

    bash
    npm init -y

    (The -y flag accepts default settings.)

  3. Install Dependencies: Install Fastify for the web server, the Vonage SDK for signature verification, dotenv for environment variables, and @fastify/formbody to parse x-www-form-urlencoded data from webhooks.

    bash
    npm install fastify @vonage/server-sdk dotenv @fastify/formbody
  4. Create Project Structure: Create the basic files and folders.

    bash
    touch index.js .env .env-example .gitignore
    • index.js: Main application file where your Fastify server code lives
    • .env: Stores sensitive credentials (API keys, secrets). Never commit this file to Git.
    • .env-example: Template file showing required environment variables. Commit this file.
    • .gitignore: Specifies files and directories that Git should ignore
  5. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing dependencies and secrets.

    text
    # .gitignore
    
    node_modules/
    .env
    *.log
  6. Define Environment Variables (.env-example and .env): Populate .env-example with the variables needed. You'll get these values from the Vonage Dashboard.

    dotenv
    # .env-example
    
    # Vonage API Credentials
    VONAGE_API_KEY=YOUR_VONAGE_API_KEY
    VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
    VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
    # Optional but recommended for webhook security
    VONAGE_SIGNATURE_SECRET=YOUR_VONAGE_WEBHOOK_SIGNATURE_SECRET
    
    # Application Settings
    PORT=3000
    LOG_LEVEL=info

    Now copy .env-example to .env and fill in the actual values in .env as you obtain them.

    • VONAGE_API_KEY, VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.
    • VONAGE_APPLICATION_ID: Generated when you create a Vonage Application (covered later).
    • VONAGE_SIGNATURE_SECRET: Found in your Vonage API Settings when you enable signed webhooks (recommended, covered later).
    • PORT: Local port your Fastify server listens on (defaults to 3000).
    • LOG_LEVEL: Controls logging verbosity (e.g., info, debug, warn, error). The .env value overrides the code default.

    Why .env? Storing secrets in environment variables is a standard security practice. It separates configuration from code, making it easier to manage different environments (development, staging, production) and preventing accidental exposure of sensitive data in version control.

How Do You Implement the Inbound SMS Webhook Endpoint?

This is the core of the application – handling the incoming POST request from Vonage when someone sends an SMS to your virtual number.

  1. Load Environment Variables: At the top of index.js, load the variables from your .env file.

    javascript
    // index.js
    require('dotenv').config();
  2. Initialize Fastify: Import Fastify, create an instance with logging enabled, and register the form body parser.

    javascript
    // index.js
    const fastify = require('fastify')({
      logger: {
        level: process.env.LOG_LEVEL || 'info', // Use level from .env or default to 'info'
      }
    });
    
    // Register plugin to parse 'application/x-www-form-urlencoded' bodies
    fastify.register(require('@fastify/formbody'));
    • Why logger: { level: ... }? Fastify's built-in Pino logger is efficient and provides structured logging, crucial for debugging and monitoring. Setting the level via .env allows environment-specific verbosity.
    • Why @fastify/formbody? Vonage webhooks send data as x-www-form-urlencoded. This plugin automatically parses it into request.body.
  3. Define the Inbound Webhook Route: Create a POST route that listens for incoming messages from Vonage. Use /webhooks/inbound-sms as the path.

    javascript
    // index.js
    
    // Define the webhook endpoint for inbound SMS
    fastify.post('/webhooks/inbound-sms', async (request, reply) => {
      // Log the received request body for debugging
      fastify.log.info({ body: request.body }, 'Received inbound SMS webhook');
    
      // Basic validation: Check if essential fields exist
      // Vonage Messages API payload structure can vary slightly.
      // Check the documentation for the exact fields you need.
      const { msisdn, to, text, messageId } = request.body; // Common fields for SMS
    
      if (!msisdn || !to || !text || !messageId) {
          fastify.log.warn('Webhook received incomplete data:', request.body);
          // Respond with 400 Bad Request if critical data is missing
          return reply.status(400).send({ error: 'Missing required message fields' });
      }
    
      // --- Placeholder for Business Logic ---
      // Add your application's logic here:
      // - Store the message in a database
      // - Trigger a response SMS
      // - Call another API
      // - etc.
      fastify.log.info(`Processing message ${messageId} from ${msisdn}: "${text}"`);
      // --- End Placeholder ---
    
      // IMPORTANT: Acknowledge receipt to Vonage
      // Vonage expects a 200 OK response within a reasonable timeframe.
      // Failing to respond or responding with an error might cause Vonage to retry the webhook.
      reply.status(200).send();
    });
    • Why async (request, reply)? Using async/await makes handling asynchronous operations (like database calls you might add later) cleaner.
    • Why request.body? This object contains the parsed data sent by Vonage, thanks to @fastify/formbody. The exact properties (msisdn, to, text, messageId, etc.) depend on the Vonage API used (Messages API in this case) and the channel. Refer to the Vonage Messages API webhook reference for details.
    • Why fastify.log.info? Using the instance logger (fastify.log) ensures logs are structured and include request context if configured.
    • Why reply.status(200).send()? This is crucial. It tells Vonage you successfully received the webhook. Without this, Vonage might consider the delivery failed and retry, leading to duplicate processing.
  4. Start the Server: Add the code to start the Fastify server, listening on the configured port.

    javascript
    // index.js
    
    // Function to start the server
    const start = async () => {
      try {
        const port = process.env.PORT || 3000;
        await fastify.listen({ port: parseInt(port, 10), host: '0.0.0.0' });
        // Server is listening, log confirmation
      } catch (err) {
        fastify.log.error(err);
        process.exit(1);
      }
    };
    
    // Run the server
    start();
    • Why host: '0.0.0.0'? This makes the server accessible from outside the local machine (necessary for ngrok and deployment).
    • Why parseInt(port, 10)? Environment variables are strings; listen expects a number.
    • Why try...catch and process.exit(1)? This ensures graceful error handling during server startup. If the server fails to start (e.g., port already in use), it logs the error and exits cleanly.

How Do You Build a Complete API Layer?

For this use case (receiving inbound webhooks), the Fastify route /webhooks/inbound-sms is the API endpoint that Vonage interacts with. This guide doesn't build a separate API for other clients to call.

However, if you extend this application, you might add routes like:

  • /api/messages: Retrieve stored messages (requires database integration).
  • /api/send-sms: Trigger outbound SMS messages via the Vonage API (requires using the @vonage/server-sdk).
  • /health: A simple endpoint for health checks.

Authentication (e.g., API keys, JWT) is crucial for any new endpoints you expose, but the webhook endpoint itself relies on Vonage's mechanism (ideally signed webhooks, see Security section) for authenticity.

How Do You Configure Vonage to Send Webhooks to Your Fastify App?

Configure Vonage to send inbound SMS messages to your running application.

  1. Start Your Local Server: Run your Fastify application.

    bash
    node index.js

    You should see output indicating the server is listening (e.g., {"level":30,"time":...,"pid":...,"hostname":"...","msg":"Server listening at http://127.0.0.1:3000"}).

  2. Expose Local Server with ngrok: Open a new terminal window (leave the server running). Start ngrok to forward a public URL to your local port 3000.

    bash
    ngrok http 3000

    ngrok displays output like this:

    text
    Session Status                online
    Account                       Your Name (Plan: Free)
    Version                       x.x.x
    Region                        United States (us)
    Web Interface                 http://127.0.0.1:4040
    Forwarding                    https://<unique-code>.ngrok-free.app -> http://localhost:3000
    
    Connections                   ttl     opn     rt1     rt5     p50     p90
                                  0       0       0.00    0.00    0.00    0.00

    Copy the https://<unique-code>.ngrok-free.app URL. This is your temporary public base URL for webhooks during development. Note: This URL changes every time you restart ngrok (unless you have a paid plan with static domains). A stable, permanent URL is required for production (see Deployment section).

  3. Configure Vonage Application:

    • Log in to your Vonage API Dashboard.
    • Navigate to Applications > Create a new application.
    • Name: Give your application a descriptive name (e.g., Fastify Inbound SMS Handler).
    • Generate Public/Private Key: Click this button. A private.key file downloads. Save this file securely. Note: This private key is primarily used for generating JWTs for authenticating outbound API calls (e.g., sending SMS) and is not needed for verifying inbound webhook signatures using the signature secret as described in this guide.
    • Capabilities: Toggle Messages ON.
      • Inbound URL: Paste your ngrok Forwarding URL and append your webhook path: https://<unique-code>.ngrok-free.app/webhooks/inbound-sms
      • Status URL: Use the same URL for delivery receipts, or create a separate endpoint (/webhooks/status) if needed: https://<unique-code>.ngrok-free.app/webhooks/inbound-sms (or /webhooks/status)
    • Click Generate new application.
    • You're taken to the application's details page. Copy the Application ID and paste it into your .env file for VONAGE_APPLICATION_ID.
  4. Link Vonage Number:

    • On the application details page, scroll down to Linked numbers.
    • Click Link next to the Vonage virtual number you want to use for receiving SMS. If you don't have one, go to Numbers > Buy numbers to purchase one (ensure it has SMS capability).
  5. Configure API Settings (Webhook Signatures – Recommended):

    • Navigate to API Settings in the main dashboard menu.
    • Scroll down to Webhook signatures.
    • Select SHA-256 HMAC (Recommended) from the "Select signature algorithm" dropdown.
    • Click Save changes.
    • A Signature secret displays. Copy this secret and paste it into your .env file for VONAGE_SIGNATURE_SECRET.
    • Why Signature Verification? This ensures that incoming requests to your webhook actually came from Vonage and haven't been tampered with. It prevents unauthorized actors from sending fake messages to your endpoint.
  6. Ensure Messages API is Default for SMS (If Sending Later):

    • Still in API Settings, ensure under SMS Settings, the "Default SMS Setting" is set to Messages API. This is important if you plan to send SMS using the Messages API syntax later.
  7. Update .env: Make sure your .env file now contains the actual VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, and VONAGE_SIGNATURE_SECRET. Restart your Node.js server (Ctrl+C then node index.js) to load the new values.

How Do You Implement Error Handling and Logging?

Robust error handling ensures your application handles unexpected issues gracefully without crashing or losing data.

  1. Structured Logging: Fastify's default logger (Pino) is already structured (JSON). Log meaningful information, especially within catch blocks.

    javascript
    // Example within a route
    try {
      // Some operation that might fail
      // await potentiallyFailingOperation();
    } catch (error) {
      fastify.log.error({ err: error, messageId: request.body?.messageId }, 'Failed to process inbound message');
      // Decide if the client needs an error response or if 200 OK is still appropriate
      // If the error means Vonage should retry, send a 500.
      // If you handled the error locally and Vonage shouldn't retry, send 200.
      // Example: Send 500 to potentially trigger a retry from Vonage
      reply.status(500).send({ error: 'Internal server error during processing' });
      return; // Important to return after sending reply
    }
  2. Webhook Acknowledgement: As stressed before, always send a 200 OK unless you specifically want Vonage to retry (e.g., temporary internal failure). Log errors before sending the 200 OK if the failure doesn't require a retry.

  3. Retry Mechanisms (Vonage Side): Vonage has its own retry mechanism for webhooks that fail (non-2xx response or timeout). You don't typically need to implement retry logic within your webhook handler for the initial reception, but ensure you acknowledge receipt promptly. Retries might be needed if your handler calls other external services.

  4. Log Analysis: In production, use log management tools (like Datadog, Logz.io, ELK stack) to ingest, search, and analyze your structured logs for troubleshooting. Filter by messageId or msisdn to track specific message flows.

How Do You Create a Database Schema for SMS Messages?

Storing messages is a common requirement. While not implemented in this core guide, here's how to approach it:

  1. Choose a Database: PostgreSQL, MongoDB, MySQL, etc.

  2. Choose an ORM/Driver: Prisma (recommended for type safety and migrations), TypeORM, Sequelize, or native drivers (pg, mysql2, mongodb).

  3. Define Schema: Create a table/collection for messages.

    • Example (SQL-like pseudo-schema):

      sql
      CREATE TABLE inbound_messages (
          id SERIAL PRIMARY KEY, -- Or UUID
          vonage_message_id VARCHAR(255) UNIQUE NOT NULL, -- Vonage's ID
          sender_msisdn VARCHAR(50) NOT NULL,
          recipient_number VARCHAR(50) NOT NULL,
          message_text TEXT,
          received_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
          processed_at TIMESTAMP WITH TIME ZONE NULL,
          -- Add other relevant fields from the webhook payload
          raw_payload JSONB NULL -- Store the full webhook payload
      );
    • Entity Relationship Diagram (ERD): For this simple case, it's just one table. More complex apps might link messages to users, conversations, etc.

  4. Implement Data Access: Create functions (e.g., saveInboundMessage(messageData)) using your chosen ORM/driver to insert data into the database within your webhook handler.

  5. Migrations: Use the migration tool provided by your ORM (e.g., prisma migrate dev) to manage schema changes safely.

How Do You Add Security Features to Your Webhook?

Security is paramount for public-facing webhooks.

  1. Webhook Signature Verification (Implement): This is the most crucial security measure for webhooks. Vonage uses JSON Web Token (JWT) Bearer Authorization with HMAC-SHA256 signatures to authenticate webhook requests.

    Critical Security Requirements:

    • Minimum Secret Length: Use at least 32 bits for the signature secret to prevent brute-force attacks.
    • Token Expiration: JWT signatures expire 5 minutes after issuance. Verifying an expired JWT fails.
    • Time Synchronization: Use Network Time Protocol (NTP) to ensure accurate time synchronization on your server.
    • Raw Body Required: Verification must use the raw request body. Converting parsed JSON back to a string may produce different values, causing verification to fail.
    • Payload Hash Validation: Verify the payload hasn't been tampered with by comparing a SHA-256 hash of the payload to the payload_hash field in the JWT claims.

    Source: Vonage Webhook Signature Security | Validating Inbound Messages

    • Ensure the Vonage SDK is installed (npm install @vonage/server-sdk).
    • Import WebhookSignature and initialize necessary components.
    • Use the verifySignature method within your webhook handler.
    javascript
    // index.js
    require('dotenv').config();
    // Attempt to import WebhookSignature from the top level.
    // Note: The exact import path might vary depending on the @vonage/server-sdk version.
    // Check the SDK documentation if this import fails.
    const { Vonage, WebhookSignature } = require('@vonage/server-sdk');
    
    const fastify = require('fastify')({ logger: { level: process.env.LOG_LEVEL || 'info' } });
    fastify.register(require('@fastify/formbody'));
    
    // No API key/secret needed if only verifying signatures
    const vonage = new Vonage({}); // Empty constructor is fine for this
    
    const signatureSecret = process.env.VONAGE_SIGNATURE_SECRET;
    
    // Define the webhook endpoint for inbound SMS
    fastify.post('/webhooks/inbound-sms', async (request, reply) => {
        fastify.log.info('Received request for /webhooks/inbound-sms');
    
        // --- Security: Verify Signature ---
        if (!signatureSecret) {
            fastify.log.warn('VONAGE_SIGNATURE_SECRET not set. Skipping signature verification. This is insecure!');
            // Consider failing if security is critical:
            // return reply.status(500).send({ error: 'Server configuration error: Signature secret missing' });
        } else {
            try {
                // Extract JWT from "Bearer <jwt>" in the header (adjust if format differs)
                const signatureHeader = request.headers['x-vonage-signature'];
                const token = signatureHeader?.startsWith('Bearer ') ? signatureHeader.substring(7) : signatureHeader;
    
                if (!token) {
                   fastify.log.warn('Missing x-vonage-signature header or invalid format');
                   return reply.status(401).send({ error: 'Unauthorized: Missing signature' });
                }
    
                // --- Potential Issue: Raw vs Parsed Body ---
                // The verifySignature method might require the raw, unparsed request body
                // instead of the parsed `request.body` object provided by Fastify middleware.
                // This depends on the specific version of `@vonage/server-sdk`.
                // If verification fails unexpectedly, check the SDK documentation.
                // You might need to use a plugin like `fastify-raw-body` to access the raw body
                // and pass that buffer or string to verifySignature instead.
                // Example (if raw body needed): const isValid = WebhookSignature.verifySignature(request.rawBody, token, signatureSecret);
                // Test thoroughly to confirm what your SDK version expects.
                // For now, we assume the parsed body works based on common usage:
                const isValid = WebhookSignature.verifySignature(request.body, token, signatureSecret);
    
                if (!isValid) {
                    fastify.log.warn('Invalid webhook signature received.');
                    return reply.status(401).send({ error: 'Unauthorized: Invalid signature' });
                }
                fastify.log.info('Webhook signature verified successfully.');
    
            } catch (error) {
                fastify.log.error({ err: error }, 'Error during signature verification');
                // Check if the error is specifically a signature validation error vs. another processing error
                return reply.status(401).send({ error: 'Unauthorized: Signature check failed' });
            }
        }
         // --- End Security ---
    
        // Proceed with processing only if signature is valid (or verification skipped)
        fastify.log.info({ body: request.body }, 'Processing inbound SMS webhook (post-security)');
    
        const { msisdn, to, text, messageId } = request.body; // Common fields for SMS
         if (!msisdn || !to || !text || !messageId) {
             fastify.log.warn('Webhook received incomplete data (post-security):', request.body);
             return reply.status(400).send({ error: 'Missing required message fields' });
         }
    
         fastify.log.info(`Processing message ${messageId} from ${msisdn}: "${text}"`);
    
        // Acknowledge receipt
        reply.status(200).send();
    });
    
    // ... (start function definition) ...
    
    // Function to start the server
    const start = async () => {
      try {
        const port = process.env.PORT || 3000;
        await fastify.listen({ port: parseInt(port, 10), host: '0.0.0.0' });
        // Logging is handled by the listen method completion or the logger itself
      } catch (err) {
        fastify.log.error(err);
        process.exit(1);
      }
    };
    
    // Run the server
    start();
  2. HTTPS: Always use HTTPS for your webhook URLs. ngrok provides this automatically for development. In production, your hosting platform or reverse proxy (like Nginx) should handle SSL/TLS termination.

  3. Input Sanitization: If you process the text content further (e.g., display it in a web UI, use it in database queries), sanitize it to prevent Cross-Site Scripting (XSS) or SQL Injection attacks. Libraries like DOMPurify (for HTML context) or parameterized queries (for databases) are essential. For simply logging, sanitization is less critical but still good practice if logs might be viewed in sensitive contexts.

  4. Rate Limiting: Protect your endpoint from abuse or accidental loops by implementing rate limiting.

    bash
    npm install @fastify/rate-limit
    javascript
    // index.js (near other registrations)
    fastify.register(require('@fastify/rate-limit'), {
      max: 100, // Max requests per window per IP (default)
      timeWindow: '1 minute'
      // Consider more sophisticated keying if needed (e.g., based on Vonage ID)
    });

    Adjust max and timeWindow based on expected legitimate traffic from Vonage's IP ranges.

  5. Disable Unnecessary HTTP Methods: Your webhook only needs POST. While Fastify defaults to 404 for undefined routes/methods, explicitly ensuring only POST is handled for /webhooks/inbound-sms is good practice (which our fastify.post definition inherently does).

How Do You Handle Special Cases in SMS Processing?

  • Character Encoding: Vonage generally handles standard SMS encoding (GSM-7, UCS-2 for non-Latin characters). Ensure your application/database can store UTF-8 correctly if needed. The text field in the webhook payload should be decoded correctly by Vonage into a standard string format (usually UTF-8).
  • Concatenated Messages (Multipart SMS): Longer SMS messages are split by carriers and reassembled by Vonage before hitting your webhook. The webhook payload might contain information about this (e.g., concat-ref, concat-total, concat-part within the message object in some API versions). Your application usually receives the complete text after reassembly by Vonage, but be aware that carrier reassembly issues can occasionally occur (though rare).
  • Different Payload Formats: If using older Vonage APIs (like the legacy SMS API) or different channels (WhatsApp, Viber), the webhook payload structure differs significantly. Always consult the specific API documentation for the expected fields.
  • Time Zones: Timestamps in Vonage webhooks (timestamp field) are typically in UTC (ISO 8601 format). Store dates in UTC in your database and convert to the user's local time zone only when displaying or processing based on local time.

How Do You Optimize Fastify Performance for High Traffic?

For a simple inbound webhook logger, performance is unlikely to be an issue with Fastify. However, if your handler performs complex operations:

  • Asynchronous Processing: If processing takes significant time (e.g., calling external APIs, complex database operations, AI analysis), acknowledge the webhook (200 OK) immediately and perform the heavy lifting asynchronously. Use a message queue (like RabbitMQ, Redis Streams, Kafka, BullMQ) and background workers. This prevents Vonage webhook timeouts and makes your endpoint more resilient.
  • Caching: If the handler frequently fetches the same data based on webhook content (e.g., user profiles based on msisdn), implement caching using Redis or Memcached to reduce database load. Use a library like @fastify/caching.
  • Database Indexing: Ensure database columns used for lookups (e.g., vonage_message_id, sender_msisdn) are indexed appropriately.
  • Load Testing: Use tools like k6, artillery, or autocannon to simulate high webhook volume and identify bottlenecks before going to production. Test your asynchronous processing pipeline as well.

How Do You Add Monitoring and Observability?

  • Health Checks: Add a simple health check endpoint.

    javascript
    // index.js
    fastify.get('/health', async (request, reply) => {
      try {
        // Optionally add checks: database connectivity, queue status etc.
        // await checkDatabaseConnection();
        return { status: 'ok', timestamp: new Date().toISOString() };
      } catch (error) {
        fastify.log.error({ err: error }, 'Health check failed');
        reply.status(503).send({ status: 'error', reason: 'Service unavailable' });
      }
    });

    Configure your monitoring system (e.g., Kubernetes liveness/readiness probes, external uptime checker) to hit this endpoint.

  • Metrics: Use a library like fastify-metrics to expose application metrics (request latency, counts per route, error rates) in a Prometheus-compatible format. Use Prometheus and Grafana (or a cloud provider's monitoring service like CloudWatch, Datadog) to scrape and visualize these metrics. Create dashboards showing inbound message rate, error rate by status code, processing latency (especially if using async processing).

  • Error Tracking: Integrate with services like Sentry (@sentry/node, @sentry/fastify) or Datadog APM to capture and aggregate errors automatically, providing stack traces, request context, and environment details. Configure alerts for high error rates or specific critical error types (like signature validation failures).

  • Logging: As emphasized, structured logging (JSON) is key. Ensure logs are shipped to a centralized logging platform (e.g., ELK Stack, Splunk, Datadog Logs, CloudWatch Logs) for aggregation, searching, and analysis. Correlate logs using unique request IDs or the messageId.

What Are Common Troubleshooting Issues and Solutions?

  • Webhook Not Triggering:
    • Check ngrok/Tunnel: Is it running? Is the Forwarding URL exactly correct (HTTPS!) in the Vonage Application settings? Has the ngrok URL expired (free tier)? Is there network/firewall blocking?
    • Check Server: Is your node index.js server running without startup errors? Check server logs for crashes.
    • Check Vonage Dashboard: Look at the Logs section for errors related to your number or application webhook delivery attempts. Check if the number is correctly linked to the application handling messages.
    • Check URL Path: Ensure the path in Vonage (/webhooks/inbound-sms) exactly matches your Fastify route definition (fastify.post('/webhooks/inbound-sms', ...)). Case-sensitive!
  • Receiving Repeated Webhooks:
    • Check Response Code: Are you consistently sending a 200 OK response from your handler within Vonage's timeout window (usually a few seconds)? Any other status code (4xx, 5xx) or a timeout will likely cause Vonage to retry. Check your server logs for errors occurring before the reply.status(200).send() is called. Check for slow synchronous operations blocking the response.
  • Signature Verification Failures:
    • Check Secret: Ensure VONAGE_SIGNATURE_SECRET in .env exactly matches the secret in Vonage dashboard (no extra spaces/characters).
    • Check Time Sync: JWT tokens expire in 5 minutes. Server clock skew causes premature expiration. Use NTP to sync server time.
    • Raw Body Issue: Some SDK versions require raw request body for verification. If verification fails, install fastify-raw-body plugin and pass request.rawBody to verifySignature().
    • Check Header: Verify x-vonage-signature header exists and contains valid JWT token format.

Frequently Asked Questions

How do I receive inbound SMS with Vonage using Fastify?

Set up a Fastify POST endpoint to receive webhook requests from Vonage when SMS messages arrive. Install @vonage/server-sdk and @fastify/formbody, configure your webhook URL in the Vonage dashboard, link your virtual number to your application, and implement signature verification for security. The webhook receives the message data as form-encoded payload.

What version of Fastify works with Vonage webhooks?

Both Fastify v4 and v5 work with Vonage webhooks. Fastify v5 (latest) requires Node.js 20+ and offers 5–10% performance improvements. Fastify v4 supports Node.js 14+ but reaches end-of-life on June 30, 2025. Use Fastify v5 for new projects to ensure long-term support and better performance.

How do I verify Vonage webhook signatures with JWT?

Use the verifySignature() method from @vonage/server-sdk. Extract the Authorization header, pass the raw request body and signature secret to the verification function. The JWT contains signature claims with payload hash, timestamp, and expiration (5 minutes). Always verify signatures in production to prevent unauthorized webhook requests and ensure message integrity.

Why is my Vonage webhook not receiving messages?

Check five common issues: (1) ngrok or tunnel is running with correct HTTPS URL in Vonage dashboard, (2) your Fastify server is running without errors, (3) virtual number is linked to your application in Vonage dashboard, (4) API settings switched to "Messages API" (not SMS API), and (5) webhook URL path exactly matches your route definition (case-sensitive).

Can I use Fastify v4 with Vonage webhooks?

Yes, Fastify v4 works with Vonage webhooks and supports Node.js 14+. However, Fastify v4 reaches end-of-life on June 30, 2025. Plan to migrate to Fastify v5 before this date to receive security updates and new features. The migration is straightforward – primarily requires updating to Node.js 20+ and adjusting a few API changes.

How do I test Vonage webhooks locally?

Use ngrok to create a public HTTPS tunnel to your local Fastify server. Run ngrok http 3000, copy the HTTPS forwarding URL, paste it into your Vonage application's inbound message webhook URL field (add /webhooks/inbound-sms path), and send a test SMS to your Vonage number. Check your server logs for the webhook request.

What is the minimum secret length for Vonage webhook security?

Use at least 32 bits (4 bytes) for your signature secret to prevent brute-force attacks. Generate secrets using cryptographically secure random generators like crypto.randomBytes(32).toString('hex') in Node.js. Store secrets in environment variables, never commit them to version control, and rotate secrets periodically following security best practices.

How do I handle JWT token expiration errors?

Vonage JWT signatures expire 5 minutes after issuance. If verification fails with expiration errors, check your server's time synchronization using Network Time Protocol (NTP). Clock skew between your server and Vonage's servers causes premature expiration. Use ntpdate or similar tools to sync your server time, and monitor time drift in production environments.

Should I respond to webhooks synchronously or asynchronously?

Respond with 200 OK immediately (synchronously) and process SMS messages asynchronously using a job queue. Vonage expects responses within a few seconds – timeouts trigger retries. For complex processing (database writes, external API calls, business logic), acknowledge receipt immediately, then process in background workers. This prevents duplicate webhook deliveries and improves reliability.

How do I prevent duplicate webhook processing?

Check for duplicate messageId values before processing. Store processed message IDs in your database with a unique constraint, or use Redis with TTL for temporary deduplication. Vonage may retry webhooks if your server doesn't respond with 200 OK quickly enough, so idempotent processing prevents duplicate actions (duplicate database records, duplicate API calls, duplicate user notifications).

What database schema should I use for inbound SMS messages?

Store essential fields: messageId (unique identifier), from (sender phone number in E.164), to (your Vonage number), text (message content), timestamp (message receive time), channel (SMS/MMS/etc), and processedAt (processing timestamp). Add indexes on messageId (unique), from, and timestamp for efficient queries. Consider partitioning by date for high-volume applications.

How do I optimize Fastify performance for high SMS traffic?

Enable clustering to use all CPU cores (Node.js cluster module or PM2), implement connection pooling for databases, use Redis for caching and rate limiting, enable Fastify's schema validation for faster request parsing, compress responses with @fastify/compress, and monitor with health checks and metrics. Deploy behind a load balancer and scale horizontally for very high traffic.