messaging channels
messaging channels
How to Send WhatsApp Messages with Node.js and Fastify | Vonage API Tutorial
Learn how to build WhatsApp messaging into your Node.js app using Vonage Messages API and Fastify. Step-by-step guide with webhook authentication, message sending/receiving, and production-ready code examples.
Note: This article covers Vonage Messages API integration. The filename references Twilio, but the content is specifically for Vonage's WhatsApp Business API.
Learn how to send and receive WhatsApp messages programmatically using the Vonage Messages API with Node.js v22 and Fastify v5. This step-by-step tutorial shows you how to build a production-ready WhatsApp integration, from initial setup to secure webhook handling with JWT authentication, complete with working code examples you can deploy today.
This integration enables real-world use cases like customer support automation, appointment reminders, order notifications, two-factor authentication (2FA), and interactive chatbots—all through WhatsApp Business API.
By the end of this tutorial, you'll have a functional Fastify application capable of:
- Sending WhatsApp messages via the Vonage Messages API
- Receiving incoming WhatsApp messages through secure webhooks
- Validating webhook signatures using JWT Bearer tokens to ensure requests originate from Vonage
- Implementing production-grade logging and error handling
This guide assumes you have foundational knowledge of Node.js, asynchronous programming (async/await), and REST APIs.
What You'll Build: WhatsApp Messaging API Integration
Problem: Businesses need reliable ways to communicate with customers on preferred channels like WhatsApp. Building this integration from scratch involves handling API authentication, message sending protocols, receiving incoming messages (webhooks), and securing these endpoints.
Solution: Build a Node.js backend using the Fastify framework to interact with the Vonage Messages API. This provides a structured way to send messages and expose secure HTTP endpoints (webhooks) for receiving messages and delivery status updates from WhatsApp via Vonage.
Technologies:
- Node.js: JavaScript runtime environment for server-side development
- Fastify: High-performance, low-overhead web framework for Node.js, known for its speed and developer experience
- Vonage Messages API: Unified API for sending and receiving messages across various channels, including WhatsApp Business API, SMS, MMS, and Facebook Messenger
- Vonage Node SDK (
@vonage/server-sdk): Simplifies interaction with Vonage APIs in Node.js applications dotenv: Manages environment variables securelyngrok(for development): Exposes local development servers to the internet for webhook testingjsonwebtoken: Validates JWT tokens for webhook authentication
For additional messaging options, see our guides on SMS integration with E.164 formatting and 10DLC registration for US phone numbers.
System Architecture:
graph LR
User[WhatsApp User] -- Sends/Receives Message --> WhatsApp
WhatsApp -- Message Events --> Vonage[Vonage Platform]
Vonage -- Sends Message via API --> API[Fastify App API Endpoint /send]
Vonage -- Inbound/Status Webhook --> Webhook[Fastify App Webhook Endpoint /webhooks/*]
API -- Uses Vonage SDK --> Vonage
Webhook -- Processes Request --> AppLogic[Application Logic]
AppLogic -- Sends Response via SDK --> Vonage
Developer[Developer/App] -- Calls /send --> API
subgraph Your Server
API
Webhook
AppLogic
end
style Vonage fill:#00a5bd,stroke:#333,stroke-width:2px,color:#fff
style API fill:#f9f,stroke:#333,stroke-width:2px
style Webhook fill:#f9f,stroke:#333,stroke-width:2px
style AppLogic fill:#ccf,stroke:#333,stroke-width:2px(Note: Ensure your publishing platform supports Mermaid diagram rendering.)
Prerequisites:
- Node.js: LTS version v22.x recommended (latest LTS as of 2025), minimum v20.x required for Fastify v5 compatibility
- npm or yarn package manager
- A Vonage account (Sign up for free at vonage.com)
- A publicly accessible URL for webhook testing (use ngrok for local development)
- Basic familiarity with terminal/command line usage
- A WhatsApp-enabled mobile number for testing
Version Compatibility (2025):
- Vonage Node SDK: v3.24.1 or later (
@vonage/server-sdk) - Fastify: v5.x (requires Node.js v20+)
- Node.js: v22.x LTS recommended, v20.x minimum
- jsonwebtoken: Latest version for JWT validation
Final Outcome: A Fastify application with endpoints to send WhatsApp messages and receive/validate incoming message webhooks from Vonage with production-ready security.
Step 1: Set Up Your Node.js Development Environment
Ensure Node.js and npm/yarn are installed. Verify by opening your terminal and running:
node -v
npm -v
# or
# yarn -vIf not installed, download and install Node.js from nodejs.org. For additional setup guidance, check our Node.js SMS tutorial with Express which covers similar environment configuration.
Step 2: Create Your Fastify Project Structure
Create your project directory and initialize it with npm (or yarn).
-
Create Project Directory:
bashmkdir fastify-vonage-whatsapp cd fastify-vonage-whatsapp -
Initialize Node.js Project:
bashnpm init -y # or # yarn init -yThis creates a
package.jsonfile. -
Install Dependencies: Install production dependencies first:
bashnpm install fastify @vonage/server-sdk dotenv jsonwebtoken # or # yarn add fastify @vonage/server-sdk dotenv jsonwebtokenThen, install development dependencies like
pino-pretty:bashnpm install -D pino-pretty # or # yarn add -D pino-prettyfastify: The web framework@vonage/server-sdk: The official Vonage Node SDK for WhatsApp messagingdotenv: Loads environment variables from a.envfilejsonwebtoken: Validates JWT tokens for webhook authenticationpino-pretty: (Dev Dependency) Makes Fastify's logs more readable during development
-
Set up Basic Project Structure: Create the following files and directories:
fastify-vonage-whatsapp/ ├── node_modules/ ├── .env ├── .env.example ├── .gitignore ├── package.json ├── src/ │ ├── app.js │ ├── server.js │ └── routes/ │ └── vonage.js └── yarn.lock or package-lock.json -
Configure
.gitignore: Create a.gitignorefile to prevent committing sensitive information and unnecessary files:text# Environment variables .env # Node dependencies node_modules/ # Build artifacts dist/ build/ # Log files *.log npm-debug.log* yarn-debug.log* yarn-error.log* # OS generated files .DS_Store Thumbs.db
Step 3: Configure Your Vonage WhatsApp Account
Configure your Vonage account and the WhatsApp Sandbox before writing code.
- Sign Up/Log In: Go to the Vonage Dashboard and create an account or log in.
- API Credentials: Navigate to the API settings section (usually under your profile name or "API Settings"). Note down your API Key and API Secret. You'll need these shortly.
- Set up WhatsApp Sandbox:
- In the Vonage Dashboard sidebar, find "Messages and Dispatch" > "Sandbox"
- Activate the WhatsApp Sandbox by scanning the QR code with your WhatsApp app or sending the specified message to the provided Vonage sandbox number
- Keep this Sandbox page open; you'll need the Vonage sandbox number and to configure webhooks later. The Sandbox allows testing without needing a dedicated WhatsApp Business number initially.
- Generate Signature Secret: In the Vonage Dashboard, navigate to the webhook settings for your Messages API Sandbox. Generate or note the Signature Secret used for JWT webhook validation. This is a shared secret between you and Vonage used to verify webhook authenticity.
Step 4: Set Up API Credentials and Environment Variables
Use environment variables to store sensitive credentials and configuration details.
-
Create
.env.example: This file serves as a template for required variables.dotenv# .env.example # Vonage Credentials VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_APPLICATION_ID=OPTIONAL_VONAGE_APPLICATION_ID # Needed for JWT-based authentication VONAGE_PRIVATE_KEY_PATH=OPTIONAL_PATH_TO_PRIVATE_KEY # Needed for JWT-based authentication VONAGE_SIGNATURE_SECRET=YOUR_VONAGE_WEBHOOK_SIGNATURE_SECRET # Get from Vonage Dashboard for JWT webhook validation # Vonage Numbers VONAGE_WHATSAPP_NUMBER=VONAGE_SANDBOX_NUMBER # Get this from the Vonage Sandbox page # Application Settings PORT=3000 HOST=0.0.0.0 LOG_LEVEL=info # Webhook Base URL (for ngrok or deployed env) WEBHOOK_BASE_URL=YOUR_NGROK_OR_DEPLOYED_URL -
Create
.env: Duplicate.env.example, rename it to.env, and fill in your actual values.VONAGE_API_KEY,VONAGE_API_SECRET: From your Vonage dashboardVONAGE_WHATSAPP_NUMBER: The phone number provided on the Vonage WhatsApp Sandbox pageVONAGE_SIGNATURE_SECRET: Obtain this from your Vonage Dashboard under the webhook settings for Messages API. This is the shared secret used to validate JWT tokens in webhook requests.PORT,HOST,LOG_LEVEL: Default application settingsWEBHOOK_BASE_URL: Leave this blank for now; fill it when running ngrokVONAGE_APPLICATION_ID,VONAGE_PRIVATE_KEY_PATH: Not required for the basic authentication method (API Key/Secret) used in this guide but needed for advanced JWT-based authentication features
-
Load Environment Variables: Modify your
package.jsonscripts to usedotenv(for loading.envfiles) andpino-pretty(for readable logs) during development:json// package.json "scripts": { "start": "node src/server.js", "dev": "node -r dotenv/config src/server.js | pino-pretty" },- The
startscript runs the server directly (suitable for production where env vars are set externally) - The
devscript uses-r dotenv/configto preload environment variables from your.envfile before the application starts. It then pipes the JSON output logs throughpino-prettyfor better readability in the terminal
- The
5. Installing Vonage SDK (Already Done)
We installed @vonage/server-sdk and jsonwebtoken in Step 2. These packages provide convenient methods for interacting with the Vonage API and validating webhook JWTs.
Step 6: How to Send WhatsApp Messages Programmatically
Create the Fastify application structure and add a route to send messages.
-
Set up Fastify App (
src/app.js):javascript// src/app.js 'use strict'; const Fastify = require('fastify'); const vonageRoutes = require('./routes/vonage'); const { Vonage } = require('@vonage/server-sdk'); function build(opts = {}) { const app = Fastify(opts); // Initialize Vonage Client // Note: Ensure VONAGE_API_KEY and VONAGE_API_SECRET are set in .env const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, // Note: applicationId and privateKey are not needed for Key/Secret auth }); // Make Vonage client accessible in routes app.decorate('vonage', vonage); // Register routes app.register(vonageRoutes, { prefix: '/api/vonage' }); // Basic health check route app.get('/health', async (request, reply) => { return { status: 'ok', timestamp: new Date().toISOString() }; }); return app; } module.exports = build;- Initialize Fastify
- Create a Vonage client instance using API Key and Secret from environment variables
- Use
app.decorateto make thevonageclient easily available within route handlers (request.server.vonage) - Register specific Vonage routes under the
/api/vonageprefix - Include a simple
/healthcheck endpoint
-
Create Server Entry Point (
src/server.js):javascript// src/server.js 'use strict'; // Read .env file if not preloaded (e.g., via `node -r dotenv/config`) // This ensures it works if started with just `node src/server.js` in dev if (process.env.NODE_ENV !== 'production' && !process.env.DOTENV_CONFIG_PATH && require('fs').existsSync('.env')) { console.log('Loading .env file for non-production environment.'); require('dotenv').config(); } const server = require('./app')({ logger: { level: process.env.LOG_LEVEL || 'info', // pino-pretty is handled by the `npm run dev` script pipe, not here }, }); const start = async () => { try { const port = parseInt(process.env.PORT || '3000', 10); const host = process.env.HOST || '0.0.0.0'; await server.listen({ port: port, host: host }); // Note: Fastify logs the listening address automatically with default logger settings. server.log.info(`Access health check at http://localhost:${port}/health`); server.log.info(`API prefix: /api/vonage`); } catch (err) { server.log.error(err); process.exit(1); } }; start();- Configure the Fastify logger based on the environment variable
- Start the server, listening on the configured host and port
- Include a check to load
.envif not preloaded via the script (useful for some debugging scenarios)
-
Create Send Message Route (
src/routes/vonage.js):javascript// src/routes/vonage.js 'use strict'; const jwt = require('jsonwebtoken'); const sendMessageSchema = { body: { type: 'object', required: ['to', 'text'], properties: { to: { type: 'string', description: 'Recipient WhatsApp number (E.164 format)', pattern: '^\\+?[1-9]\\d{1,14}' }, // Basic E.164 pattern text: { type: 'string', description: 'Message content', minLength: 1 }, }, }, response: { 200: { type: 'object', properties: { message_uuid: { type: 'string' }, detail: { type: 'string' } } }, // Define common error responses for better OpenAPI/Swagger documentation 400: { $ref: 'http://example.com/schemas/error#/definitions/badRequest' }, // Placeholder 500: { $ref: 'http://example.com/schemas/error#/definitions/serverError' } // Placeholder } }; // --- Helper function for JWT Signature Validation --- // Vonage Messages API uses JWT Bearer tokens in the Authorization header function validateVonageJWT(request, secret, log) { try { // Extract the JWT from the Authorization header const authHeader = request.headers['authorization']; if (!authHeader) { log.warn('Missing Authorization header in webhook request.'); return false; } // Authorization header format: "Bearer <jwt_token>" const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') { log.warn('Invalid Authorization header format. Expected "Bearer <token>".'); return false; } const token = parts[1]; // Verify and decode the JWT using the signature secret const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] // Vonage uses HMAC-SHA256 }); log.info('JWT signature validated successfully.'); // Optional: Verify payload_hash to ensure payload hasn't been tampered with if (decoded.payload_hash && request.body) { const crypto = require('crypto'); const bodyString = JSON.stringify(request.body); const calculatedHash = crypto.createHash('sha256').update(bodyString).digest('hex'); if (decoded.payload_hash !== calculatedHash) { log.warn('Payload hash mismatch. Possible tampering detected.'); return false; } log.info('Payload hash verified successfully.'); } // Optional: Check token age using 'iat' (issued at) claim if (decoded.iat) { const tokenAge = Date.now() / 1000 - decoded.iat; const MAX_TOKEN_AGE = 300; // 5 minutes if (tokenAge > MAX_TOKEN_AGE) { log.warn(`Token is stale (age: ${tokenAge}s). Possible replay attack.`); return false; } } return true; // JWT is valid } catch (error) { if (error.name === 'JsonWebTokenError') { log.warn('Invalid JWT signature.'); } else if (error.name === 'TokenExpiredError') { log.warn('JWT token has expired.'); } else { log.error({ msg: 'Error during JWT validation', error: error.message, stack: error.stack }); } return false; } } // End of validateVonageJWT async function vonageRoutes(fastify, options) { const vonage = fastify.vonage; // Access decorated Vonage client const signatureSecret = process.env.VONAGE_SIGNATURE_SECRET; // --- Send WhatsApp Message Route --- fastify.post('/send-whatsapp', { schema: sendMessageSchema }, async (request, reply) => { const { to, text } = request.body; const from = process.env.VONAGE_WHATSAPP_NUMBER; if (!from) { fastify.log.error('VONAGE_WHATSAPP_NUMBER environment variable not set.'); return reply.status(500).send({ error: 'Server configuration error: Missing sender number.' }); } fastify.log.info(`Attempting to send WhatsApp message from ${from} to ${to}`); try { const resp = await vonage.messages.send({ message_type: "text", text: text, to: to, from: from, channel: "whatsapp" }); fastify.log.info(`Message sent successfully. UUID: ${resp.messageUuid}`); return reply.send({ message_uuid: resp.messageUuid, detail: `Message sent to ${to}` }); } catch (error) { fastify.log.error({ msg: 'Error sending WhatsApp message', error: error?.response?.data || error.message, statusCode: error?.response?.status }); // Provide more specific error feedback if possible let statusCode = 500; let errorMessage = 'Failed to send message due to an internal server error.'; if (error.response?.data) { // Use Vonage's error details if available errorMessage = error.response.data.title || error.response.data.detail || errorMessage; if (error.response.status >= 400 && error.response.status < 500) { statusCode = error.response.status; } } else if (error.message && error.message.toLowerCase().includes('number')) { // Basic check for common number format issues statusCode = 400; errorMessage = 'Invalid recipient number format. Ensure it uses E.164 format (e.g., +14155552671).'; } // Return a structured error return reply.status(statusCode).send({ error: { message: errorMessage, type: error.response?.data?.type, // Include Vonage error type if available details: error.response?.data?.invalid_parameters // Include specific invalid params if available } }); } }); // --- Inbound Message Webhook --- fastify.post('/webhooks/inbound', { // Optional: Add schema validation for expected inbound webhook structure if needed }, async (request, reply) => { fastify.log.info('Received inbound webhook'); // 1. JWT Signature Validation (CRITICAL for Security) if (signatureSecret) { if (!validateVonageJWT(request, signatureSecret, fastify.log)) { fastify.log.warn('Invalid webhook JWT signature received.'); return reply.status(401).send({ error: 'Invalid signature' }); } } else { // WARNING: Only skip validation in very specific, controlled non-production scenarios. fastify.log.warn('Webhook signature validation skipped (VONAGE_SIGNATURE_SECRET not set). THIS IS INSECURE.'); } // 2. Process the message const inboundData = request.body; fastify.log.info({ msg: 'Inbound message data:', data: inboundData }); // Example: Log sender and message text if (inboundData?.from?.type === 'whatsapp' && inboundData?.message?.content?.text) { fastify.log.info(`Message from ${inboundData.from.number}: ${inboundData.message.content.text}`); // TODO: Add your business logic here (e.g., reply, store in DB, queue for processing) } else { fastify.log.warn('Received inbound webhook with unexpected structure or non-text message.'); } // 3. Acknowledge receipt immediately // Vonage requires a 200 OK response quickly. Defer long processing. reply.status(200).send('OK'); }); // --- Message Status Webhook --- fastify.post('/webhooks/status', { // Optional: Add schema validation for expected status webhook structure if needed }, async (request, reply) => { fastify.log.info('Received status webhook'); // 1. JWT Signature Validation (CRITICAL for Security) if (signatureSecret) { if (!validateVonageJWT(request, signatureSecret, fastify.log)) { fastify.log.warn('Invalid webhook JWT signature received.'); return reply.status(401).send({ error: 'Invalid signature' }); } } else { fastify.log.warn('Webhook signature validation skipped (VONAGE_SIGNATURE_SECRET not set). THIS IS INSECURE.'); } // 2. Process the status update const statusData = request.body; fastify.log.info({ msg: 'Message status update:', data: statusData }); // Example: Log message UUID and status if (statusData?.message_uuid && statusData?.status) { fastify.log.info(`Status for message ${statusData.message_uuid}: ${statusData.status} (Timestamp: ${statusData.timestamp})`); // TODO: Update message status in your database if needed } else { fastify.log.warn('Received status webhook with unexpected structure.'); } // 3. Acknowledge receipt immediately reply.status(200).send('OK'); }); } // End of vonageRoutes plugin function module.exports = vonageRoutes;- Define a
sendMessageSchemafor request body validation using Fastify's built-in capabilities, including a basic E.164 pattern check for thetonumber - The route handler
/api/vonage/send-whatsapptakes thetonumber andtextfrom the request body - Retrieve the
VONAGE_WHATSAPP_NUMBERfrom environment variables to use as thefromnumber - Call
vonage.messages.send()with the required parameters for a text message via WhatsApp - Enhanced logging and error handling included, attempting to extract and return specific error details from the Vonage API response
- The
validateVonageJWThelper function implements JWT validation using thejsonwebtokenlibrary - Webhook routes (
/webhooks/inbound,/webhooks/status) use the helper for signature validation and log incoming data, responding quickly with200 OK
- Define a
-
Test Sending:
- Run the development server:
npm run dev(oryarn dev) - Open a new terminal and use
curl(or a tool like Postman) to send a request. ReplaceYOUR_WHATSAPP_NUMBERwith your actual number linked to the Vonage Sandbox (in E.164 format, e.g.,+14155552671)
bashcurl -X POST http://localhost:3000/api/vonage/send-whatsapp \ -H "Content-Type: application/json" \ -d '{ "to": "YOUR_WHATSAPP_NUMBER", "text": "Hello from Fastify and Vonage!" }'- You should receive the WhatsApp message on your phone and see a success response in the terminal (like
{"message_uuid":"...","detail":"Message sent to YOUR_WHATSAPP_NUMBER"}). Check the server logs as well.
- Run the development server:
Step 7: How to Receive WhatsApp Messages via Webhooks
Vonage uses webhooks to notify your application about incoming WhatsApp messages and message status updates. The routes for this (/api/vonage/webhooks/inbound and /api/vonage/webhooks/status) are already included in src/routes/vonage.js from the previous step.
Step 8: Expose Your Local Server for Webhook Testing
Allow Vonage's servers to reach your local development machine using ngrok.
- Install ngrok: Follow the instructions at ngrok.com. Sign up for a free account and install an auth token.
- Start Your Fastify App: If it's not running, start it:
npm run dev. Note the port (default is 3000). - Start ngrok: Open a new terminal window and run:
(Replacebash
ngrok http 30003000if your app uses a different port). - Copy the ngrok URL: ngrok will display forwarding URLs. Copy the
httpsURL (e.g.,https://random-string.ngrok-free.app). - Update
.env: Set theWEBHOOK_BASE_URLin your.envfile to this ngrok URL.dotenv# .env # ... other vars WEBHOOK_BASE_URL=https://<your-random-string>.ngrok-free.app # Use the actual URL from ngrok output - Restart Fastify App: Stop (
Ctrl+C) and restart your Fastify app (npm run dev) to pick up theWEBHOOK_BASE_URLif your code uses it (though it's primarily for configuring Vonage). - Configure Vonage Webhooks:
- Go back to your Vonage Dashboard's Sandbox page
- Find the "Webhooks" section
- Enter your Inbound URL:
YOUR_NGROK_HTTPS_URL/api/vonage/webhooks/inbound(e.g.,https://<your-random-string>.ngrok-free.app/api/vonage/webhooks/inbound) - Enter your Status URL:
YOUR_NGROK_HTTPS_URL/api/vonage/webhooks/status(e.g.,https://<your-random-string>.ngrok-free.app/api/vonage/webhooks/status) - Important: Ensure the signature secret in your Vonage Dashboard matches the
VONAGE_SIGNATURE_SECRETin your.envfile exactly - Click "Save webhooks"
Step 9: Test Your WhatsApp Message Receiving
Test receiving messages now.
- Send a WhatsApp Message: From the phone number linked to your Vonage Sandbox, send a message to the Vonage Sandbox number.
- Check Logs: Observe the terminal running your Fastify application (
npm run dev). You should see logs indicating:Received inbound webhookJWT signature validated successfully.(if validation passes)Inbound message data: { ... }(showing the message payload)Message from <your_number>: <your_message_text>
- Check ngrok Console: The terminal running ngrok (
ngrok http 3000) will also show incomingPOSTrequests to your webhook URLs (e.g.,POST /api/vonage/webhooks/inbound 200 OK).
If you see errors, especially related to signature validation (401 Unauthorized response in ngrok, validation failure logs in Fastify):
- Verify the
VONAGE_SIGNATURE_SECRETin your.envexactly matches the secret configured in the Vonage Dashboard. Even a single character difference or whitespace will cause failure. - Confirm the webhook URLs in the Vonage Dashboard are correct (using
httpsand the full path/api/vonage/webhooks/...). - Ensure your Fastify app and ngrok are running and connected.
- Check that the
jsonwebtokenpackage is installed. - Review Fastify logs for specific JWT validation error messages.
Step 10: Secure Your WhatsApp Webhooks with JWT Authentication
Validating webhook signatures is critical to ensure requests genuinely come from Vonage and haven't been tampered with or forged.
Vonage Messages API Webhook Security:
Vonage uses JSON Web Token (JWT) Bearer Authorization for webhooks sent from the Messages API. This provides robust authentication different from HMAC signature validation.
How JWT Webhook Authentication Works:
-
Authorization Header: Vonage includes a JWT in the
Authorizationheader of each webhook request in the format:Authorization: Bearer <jwt_token> -
JWT Contents: The JWT is signed using HMAC-SHA256 with your signature secret and contains:
- Standard JWT claims (
iat– issued at timestamp) payload_hash– SHA-256 hash of the webhook payload for tamper detection- Other Vonage-specific claims
- Standard JWT claims (
-
Validation Process:
- Extract the JWT from the Authorization header
- Verify the signature using your
VONAGE_SIGNATURE_SECRET - Optionally verify the
payload_hashmatches the actual payload - Optionally check the
iattimestamp to reject stale tokens
Implementation:
The validateVonageJWT function in src/routes/vonage.js implements this validation using the jsonwebtoken library. Ensure you have installed it:
npm install jsonwebtoken
# or
yarn add jsonwebtokenSecurity Best Practices for WhatsApp Webhooks:
- Always validate signatures in production – Never skip JWT validation
- Keep your signature secret secure – Store it in environment variables, never commit to version control
- Use the same secret in both places – The secret in your
.envmust exactly match the one in your Vonage Dashboard - Implement payload hash verification – Adds an extra layer of security against tampering
- Check token age – Reject tokens older than a few minutes to prevent replay attacks
- Log validation failures – Monitor for potential security issues
Troubleshooting Webhook Authentication:
If webhook validation fails:
- Verify
VONAGE_SIGNATURE_SECRETmatches exactly in both.envand Vonage Dashboard - Check that webhook URLs in Vonage Dashboard use
httpsand the correct paths - Ensure the
jsonwebtokenpackage is installed - Review Fastify logs for specific JWT validation error messages
- Check for whitespace or encoding issues in the secret