code examples
code examples
Build Production-Ready Vonage WhatsApp Integration with Fastify & Node.js (2024)
Complete guide to building a Vonage WhatsApp integration using Fastify and Node.js. Learn webhook setup, JWT security, rate limits, and production deployment with code examples.
Build Production-Ready Vonage WhatsApp Integration with Fastify & Node.js
Build a production-ready Node.js application using Fastify to send and receive WhatsApp messages via the Vonage Messages API. You'll implement webhook handling, JWT security, error management, and learn critical production considerations including Meta's rate limits and messaging tiers.
Your Project Goal: Create a robust Fastify backend service that:
- Receives incoming WhatsApp messages sent to your Vonage number via webhooks
- Replies to incoming messages automatically
- Securely validates webhook requests using JWT signatures
- Provides a foundation for complex WhatsApp bot interactions
Why Use This Approach?
- Fastify: A high-performance, low-overhead Node.js web framework focused on developer experience and speed. Its plugin architecture and built-in features like logging and schema validation make it excellent for building robust APIs.
- Vonage Messages API: A unified API for sending and receiving messages across multiple channels, including WhatsApp, SMS, MMS, Facebook Messenger, and Viber. You'll use its WhatsApp capabilities via the official Vonage Node SDK.
- Vonage WhatsApp Sandbox: Enables rapid development and testing without needing a dedicated WhatsApp Business Account initially.
- Webhook Architecture: The standard mechanism for real-time communication from platforms like Vonage to your application.
System Architecture:
graph LR
User[WhatsApp User] -- 1. Sends Message --> WhatsApp
WhatsApp -- 2. Forwards to Vonage --> Vonage[Vonage Platform]
Vonage -- 3. Sends Inbound Webhook --> Ngrok[(ngrok Tunnel)]
Ngrok -- 4. Forwards Request --> App[Fastify App]
App -- 5. Processes & Sends Reply --> Vonage
Vonage -- 6. Sends Status Webhook --> Ngrok
Ngrok -- 7. Forwards Request --> App
Vonage -- 8. Delivers Reply --> WhatsApp
WhatsApp -- 9. Displays Reply --> User
style App fill:#f9f,stroke:#333,stroke-width:2px
style Ngrok fill:#ccf,stroke:#333,stroke-width:2px
style Vonage fill:#aaf,stroke:#333,stroke-width:2pxPrerequisites:
<!-- GAP: Missing commands to verify Node.js version (node -v) and upgrade instructions (Type: Substantive) -->- Node.js: Version 20.x or 22.x LTS installed. This guide uses Fastify v5, which requires Node.js 20 or newer. If you're using Node.js 18 or earlier, you'll need to use Fastify v4 (supported until June 30, 2025) or upgrade your Node.js version. Source: Fastify LTS Documentation
- npm or yarn: Node.js package manager.
- Vonage API Account: Sign up for free at vonage.com if you don't have one. You'll get free credit to start.
- WhatsApp Account: A personal WhatsApp account on a smartphone for testing.
- ngrok: A tool to expose your local development server to the internet. Download and install it from ngrok.com. You'll need to sign up for a free account to get an authentication token. As of 2023, ngrok provides one free static domain per account, eliminating the need to update webhook URLs on every restart. Source: ngrok Agent Documentation
Final Outcome: A running Fastify application on your local machine, connected to the Vonage WhatsApp Sandbox via ngrok, capable of receiving WhatsApp messages and sending automated replies.
1. Setting Up the Project
Initialize your Node.js project using Fastify.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
bashmkdir fastify-vonage-whatsapp cd fastify-vonage-whatsapp -
Initialize Node.js Project: Create a
package.jsonfile.bashnpm init -y -
Install Dependencies: Install Fastify, its environment variable handler (
@fastify/env), sensible defaults (@fastify/sensible), and the Vonage SDK components.bashnpm install fastify @fastify/env @fastify/sensible @vonage/server-sdk @vonage/messages @vonage/jwtfastify: The core web framework@fastify/env: Loads and validates environment variables from a.envfile@fastify/sensible: Provides sensible defaults for error handling and security headers@vonage/server-sdk: The main Vonage SDK package@vonage/messages: Specifically for using the Messages API@vonage/jwt: Verifies webhook signatures
-
Project Structure: Create the following basic structure:
textfastify-vonage-whatsapp/ ├── src/ │ ├── hooks/ │ ├── routes/ │ ├── services/ │ └── server.js ├── .env ├── .gitignore ├── package.json └── private.key # Added via Vonage setup, ensure it's gitignoredsrc/: Contains your application source codesrc/hooks/: Fastify request lifecycle hooks (like authentication/validation)src/routes/: API endpoints (webhooks)src/services/: Business logic for interacting with the Vonage APIsrc/server.js: Main application entry point where you configure and start the Fastify server.env: Stores sensitive configuration and API keys (you'll create this later).gitignore: Specifies files/directories Git should ignoreprivate.key: The private key downloaded from Vonage (ensure it's listed in.gitignore)
-
Create
.gitignore: Add common Node.js ignores plus your sensitive files:plaintext# .gitignore # Node node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* pids/ *.pid *.seed *.log *.log.* # Environment Variables .env* !/.env.example # Vonage Private Key private.key # OS generated files .DS_Store Thumbs.db -
Basic Fastify Server (
src/server.js): Create a minimal Fastify server to ensure your setup works.javascript// src/server.js 'use strict'; const Fastify = require('fastify'); // Instantiate Fastify with basic logging const fastify = Fastify({ logger: true, // Use true for basic Pino logging, or configure logger options }); // Declare a simple health check route fastify.get('/health', async (request, reply) => { return { status: 'ok' }; }); // Run the server const start = async () => { try { // For now, hardcode the port. You'll use environment variables later. await fastify.listen({ port: 8000, host: '0.0.0.0' }); fastify.log.info(`Server listening on ${fastify.server.address().port}`); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); -
Run the Basic Server: Execute the server file.
bashnode src/server.jsYou should see log output indicating your server is listening on port 8000. Test the health check endpoint using
curl http://localhost:8000/healthin another terminal window. Stop the server withCtrl+C.
2. Vonage Configuration
Configure your Vonage account and application to work with the Messages API and WhatsApp Sandbox.
- Create a Vonage Application:
- Log in to your Vonage API Dashboard.
- Navigate to Applications → Create a new application.
- Give your application a name (e.g., "Fastify WhatsApp Demo").
- Generate Public/Private Key Pair: Click the Generate public and private key button. This downloads a
private.keyfile. Save this file securely. Place it in your root directory (fastify-vonage-whatsapp/private.key). You addedprivate.keyto.gitignoreto prevent accidentally committing it. Warning: While gitignored, storing private keys directly within your application's directory structure is generally discouraged for security reasons, even locally. Store it outside the project folder if your local setup allows. Under no circumstances should this key ever be committed to version control. The public key is automatically associated with your application in the dashboard. - Enable Capabilities: Scroll down to Capabilities and toggle Messages ON. You'll see fields for Inbound URL and Status URL. Fill these in the next section after setting up ngrok.
- Click Create application.
- Note Your Application ID: After creation, you'll see your application's page. Copy the Application ID – you'll need it for your environment variables.
- Set Up Vonage WhatsApp Sandbox:
- In the Vonage Dashboard sidebar, navigate to Messages API → Sandbox.
- Read the instructions. Add the Vonage Sandbox contact number to your phone's contacts and send it a specific message via WhatsApp to opt-in your number for testing.
- Add the Sandbox Number: The number is displayed prominently (often
+14157386102, but always verify the current number shown on your specific Sandbox page as it can change). Save it as a contact (e.g., "Vonage Sandbox"). - Send Opt-in Message: Open WhatsApp on your phone, find the Vonage Sandbox contact, and send it the exact opt-in phrase shown on the Sandbox page (it includes a unique keyword).
- Allowlist Your Number: Once Vonage receives your message, your number appears in the Allowlisted testing numbers section on the Sandbox page. Only numbers listed here can interact with the Sandbox.
- Configure Sandbox Webhooks: Similar to the application, the Sandbox page has fields for Inbound webhook URL and Status webhook URL. Configure these along with the application webhooks in the next step. For the Sandbox to work correctly, both the Application and the Sandbox webhooks need to be set.
-
Configure Environment Variables (
.env): Create a file named.envin your project root (fastify-vonage-whatsapp/.env). Add the following variables, replacing the placeholders with your actual credentials:dotenv# .env # Server Configuration PORT=8000 HOST=0.0.0.0 # Listen on all available network interfaces # Vonage API Credentials # Found on the Vonage API Dashboard homepage VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET # Vonage Application Details # Found in Your Applications -> [Your Application Name] VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID # Path relative to the project root where you saved the private key VONAGE_PRIVATE_KEY=./private.key # Vonage WhatsApp Sandbox Number # Found on the Messages API -> Sandbox page. **VERIFY THE NUMBER ON YOUR DASHBOARD**. Do not include '+'. VONAGE_WHATSAPP_NUMBER=YOUR_SANDBOX_NUMBER_FROM_DASHBOARD # Vonage Signature Secret # Found in Vonage Dashboard -> Settings -> API settings -> Default Signing Secret for API Key VONAGE_API_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET # Vonage API Host (Use Sandbox URL for testing) VONAGE_API_HOST=https://messages-sandbox.nexmo.comExplanation of Variables:
PORT,HOST: Configure where your Fastify server listens.0.0.0.0is important for containerized environments or VMs.VONAGE_API_KEY,VONAGE_API_SECRET: Your main Vonage account credentials, found directly on the dashboard landing page after logging in.VONAGE_APPLICATION_ID: The unique ID for the Vonage Application you created.VONAGE_PRIVATE_KEY: The path to theprivate.keyfile you downloaded. The path./private.keyassumes you run thenode src/server.jscommand from the project's root directory (fastify-vonage-whatsapp/). If running from a different directory, adjust the path accordingly or use an absolute path during development (though this is less portable).VONAGE_WHATSAPP_NUMBER: The specific phone number assigned to the Vonage WhatsApp Sandbox. Important: Verify the current number on your Vonage dashboard and use it without a leading+or00.VONAGE_API_SIGNATURE_SECRET: Used by Vonage to sign webhook requests with a JWT. Verify this signature to ensure requests genuinely came from Vonage. Find this in your main dashboard Settings page under API settings.VONAGE_API_HOST: Specifies the API endpoint URL. For testing with the Sandbox, usehttps://messages-sandbox.nexmo.com. For production with a WhatsApp Business Account, use the standard production URL (https://api.nexmo.com).
3. Configure Webhooks with ngrok
Vonage needs a publicly accessible URL to send webhook events (incoming messages and status updates) to your application running locally. ngrok creates a secure tunnel from the internet to your machine. While other tunneling services like localtunnel or Cloudflare Tunnels exist, this guide uses ngrok due to its popularity and ease of use for development.
-
Start ngrok: Open a new terminal window (keep your Fastify server terminal separate). Run ngrok, telling it to forward traffic to the port your Fastify app is listening on (Port 8000).
bash# Make sure you're logged into ngrok for longer sessions: ngrok config add-authtoken YOUR_TOKEN ngrok http 8000
-
Copy the ngrok Forwarding URL: ngrok displays output similar to this:
Forwarding https://<random-string>.ngrok-free.app -> http://localhost:8000Copy the
https://...URL (yours will have a different random string). This is your public URL. Note: Free ngrok URLs change every time you restart ngrok unless you use the free static domain feature. -
Configure Vonage Webhooks:
- Go back to your Vonage Application settings in the dashboard (Applications → Your Application).
- Under the Messages capability:
- Set Inbound URL:
YOUR_NGROK_URL/webhooks/inbound(e.g.,https://<random-string>.ngrok-free.app/webhooks/inbound) - Set Status URL:
YOUR_NGROK_URL/webhooks/status(e.g.,https://<random-string>.ngrok-free.app/webhooks/status)
- Set Inbound URL:
- Click Save changes.
- Now go to the Vonage Messages API Sandbox page in the dashboard.
- Enter the exact same URLs in the webhook fields:
- Inbound webhook URL:
YOUR_NGROK_URL/webhooks/inbound - Status webhook URL:
YOUR_NGROK_URL/webhooks/status
- Inbound webhook URL:
- Click Save webhooks.
Why
/webhooks/inboundand/webhooks/status? These are the specific endpoints you'll create in your Fastify application to handle these two types of events. It's crucial that both the Application and Sandbox webhooks point to your ngrok tunnel.
4. Implementing Core Functionality (Fastify Adaptation)
Now, let's write the Fastify code to handle the webhooks and send replies.
-
Load Environment Variables (
src/server.js): Use@fastify/envto load and validate variables from.env.javascript// src/server.js 'use strict'; const Fastify = require('fastify'); const sensible = require('@fastify/sensible'); const fastifyEnv = require('@fastify/env'); const path = require('path'); // Needed for path resolution // Define schema for environment variables const schema = { type: 'object', required: [ 'PORT', 'HOST', 'VONAGE_API_KEY', 'VONAGE_API_SECRET', 'VONAGE_APPLICATION_ID', 'VONAGE_PRIVATE_KEY', // Path or content 'VONAGE_WHATSAPP_NUMBER', 'VONAGE_API_SIGNATURE_SECRET', 'VONAGE_API_HOST', ], properties: { PORT: { type: 'string', default: 8000 }, HOST: { type: 'string', default: '0.0.0.0' }, VONAGE_API_KEY: { type: 'string' }, VONAGE_API_SECRET: { type: 'string' }, VONAGE_APPLICATION_ID: { type: 'string' }, VONAGE_PRIVATE_KEY: { type: 'string' }, // Can be path or key content VONAGE_WHATSAPP_NUMBER: { type: 'string' }, VONAGE_API_SIGNATURE_SECRET: { type: 'string' }, VONAGE_API_HOST: { type: 'string' }, }, }; const envOptions = { confKey: 'config', // Access environment variables via `fastify.config` schema: schema, dotenv: true, // Load .env file }; const fastify = Fastify({ logger: { level: 'info', // Default log level // Pretty print logs in development, use structured JSON in production transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, } : undefined, }, }); // Register plugins fastify .register(fastifyEnv, envOptions) .register(sensible) .register(require('./routes/webhooks')); // Register routes AFTER env // Start function remains the same, but uses config const start = async () => { try { // Wait for plugins to load, especially fastifyEnv await fastify.ready(); await fastify.listen({ port: fastify.config.PORT, host: fastify.config.HOST, }); // Note: fastify.server.address() might be null until listen completes // We log the port from config directly now fastify.log.info( `Server listening on host ${fastify.config.HOST} port ${fastify.config.PORT}` ); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); // Export fastify instance if needed for testing module.exports = fastify;- We define a JSON schema for expected environment variables.
@fastify/envwill throw an error on startup if required variables are missing. - We configure Pino (Fastify's logger) for pretty printing during development (
NODE_ENV !== 'production'). fastify.ready()ensures plugins like@fastify/envare loaded before we accessfastify.config.- Routes are registered after core plugins like
fastifyEnv.
- We define a JSON schema for expected environment variables.
-
Vonage Service (
src/services/vonageService.js): Create a service to encapsulate Vonage client initialization and message sending logic, handling private key as path or content.javascript// src/services/vonageService.js 'use strict'; const { Vonage } = require('@vonage/server-sdk'); const { WhatsAppText } = require('@vonage/messages'); const path = require('path'); const fs = require('fs'); let vonageClient; function initializeVonageClient(config, logger) { if (!vonageClient) { let privateKeyContent = config.VONAGE_PRIVATE_KEY; // Check if the variable content looks like a path vs the actual key const looksLikePath = /[\\/]|(\.key$)/.test(privateKeyContent); const looksLikePEMKey = privateKeyContent.startsWith('-----BEGIN PRIVATE KEY-----'); if (looksLikePath && !looksLikePEMKey) { // Assume it's a path, try to read the file const privateKeyPath = path.resolve(privateKeyContent); // Resolve relative to CWD try { privateKeyContent = fs.readFileSync(privateKeyPath, 'utf8'); logger.info(`Read private key from path: ${privateKeyPath}`); } catch (err) { logger.error(`Failed to read private key from configured path: ${privateKeyPath}`); logger.error(err); // If reading fails, check if maybe it WAS the key content after all but looked like a path if (config.VONAGE_PRIVATE_KEY.startsWith('-----BEGIN PRIVATE KEY-----')) { logger.warn('VONAGE_PRIVATE_KEY looked like a path but failed to read; attempting to use content directly.'); // Reset to use original content privateKeyContent = config.VONAGE_PRIVATE_KEY; } else { throw new Error('Could not read VONAGE_PRIVATE_KEY file and content does not look like a PEM key. Check path/permissions or provide key content directly.'); } } } else if (looksLikePEMKey) { // Assume it's the key content itself logger.info('Using private key directly from VONAGE_PRIVATE_KEY environment variable content.'); } else { // Content is ambiguous or invalid logger.error('VONAGE_PRIVATE_KEY content is neither a readable path nor does it look like a PEM private key.'); throw new Error('Invalid VONAGE_PRIVATE_KEY configuration.'); } // Now initialize Vonage with the resolved privateKeyContent vonageClient = new Vonage( { apiKey: config.VONAGE_API_KEY, apiSecret: config.VONAGE_API_SECRET, applicationId: config.VONAGE_APPLICATION_ID, privateKey: privateKeyContent, // Use the resolved key content here }, { apiHost: config.VONAGE_API_HOST, // Use the sandbox or production host logger: logger.child({ service: 'vonage-sdk' }), // Pass a child logger for context } ); logger.info('Vonage client initialized.'); } return vonageClient; } async function sendWhatsAppReply(to_number, text, config, logger) { const client = initializeVonageClient(config, logger); // Ensure client is initialized const from_number = config.VONAGE_WHATSAPP_NUMBER; logger.info(`Attempting to send WhatsApp message from ${from_number} to ${to_number}`); try { const response = await client.messages.send( new WhatsAppText({ from: from_number, to: to_number, text: text, }) ); logger.info( { msgUuid: response.messageUUID }, `Message sent successfully` ); return response; } catch (error) { logger.error('Error sending WhatsApp message via Vonage:'); // Log detailed error information if available if (error.response && error.response.data) { logger.error( { vonageError: error.response.data, statusCode: error.response.status }, `Vonage API Error` ); } else { logger.error(error); } // Re-throw or handle appropriately for the caller throw new Error('Failed to send WhatsApp message.'); } } module.exports = { initializeVonageClient, sendWhatsAppReply, };- Detects if
VONAGE_PRIVATE_KEYis a path or the key content. - Reads the file content if it looks like a path.
- Uses the content directly if it looks like a PEM key.
- Initializes the
Vonageclient using configuration values and the resolved key content. - Uses the specific
apiHostfrom the config. - Includes detailed error logging.
- Passes a child logger to the Vonage SDK for better context.
- Detects if
-
JWT Signature Verification Hook (
src/hooks/verifyVonageSignature.js): Create a Fastify hook to verify theAuthorizationheader on incoming status webhooks.javascript// src/hooks/verifyVonageSignature.js 'use strict'; const { verifySignature } = require('@vonage/jwt'); const fp = require('fastify-plugin'); async function verifyVonageSignatureHook(fastify, options) { // Decorate fastify instance with the verification function // so it can be easily used in route options (preHandler) fastify.decorate('verifyVonageSignature', async (request, reply) => { fastify.log.trace('Attempting Vonage JWT signature verification...'); // Use trace for verbose steps const authHeader = request.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { fastify.log.warn('Missing or invalid Authorization header for webhook'); reply.code(401).send({ error: 'Unauthorized: Missing or invalid JWT Bearer token' }); return; // Stop further execution } const token = authHeader.split(' ')[1]; const secret = fastify.config.VONAGE_API_SIGNATURE_SECRET; if (!secret) { fastify.log.error('VONAGE_API_SIGNATURE_SECRET is not configured. Cannot verify webhook signature.'); reply.code(500).send({ error: 'Internal Server Error: Signature secret not configured' }); return; } try { // Note: verifySignature throws on error in some versions, returns bool in others. // Let's assume it returns boolean for safety. const isValid = verifySignature(token, secret); if (!isValid) { fastify.log.warn('Invalid Vonage JWT signature received.'); reply.code(403).send({ error: 'Forbidden: Invalid signature' }); return; // Stop further execution } fastify.log.info('Vonage JWT signature verified successfully.'); } catch (err) { // Catch potential errors from the verifySignature function itself fastify.log.error({ err }, 'Error during JWT signature verification process'); reply.code(500).send({ error: 'Internal Server Error during JWT verification' }); return; // Stop further execution } }); } // Export as a plugin module.exports = fp(verifyVonageSignatureHook);- Uses
fastify-pluginto properly encapsulate the hook/decorator. - Defines a decorator
verifyVonageSignaturewhich contains the verification logic. - Extracts the JWT from the
Authorization: Bearer <token>header. - Uses
@vonage/jwt'sverifySignaturefunction with the secret fromfastify.config. - Sends appropriate error responses (
401,403,500) if verification fails or config is missing. - Security Note: Vonage uses HMAC-SHA256 to sign webhook JWTs. The signature expires 5 minutes after issuance, and verifying an expired JWT will result in verification failure. The signing secret should be at least 32 bits to prevent brute-force attacks. For additional security when using HTTP (not HTTPS), you can verify the
payload_hashclaim in the JWT matches a SHA-256 hash of the request payload to detect tampering. However, Transport Layer Security (TLS/HTTPS) prevents man-in-the-middle attacks, making payload hash verification optional for HTTPS connections. Source: Vonage Webhook Security Documentation
- Uses
-
Webhook Routes (
src/routes/webhooks.js): Define the routes to handle/webhooks/inboundand/webhooks/status.javascript// src/routes/webhooks.js 'use strict'; const { sendWhatsAppReply } = require('../services/vonageService'); async function webhookRoutes(fastify, options) { // Register the JWT verification hook/decorator plugin // This makes fastify.verifyVonageSignature available fastify.register(require('../hooks/verifyVonageSignature')); // --- Inbound Message Webhook --- // No JWT verification applied here by default, as per common practice fastify.post('/webhooks/inbound', async (request, reply) => { fastify.log.info('Received /webhooks/inbound POST request'); fastify.log.debug({ body: request.body }, 'Inbound webhook payload:'); // Use debug for full payload // Basic validation: Check if it's a WhatsApp message with necessary fields if ( !request.body || request.body.channel !== 'whatsapp' || !request.body.from?.number || // Use optional chaining !request.body.message?.content?.text // Check for text content specifically ) { fastify.log.warn({ body: request.body }, 'Invalid, non-WhatsApp, or non-text inbound payload received.'); // Send 200 OK to Vonage even for invalid payloads we don't process, // to prevent retries for malformed requests. Log the issue. return reply.code(200).send(); } const requesterNumber = request.body.from.number; const messageText = request.body.message.content.text; const messageUuid = request.body.message_uuid; // Log the message UUID fastify.log.info( { from: requesterNumber, msgUuid: messageUuid }, `Incoming WhatsApp message: ""${messageText}""` ); // Simple auto-reply logic const replyText = `Received your message: ""${messageText}""`; try { await sendWhatsAppReply( requesterNumber, replyText, fastify.config, // Pass config fastify.log // Pass logger ); fastify.log.info({ to: requesterNumber }, `Reply sent successfully`); // Acknowledge receipt to Vonage reply.code(200).send(); } catch (error) { fastify.log.error( { err: error, to: requesterNumber }, `Failed to process inbound message or send reply` ); // Send 500 Internal Server Error if our processing fails reply.code(500).send({ error: 'Failed to process message' }); } }); // --- Status Webhook --- const statusRouteOptions = { // Apply the JWT verification hook specifically to this route's preHandler phase preHandler: [fastify.verifyVonageSignature], // Use the decorated function }; fastify.post('/webhooks/status', statusRouteOptions, async (request, reply) => { fastify.log.info('Received /webhooks/status POST request'); fastify.log.debug({ body: request.body }, 'Status webhook payload:'); // Use debug for payload // Process the status update (e.g., log it, update database) const { message_uuid, status, timestamp, error, usage } = request.body; if (message_uuid && status) { const logData = { msgUuid: message_uuid, status, timestamp, usage }; if (error) { logData.error = error; fastify.log.warn(logData, `Message status update: ${status} with error`); } else { fastify.log.info(logData, `Message status update: ${status}`); } // In a real app: await updateMessageStatusInDB(message_uuid, status, error); } else { fastify.log.warn({ body: request.body }, 'Received status webhook with missing message_uuid or status'); } // Acknowledge receipt to Vonage reply.code(200).send(); }); } module.exports = webhookRoutes;- Registers the
verifyVonageSignatureplugin. - Inbound Route:
- Logs incoming payload (using
debuglevel). - Performs basic validation for WhatsApp text messages.
- Extracts sender number, message text, and UUID.
- Calls
sendWhatsAppReply. - Handles errors and returns
200 OKor500.
- Logs incoming payload (using
- Status Route:
- Uses
preHandler: [fastify.verifyVonageSignature]to apply the JWT check before the main handler runs. - Logs the received status payload details.
- Returns
200 OK.
- Uses
- Registers the
- Register Routes in
src/server.js: This was already corrected in step 1 of this section to ensure routes are registered after core plugins. The code in step 1 is final.
5. Error Handling and Logging
Fastify provides excellent built-in logging via Pino and error handling capabilities.
- Logging:
- We configured Pino with
pino-prettyfor readable development logs insrc/server.js. In production (NODE_ENV=production), it logs structured JSON, suitable for log aggregation services (like Datadog, Splunk, ELK stack). - We pass
fastify.log(or child loggers) to our services (vonageService.js) for consistent, contextual logging. - We log key events: server start, incoming requests (with debug for payload), message content, status updates, successful replies, and crucially, errors with context (including relevant IDs like
message_uuidor phone numbers). Different log levels (info,warn,error,debug,trace) are used appropriately.
- We configured Pino with
- Error Handling:
@fastify/sensibleprovides standard error handling and utility error constructors.- Our JWT hook (
verifyVonageSignature.js) explicitly sends401,403or500errors upon verification failure or configuration issues. - Route handlers use
try...catchblocks:- To catch errors during Vonage API calls (
sendWhatsAppReply). - To handle unexpected issues during request processing.
- To catch errors during Vonage API calls (
- When our server fails internally (e.g., cannot send reply, error during JWT verification), we log the error and return a
500 Internal Server Errorto Vonage. - For invalid incoming webhook payloads (e.g., wrong format, missing fields on
/webhooks/inbound), we log the issue and return a200 OKto Vonage to prevent unnecessary retries, while ensuring our logs capture the problem.
6. WhatsApp Business API Rate Limits and Production Considerations
Before deploying your application to production with a WhatsApp Business Account (not the Sandbox), you must understand Meta's rate limits and messaging restrictions.
Messaging Limits (Tiered System)
WhatsApp uses a tiered messaging limit system based on phone number quality and verification status:
- Tier 0 (Starting): 250 business-initiated conversations per 24-hour period (default for new businesses)
- Tier 1: 1,000 conversations per 24-hour period
- Tier 2: 10,000 conversations per 24-hour period
- Tier 3: 100,000 conversations per 24-hour period
How to Increase Tiers:
- Tier 1 is achieved automatically upon successful business verification or when you reach 50% of your current limit within a 7-day period.
- Higher tiers (2-3) require sending 1,000+ delivered messages outside customer service windows to unique user phone numbers in a 30-day moving period, using templates with high quality ratings.
Source: Meta WhatsApp Messaging Limits
<!-- EXPAND: Could add flowchart showing tier progression and requirements (Type: Enhancement) -->Rate Limits
- Per-Second Limit: Maximum 80 messages per second (MPS) per business phone number, automatically scalable to 1,000 MPS based on demand
- Pair Rate Limit: Maximum 1 message every 6 seconds (~10 messages per minute) to a specific WhatsApp user number
Quality Rating Requirements
Meta assigns quality ratings (High, Medium, Low) based on user interactions:
- Response rates and response times
- User blocks and reports
- Message opt-out rates
Low quality ratings can reduce your messaging quotas or restrict messaging capabilities. Maintain high-quality interactions by responding promptly, respecting user preferences, and following WhatsApp's messaging policies.
<!-- DEPTH: Quality rating section lacks specific metrics thresholds and consequences (Priority: Medium) -->24-Hour Conversation Window
- Customer Service Window: After a user messages your business, you have 24 hours to send free-form messages without using templates.
- Marketing Messages: Outside the 24-hour window, you can only send pre-approved message templates, which Meta must review and approve.
- Service Conversations: As of November 1, 2024, service conversations (customer support within 24-hour window) are free for all businesses with no conversation cap.
Source: Meta WhatsApp Pricing Updates 2024
<!-- GAP: Missing explanation of conversation categories and pricing breakdown (Type: Substantive) -->Production Migration Checklist
When moving from Sandbox to production WhatsApp Business Account:
- Apply for WhatsApp Business API access through Vonage's managed onboarding process
- Complete business verification with Meta
- Update
VONAGE_API_HOSTfromhttps://messages-sandbox.nexmo.comtohttps://api.nexmo.com - Update
VONAGE_WHATSAPP_NUMBERto your verified business phone number - Create and submit message templates for approval in the Meta Business Manager
- Implement rate limiting in your application code to respect per-second and pair-rate limits
- Add monitoring for quality ratings and messaging tier status
- Implement conversation window tracking to use appropriate templates outside 24-hour windows
Frequently Asked Questions
How to integrate WhatsApp with Node.js and Fastify?
Integrate WhatsApp by using the Vonage Messages API with a Fastify Node.js server. Set up webhooks to receive incoming messages and send replies, ensuring your server is publicly accessible via a tool like ngrok during development. The Vonage Node.js SDK simplifies interaction with the API. This setup creates a foundation for building more complex WhatsApp bot interactions and a production-ready application.
What is the Vonage Messages API?
The Vonage Messages API is a unified platform for sending and receiving messages across multiple channels, including WhatsApp, SMS, MMS, Facebook Messenger, and Viber. The API handles routing messages between your application and users on these different platforms, providing a single interface for managing communication.
Why use Fastify for WhatsApp integration?
Fastify is a high-performance Node.js web framework ideal for building efficient and scalable WhatsApp integrations. Its speed, plugin architecture, and features like schema validation and logging make it well-suited for handling real-time message processing and API interactions. It focuses on maximizing developer experience and reducing boilerplate, resulting in clean, maintainable code.
When should I use the Vonage WhatsApp Sandbox?
Use the Vonage WhatsApp Sandbox during the initial development and testing phases of your application. It allows you to experiment and iterate quickly without needing a dedicated WhatsApp Business Account. You can test sending and receiving messages, and ensure your webhooks are set up correctly before going live.
How to set up ngrok for Vonage webhooks?
Download and install ngrok, then run it with 'ngrok http <your-fastify-port>'. Ngrok creates a public URL that forwards requests to your local server. Copy this URL and use it as the Inbound and Status URL in both your Vonage Application settings and Vonage WhatsApp Sandbox configuration. This ensures that Vonage can reach your local development environment.
What is a webhook in the context of Vonage?
A webhook is a mechanism for real-time communication from the Vonage platform to your application. Vonage sends HTTP POST requests to specific URLs (your webhooks) to deliver events like incoming messages and status updates. This allows your app to react immediately to user interactions without constantly polling the Vonage API. Two key webhook types are 'inbound', for incoming messages, and 'status', for delivery reports.
How to handle Vonage webhook security?
Secure your webhooks by verifying the JWT signature included in the 'Authorization' header of incoming webhook requests. Use the '@vonage/jwt' library's 'verifySignature' function with your Vonage API Signature Secret to validate that the request originates from Vonage. This prevents unauthorized access to your webhook endpoints. The JWT validation should happen before processing the request content.
How to send a WhatsApp message with Vonage?
Use the 'sendWhatsAppReply' function within the Vonage Node.js SDK. This function typically requires parameters like recipient number, message content, your Vonage WhatsApp Sandbox number (or Business Account number in production), API credentials, and Vonage application details. The SDK simplifies message sending and handles communication with the Vonage API.
How to reply to incoming WhatsApp messages?
In your inbound webhook handler, extract the sender's number and the incoming message text. Then, call the 'sendWhatsAppReply' function (from the Vonage service), passing the extracted sender's number as the recipient and your desired reply message. Ensure your Vonage client and credentials are correctly configured before sending the reply. The example includes a simple auto-reply logic.
What are the prerequisites for Vonage WhatsApp integration?
You will need Node.js version 20 or higher, npm or yarn, a Vonage API account, a WhatsApp account (and smartphone) for testing, and ngrok for local development. Sign up for a free Vonage account to get started and use their WhatsApp Sandbox for testing. The sandbox allows testing without a dedicated business account during the development phase.
How to test Vonage WhatsApp integration locally?
Set up the Vonage WhatsApp Sandbox, configure ngrok to expose your local server, and ensure your Fastify application is running. Then, send a WhatsApp message from your allowlisted number to the sandbox number. Your application should receive the message via the inbound webhook and then send an automatic reply, which you can observe on your phone. Check the logs for details of the interaction.
How to set up a Node.js project for Vonage integration?
Create a new directory, initialize a Node.js project with `npm init -y`, and install the required dependencies: `fastify`, `@fastify/env`, `@fastify/sensible`, `@vonage/server-sdk`, `@vonage/messages`, and `@vonage/jwt`. Organize your project with directories for routes, services, and hooks. The article provides example source code for project setup and organization.