This guide provides a step-by-step walkthrough for building a production-ready application that sends and receives WhatsApp messages using the Vonage Messages API, Node.js, and the Fastify web framework. We will cover everything from initial project setup and configuration to sending messages, handling incoming webhooks securely, error handling, and deployment considerations.
By the end of this tutorial, you will 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 to ensure requests originate from Vonage.
- Basic logging and error handling.
This guide assumes you have a foundational understanding of Node.js, asynchronous programming (async
/await
), and REST APIs.
Project Overview and Goals
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: We will 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: A JavaScript runtime environment for server-side development.
- Fastify: A high-performance, low-overhead web framework for Node.js, known for its speed and developer experience.
- Vonage Messages API: A unified API for sending and receiving messages across various channels, including WhatsApp, SMS, MMS, and Facebook Messenger. We'll use its WhatsApp capabilities.
- Vonage Node SDK (
@vonage/server-sdk
): Simplifies interaction with Vonage APIs in Node.js applications. dotenv
: To manage environment variables securely.ngrok
(for development): To expose local development servers to the internet for webhook testing.
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 recommended, e.g., v18 or later)
- npm or yarn package manager
- A Vonage account (Sign up for free at vonage.com) Verification required: Ensure link is live and correct
- A publicly accessible URL for webhook testing (we'll use ngrok for local development)
- Basic familiarity with terminal/command line usage.
- A WhatsApp-enabled mobile number for testing.
Final Outcome: A Fastify application with endpoints to send WhatsApp messages and receive/validate incoming message webhooks from Vonage.
1. Environment Setup
Ensure Node.js and npm/yarn are installed. Verify by opening your terminal and running:
node -v
npm -v
# or
# yarn -v
If not installed, download and install Node.js from nodejs.org. Verification required: Ensure link is live and correct
2. Project Scaffolding (Fastify)
Let's create our project directory and initialize it with npm (or yarn).
-
Create Project Directory:
mkdir fastify-vonage-whatsapp cd fastify-vonage-whatsapp
-
Initialize Node.js Project:
npm init -y # or # yarn init -y
This creates a
package.json
file. -
Install Dependencies: Install production dependencies first:
npm install fastify @vonage/server-sdk dotenv # or # yarn add fastify @vonage/server-sdk dotenv
Then, install development dependencies like
pino-pretty
:npm install -D pino-pretty # or # yarn add -D pino-pretty
fastify
: The web framework.@vonage/server-sdk
: The official Vonage Node SDK.dotenv
: Loads environment variables from a.env
file.pino-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.gitignore
file to prevent committing sensitive information and unnecessary files:# 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
3. Vonage Account Setup & Sandbox
Before writing code, configure your Vonage account and the WhatsApp Sandbox.
- Sign Up/Log In: Go to the Vonage Dashboard (Verification required: Ensure link is live and correct) 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. Crucially, the Sandbox allows testing without needing a dedicated WhatsApp Business number initially.
4. Configuration (Environment Variables)
We use environment variables to store sensitive credentials and configuration details.
-
Create
.env.example
: This file serves as a template for required variables.# .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/signed webhooks (distinct from Signature Secret HMAC) VONAGE_PRIVATE_KEY_PATH=OPTIONAL_PATH_TO_PRIVATE_KEY # Needed for JWT-based authentication/signed webhooks VONAGE_SIGNATURE_SECRET=YOUR_VONAGE_WEBHOOK_SIGNATURE_SECRET # Generate a strong random string for HMAC 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 dashboard.VONAGE_WHATSAPP_NUMBER
: The phone number provided on the Vonage WhatsApp Sandbox page.VONAGE_SIGNATURE_SECRET
: Generate a strong, unique secret (e.g., using a password manager oropenssl rand -hex 32
). This is vital for webhook security using the HMAC method. You will configure this secret in the Vonage dashboard later.PORT
,HOST
,LOG_LEVEL
: Default application settings.WEBHOOK_BASE_URL
: Leave this blank for now; we'll fill it when running ngrok.VONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
: These are not required for the authentication method (API Key/Secret) and webhook signature method (HMAC Signature Secret) used in this guide. They are used for JWT-based authentication, which provides access to other Vonage APIs but adds complexity.
-
Load Environment Variables: Modify your
package.json
scripts to usedotenv
(for loading.env
files) andpino-pretty
(for readable logs) during development:// package.json ""scripts"": { ""start"": ""node src/server.js"", ""dev"": ""node -r dotenv/config src/server.js | pino-pretty"" },
- The
start
script runs the server directly (suitable for production where env vars are set externally). - The
dev
script uses-r dotenv/config
to preload environment variables from your.env
file before the application starts. It then pipes the JSON output logs throughpino-pretty
for better readability in the terminal.
- The
5. Installing Vonage SDK (Already Done)
We installed @vonage/server-sdk
in Step 2. This SDK provides convenient methods for interacting with the Vonage API.
6. Sending WhatsApp Messages
Let's create the Fastify application structure and add a route to send messages.
-
Set up Fastify App (
src/app.js
):// src/app.js 'use strict'; const Fastify = require('fastify'); const vonageRoutes = require('./routes/vonage'); const { Vonage } = require('@vonage/server-sdk'); const crypto = require('crypto'); // For webhook validation function build(opts = {}) { const app = Fastify(opts); // --- Add Content Type Parser to capture raw body BEFORE parsing --- // This allows us to access the raw buffer for signature validation app.addContentTypeParser( 'application/json', { parseAs: 'buffer' }, // Keep body as a Buffer initially function (req, bodyBuffer, done) { try { // Store the raw body buffer on the request object // This makes it available in the route handler for validation req.rawBody = bodyBuffer; // Now, parse the JSON from the buffer for the route handler if (bodyBuffer.length === 0) { // Handle empty body case if necessary, maybe return null or {} done(null, null); return; } const json = JSON.parse(bodyBuffer.toString()); done(null, json); // Pass the parsed JSON to the handler } catch (err) { err.statusCode = 400; // Bad request if JSON parsing fails done(err, undefined); } } ); // Optional: Add parser for 'application/x-www-form-urlencoded' if Vonage might use it // Check Vonage docs if form data is possible for webhooks app.addContentTypeParser( 'application/x-www-form-urlencoded', { parseAs: 'buffer' }, function(req, bodyBuffer, done) { try { req.rawBody = bodyBuffer; // Store raw body // Parse form data after storing const parsed = new URLSearchParams(bodyBuffer.toString()); const json = {}; parsed.forEach((value, key) => { json[key] = value; }); done(null, json); // Pass parsed form data as an object } catch (err) { err.statusCode = 400; done(err, undefined); } } ); // 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 and crypto accessible in routes app.decorate('vonage', vonage); app.decorate('crypto', crypto); // 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;
- We initialize Fastify.
- We add content type parsers to capture the raw request body before JSON or form parsing, storing it on
req.rawBody
. This is crucial for webhook signature validation. - We create a Vonage client instance using API Key and Secret from environment variables.
- We use
app.decorate
to make thevonage
client andcrypto
module easily available within our route handlers (request.server.vonage
,request.server.crypto
). - We register our specific Vonage routes under the
/api/vonage
prefix. - A simple
/health
check endpoint is included.
-
Create Server Entry Point (
src/server.js
):// 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();
- This file configures the Fastify logger based on the environment variable.
- It starts the server, listening on the configured host and port.
- Includes a check to load
.env
if not preloaded via the script (useful for some debugging scenarios).
-
Create Send Message Route (
src/routes/vonage.js
):// src/routes/vonage.js 'use strict'; 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 Signature Validation --- function validateVonageSignature(request, secret, crypto, log) { try { // --- CRITICAL: VERIFY THIS HEADER NAME --- // Vonage documentation for the Messages API must be consulted to confirm // the exact HTTP header used for HMAC signature secret validation. // This example uses a *hypothetical* header name. Replace 'x-vonage-hmac-sha256' // with the actual header name specified by Vonage. const V_SIGNATURE_HEADER = 'x-vonage-hmac-sha256'; // <<< HYPOTHETICAL - CHECK VONAGE DOCS! const headerSignature = request.headers[V_SIGNATURE_HEADER.toLowerCase()]; if (!headerSignature) { log.warn(`Missing expected signature header ('${V_SIGNATURE_HEADER}'). Check Vonage webhook configuration and documentation.`); return false; } // Ensure the raw body was captured by the content type parser in app.js if (!request.rawBody || request.rawBody.length === 0) { log.error('Raw request body not available or empty for signature validation. Check Fastify ContentTypeParser setup.'); return false; } const hmac = crypto.createHmac('sha256'_ secret); // Calculate the signature based on the RAW request body buffer const calculatedSignature = hmac.update(request.rawBody).digest('hex'); // Securely compare the calculated signature with the one from the header // Ensure the header value is just the hex digest (adjust if it includes prefixes like 'sha256=') const headerValue = headerSignature; // Modify if header format is different (e.g._ remove prefix) if (headerValue.length !== calculatedSignature.length) { log.warn('Signature length mismatch.'); return false; } const isValid = crypto.timingSafeEqual( Buffer.from(headerValue)_ Buffer.from(calculatedSignature) ); if (!isValid) { log.warn('Calculated signature does not match header signature. Ensure VONAGE_SIGNATURE_SECRET is correct in both .env and Vonage Dashboard.'); // Avoid logging secrets or full signatures in production logs log.debug({ header: headerValue.substring(0_ 5) + '...'_ calculated: calculatedSignature.substring(0_ 5) + '...' }); return false; } return true; // Signature is valid } catch (error) { log.error({ msg: 'Error during signature validation'_ error: error.message_ stack: error.stack }); return false; } } // End of validateVonageSignature async function vonageRoutes(fastify_ options) { const vonage = fastify.vonage; // Access decorated Vonage client const crypto = fastify.crypto; // Access decorated crypto module 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. Signature Validation (CRITICAL for Security) if (signatureSecret) { // The validateVonageSignature function expects the raw body, // which should be attached by the custom content type parser in app.js if (!validateVonageSignature(request, signatureSecret, crypto, fastify.log)) { fastify.log.warn('Invalid webhook signature received.'); return reply.status(401).send({ error: 'Invalid signature' }); } fastify.log.info('Webhook signature validated successfully.'); } 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. Signature Validation (CRITICAL for Security) if (signatureSecret) { if (!validateVonageSignature(request, signatureSecret, crypto, fastify.log)) { fastify.log.warn('Invalid webhook signature received.'); return reply.status(401).send({ error: 'Invalid signature' }); } fastify.log.info('Webhook signature validated successfully.'); } 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;
- We define a
sendMessageSchema
for request body validation using Fastify's built-in capabilities, including a basic E.164 pattern check for theto
number. - The route handler
/api/vonage/send-whatsapp
takes theto
number andtext
from the request body. - It retrieves the
VONAGE_WHATSAPP_NUMBER
from environment variables to use as thefrom
number. - It calls
vonage.messages.send()
with the required parameters for a text message via WhatsApp. - Enhanced logging and error handling are included, attempting to extract and return specific error details from the Vonage API response.
- The
validateVonageSignature
helper function is defined before the mainvonageRoutes
function that uses it. - Webhook routes (
/webhooks/inbound
,/webhooks/status
) are defined withinvonageRoutes
. They use the helper for signature validation and log incoming data, responding quickly with200 OK
.
- We 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_NUMBER
with your actual number linked to the Vonage Sandbox (in E.164 format, e.g.,+14155552671
).
curl -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:
7. Receiving WhatsApp Messages (Webhooks)
Vonage uses webhooks to notify your application about incoming messages and message status updates. The routes for this (/api/vonage/webhooks/inbound
and /api/vonage/webhooks/status
) were already added within src/routes/vonage.js
in the previous step.
8. Exposing Localhost (ngrok)
To allow Vonage's servers to reach your local development machine, you need a tool like ngrok.
- Install ngrok: Follow the instructions at ngrok.com. (Verification required: Ensure link is live and correct). You'll likely need to 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:
(Replace
ngrok http 3000
3000
if your app uses a different port). - Copy the ngrok URL: ngrok will display forwarding URLs. Copy the
https
URL (e.g.,https://random-string.ngrok.io
orhttps://<your-random-string>.ngrok-free.app
). - Update
.env
: Set theWEBHOOK_BASE_URL
in your.env
file to this ngrok URL.(Note: ngrok free tier URLs might change suffix, e.g.,# .env # ... other vars WEBHOOK_BASE_URL=https://<your-random-string>.ngrok-free.app # Use the actual URL from ngrok output
.ngrok-free.app
) - Restart Fastify App: Stop (
Ctrl+C
) and restart your Fastify app (npm run dev
) to potentially pick up theWEBHOOK_BASE_URL
if your code uses it (though it's primarily for configuring Vonage). - Configure Vonage Webhooks:
- Go back to your Vonage Dashboard's Sandbox page. (Verification required: Ensure link is live and correct).
- 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
) - Crucially: Set the ""Signature secret verification"". Find the option for signature secrets (it might be labelled ""HMAC"", ""Signature Secret"", or similar - verify the exact term in the dashboard). Select the appropriate method (likely involving SHA256) and paste the
VONAGE_SIGNATURE_SECRET
you generated and put in your.env
file into the corresponding field in the Vonage dashboard. - Click ""Save webhooks"".
9. Handling Inbound Messages
Now, test receiving messages.
- 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 webhook
Webhook 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 incomingPOST
requests 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_SECRET
in your.env
exactly 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
https
and the full path/api/vonage/webhooks/...
). - Ensure the correct signature verification method is selected in the Vonage dashboard.
- Verify the correct HTTP header name is being checked in the
validateVonageSignature
function (consult Vonage docs!). - Ensure your Fastify app (
src/app.js
) includes the content type parser to capturerequest.rawBody
. - Ensure your Fastify app and ngrok are running and connected.
10. Securing Webhooks (Signature Validation Refined)
Validating webhook signatures is critical to ensure requests genuinely come from Vonage and haven't been tampered with or forged. Our webhook routes already include a call to validateVonageSignature
. The key challenge is that signature calculation must happen on the raw, unparsed request body.
Solution: Capturing the Raw Body with a Content Type Parser
The necessary code to capture the raw body using app.addContentTypeParser
was added to src/app.js
in Step 6.1. This parser intercepts incoming application/json
and application/x-www-form-urlencoded
requests, stores the raw body buffer on request.rawBody
, and then proceeds with parsing the content for the route handler. The validateVonageSignature
helper function (defined in src/routes/vonage.js
) then uses request.rawBody
to perform the HMAC calculation correctly.
Ensure the validateVonageSignature
function correctly identifies the header Vonage uses (the example uses the hypothetical x-vonage-hmac-sha256
- check Vonage documentation for the actual header name) and that the VONAGE_SIGNATURE_SECRET
environment variable matches the configuration in the Vonage dashboard precisely.