code examples
code examples
Build WhatsApp Integration with Vonage and NestJS: Complete Guide
Complete tutorial for building WhatsApp messaging with Vonage Messages API and NestJS. Includes webhooks, status updates, security, and 2025 production requirements.
Build WhatsApp Integration with Vonage and NestJS: Complete Guide
This guide walks you through building a robust WhatsApp messaging service using Node.js with the NestJS framework and the Vonage Messages API (Application Programming Interface). You'll learn to set up your environment, send messages, receive incoming messages via webhooks, handle message status updates, and implement production considerations like security, error handling, and deployment.
By completing this tutorial, you'll have a functional NestJS application that can:
- Send WhatsApp messages programmatically via the Vonage Messages API.
- Receive incoming WhatsApp messages through secure webhooks.
- Process message status updates (e.g., delivered, read).
- Handle configuration securely and effectively.
This guide assumes basic knowledge of Node.js, TypeScript, and REST APIs (Representational State Transfer Application Programming Interfaces). NestJS provides structured architecture, dependency injection, and modularity that simplify building scalable applications. Vonage provides the communication infrastructure for sending and receiving WhatsApp messages.
Prerequisites:
- Node.js (v20 LTS or v22 LTS recommended for 2025 – v22 "Jod" is Active LTS through October 2025, then Maintenance LTS through April 2027. v20 "Iron" is in Maintenance LTS through April 2026. v18 reached end-of-life in April 2025.)
- npm or yarn package manager
- A Vonage API Account (Sign up for free credit if you don't have one)
- ngrok installed (A free account is sufficient). Note: Use ngrok for local development testing only, allowing Vonage webhooks to reach your machine. Production deployments require a stable, publicly accessible HTTPS endpoint for your webhook handlers.
- Access to a WhatsApp-enabled device for testing
Verify Your Installation:
Confirm your Node.js version meets requirements:
node --version # Should show v20.x.x or v22.x.x
npm --version # Verify npm is installed
ngrok version # Confirm ngrok is availableGitHub Repository:
Find a complete, working example of the code in this guide at [repository URL placeholder – update with actual URL].
Project Architecture
The system consists of the following components:
- NestJS Application: The core backend service built with NestJS. It exposes endpoints to trigger outgoing messages and handles incoming webhooks from Vonage.
- Vonage Messages API: The third-party service used to send and receive WhatsApp messages.
- WhatsApp Platform: Where end-users interact via their WhatsApp client.
- ngrok (Development): A tool to expose your local NestJS application to the internet so Vonage webhooks can reach it during development.
sequenceDiagram
participant User as WhatsApp User
participant WhatsApp
participant Vonage as Vonage Messages API
participant Ngrok as ngrok (Dev Only)
participant NestJS as NestJS Application
User->>+WhatsApp: Sends message
WhatsApp->>+Vonage: Delivers inbound message
Vonage->>+Ngrok: Forwards message to webhook URL
Ngrok->>+NestJS: Delivers message payload to /webhooks/inbound
NestJS-->>-Ngrok: Responds 200 OK
Ngrok-->>-Vonage: Forwards 200 OK
Note right of NestJS: Process inbound message (e.g., log, reply)
NestJS->>+Vonage: Sends reply message via API
Vonage->>+WhatsApp: Delivers reply message
WhatsApp->>-User: Shows reply message
Note over Vonage, NestJS: Status Updates
Vonage->>+Ngrok: Forwards status update to webhook URL
Ngrok->>+NestJS: Delivers status payload to /webhooks/status
NestJS-->>-Ngrok: Responds 200 OK
Ngrok-->>-Vonage: Forwards 200 OK
Note right of NestJS: Process status update (e.g., log)1. Setting Up the NestJS Project
Initialize a new NestJS project and install the necessary dependencies.
1.1. Install NestJS CLI
npm install -g @nestjs/cli1.2. Create a New NestJS Project
nest new vonage-whatsapp-nestjs
cd vonage-whatsapp-nestjsChoose your preferred package manager (npm or yarn) when prompted.
1.3. Install Required Dependencies
Install the Vonage SDK packages and configuration modules:
npm install @vonage/server-sdk @vonage/messages @vonage/jwt @nestjs/config dotenv
# Or using yarn:
# yarn add @vonage/server-sdk @vonage/messages @vonage/jwt @nestjs/config dotenvWhat Each Package Does:
@vonage/server-sdk– Core Vonage SDK for Node.js@vonage/messages– Messages API client for sending WhatsApp, SMS, and other messages@vonage/jwt– JWT verification for webhook signature validation@nestjs/config– NestJS module for managing environment variablesdotenv– Loads environment variables from.envfiles
Package Compatibility:
| Package | Minimum Version | Notes |
|---|---|---|
| @vonage/server-sdk | 3.15.0+ | Supports latest Messages API features |
| @vonage/messages | 2.1.0+ | Required for WhatsApp support |
| @vonage/jwt | 1.2.0+ | Required for signature verification |
| @nestjs/config | 3.0.0+ | Compatible with NestJS 10+ |
1.4. Set Up Environment Variables
Create a .env file in the root of your project. This file stores your Vonage credentials and configuration. Never commit this file to version control.
# .env
# Vonage API Credentials & Application Details
VONAGE_API_KEY=YOUR_API_KEY_HERE
VONAGE_API_SECRET=YOUR_API_SECRET_HERE
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID_HERE
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
VONAGE_API_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET_HERE # For webhook verification
# Vonage WhatsApp Number (Sandbox or Purchased)
VONAGE_WHATSAPP_NUMBER=VONAGE_PROVIDED_WHATSAPP_NUMBER
# Application Port
PORT=3000 # Default NestJS port
# Vonage API Host (Optional: Use sandbox for testing initially)
VONAGE_API_HOST=https://messages-sandbox.nexmo.com # For production, remove this line to default to the production URL (api.nexmo.com)Secure Your Credentials:
Add these entries to your .gitignore file:
# Vonage credentials
.env
.env.*
!.env.example
private.key
*.key1.5. Configure NestJS ConfigModule
Modify your main application module (src/app.module.ts) to load environment variables:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { VonageModule } from './vonage/vonage.module'; // We will create this
import { WebhooksModule } from './webhooks/webhooks.module'; // We will create this
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Make ConfigModule available globally
envFilePath: '.env', // Load variables from .env file
}),
VonageModule, // Add Vonage module
WebhooksModule, // Add Webhooks module
],
controllers: [AppController], // Default controller
providers: [AppService], // Default service
})
export class AppModule {}1.6. Vonage Account and Application Setup
Configure your Vonage account and application before proceeding.
Step 1: Access Your API Credentials
- Log in to the Vonage API Dashboard.
- Find your
VONAGE_API_KEYandVONAGE_API_SECRETon the main dashboard page. - Navigate to Settings to find the "API key signature secret" and copy your
VONAGE_API_SIGNATURE_SECRET.
Step 2: Create a Vonage Application
- Go to Applications → Create a new application.
- Name your application (e.g., "NestJS WhatsApp Service").
- Click Generate public and private key. A
private.keyfile downloads automatically. Save this file in your NestJS project root directory. UpdateVONAGE_PRIVATE_KEY_PATHin your.envfile if you save it elsewhere. - Enable the Messages capability. Leave webhook URLs blank for now (we'll add ngrok URLs next).
- Click Generate new application.
- Copy the Application ID and save it as
VONAGE_APPLICATION_IDin your.envfile.
Step 3: Set Up WhatsApp Sandbox for Testing
- Navigate to Messages API Sandbox in the dashboard sidebar.
- Follow instructions to activate the sandbox by sending a specific message from your WhatsApp number to the provided Vonage sandbox number. This allowlists your number for testing.
- Copy the Vonage Sandbox WhatsApp number (e.g.,
14157386102) and save it asVONAGE_WHATSAPP_NUMBERin your.envfile. - Leave Sandbox Webhooks blank for now. We'll update these with ngrok URLs.
Account Verification Requirements:
- Activation Time: Sandbox access is immediate. Production WhatsApp Business API accounts require business verification (typically 1–3 business days).
- Phone Verification: You must verify ownership of the phone number you'll use in production.
- Business Profile: Complete your business profile information before going live.
Production WhatsApp Business API Requirements:
- Phone Number Restrictions: The phone number cannot already be used on WhatsApp (personal or business app). Each business phone number can associate with only one API/BSP (Business Solution Provider) at a time. Meta limits businesses to 2 business phone numbers across all WhatsApp Business Accounts (WABAs) during initial registration.
- Messaging Limits: Initially, you can send business-initiated conversations to 250 unique customers in a rolling 24-hour period. This scales automatically based on phone number status, quality rating, and conversation frequency. Customer-initiated conversations (24-hour messaging windows) are unlimited.
- Business Verification: Optional but recommended to increase messaging limits. You can start messaging without completing business verification or display name reviews.
- Facebook Business Manager: Required for production onboarding. You need access to your business's Facebook Business Manager Account.
- Geographic Restrictions: WhatsApp Business Platform cannot send/receive messages to/from Ukraine (Crimea, Donetsk, Luhansk), Cuba, Iran, North Korea, or Syria.
- Pricing Model Change (2025): Meta is transitioning from Conversation Based Pricing (CBP) to Per Message Price (PMP) effective July 1, 2025. Vonage Platform Fees updated October 1, 2025.
Cost Estimation:
| Category | Cost Estimate | Notes |
|---|---|---|
| Sandbox Testing | Free | Includes 10 EUR test credit |
| Customer-Initiated Messages | USD 0.005–0.04/message | Varies by country |
| Business-Initiated Messages | USD 0.01–0.15/message | Higher cost, country-dependent |
| Template Message Reviews | Free | 1–3 business day approval |
1.7. Start ngrok
Open a new terminal window and run ngrok to expose port 3000:
ngrok http 3000Authenticate ngrok (First-Time Setup):
If you haven't authenticated ngrok:
- Sign up at ngrok.com.
- Get your authtoken from the dashboard.
- Run:
ngrok config add-authtoken YOUR_TOKEN_HERE
ngrok displays a forwarding URL (e.g., https://<unique-subdomain>.ngrok.io or https://<unique-subdomain>.ngrok-free.app). Copy this HTTPS URL.
Production Alternatives:
For production deployments, replace ngrok with:
- Cloud Platforms: AWS Lambda, Azure Functions, Google Cloud Run
- Traditional Hosting: Heroku, DigitalOcean, AWS EC2 with SSL certificates
- Reverse Proxies: Cloudflare Tunnel, Nginx with Let's Encrypt
1.8. Update Vonage Webhook URLs
Return to your Vonage dashboard and configure webhook endpoints:
Application Webhooks:
- Navigate to Applications → Your Application ("NestJS WhatsApp Service").
- Edit the Messages capability URLs:
- Inbound URL:
YOUR_NGROK_HTTPS_URL/webhooks/inbound - Status URL:
YOUR_NGROK_HTTPS_URL/webhooks/status
- Inbound URL:
- Save the changes.
Sandbox Webhooks:
- Navigate to Messages API Sandbox.
- Edit the Webhook URLs:
- Inbound URL:
YOUR_NGROK_HTTPS_URL/webhooks/inbound - Status URL:
YOUR_NGROK_HTTPS_URL/webhooks/status
- Inbound URL:
- Click Save webhooks.
Vonage now knows where to send incoming messages and status updates for your application and sandbox environment.
2. Implementing Core Functionality (Vonage Service)
Create a dedicated module and service to handle interactions with the Vonage SDK.
2.1. Generate Vonage Module and Service
Use the NestJS CLI to generate the necessary files:
nest g module vonage
nest g service vonage --no-spec # --no-spec skips generating a test file for nowThis creates src/vonage/vonage.module.ts and src/vonage/vonage.service.ts.
2.2. Configure VonageModule
Set up the module to export the service:
// src/vonage/vonage.module.ts
import { Module } from '@nestjs/common';
import { VonageService } from './vonage.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule], // Ensure ConfigService is available
providers: [VonageService],
exports: [VonageService], // Export the service so other modules can use it
})
export class VonageModule {}2.3. Implement VonageService
This service initializes the Vonage SDK client using credentials from environment variables and provides methods to send WhatsApp messages.
// src/vonage/vonage.service.ts
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Vonage } from '@vonage/server-sdk';
import { WhatsAppText } from '@vonage/messages';
import * as fs from 'fs';
@Injectable()
export class VonageService implements OnModuleInit {
private readonly logger = new Logger(VonageService.name);
private vonageClient: Vonage;
private vonageWhatsAppNumber: string;
private readonly MAX_RETRIES = 3;
private readonly RETRY_DELAY_MS = 1000;
constructor(private configService: ConfigService) {}
// Initialize the client when the module loads
onModuleInit() {
try {
const apiKey = this.configService.get<string>('VONAGE_API_KEY');
const apiSecret = this.configService.get<string>('VONAGE_API_SECRET');
const applicationId = this.configService.get<string>(
'VONAGE_APPLICATION_ID',
);
const privateKeyPath = this.configService.get<string>(
'VONAGE_PRIVATE_KEY_PATH',
);
// Check for optional sandbox host, default to production if not set
const apiHost = this.configService.get<string>('VONAGE_API_HOST');
this.vonageWhatsAppNumber = this.configService.get<string>(
'VONAGE_WHATSAPP_NUMBER',
);
// Validate required config
if (
!apiKey ||
!apiSecret ||
!applicationId ||
!privateKeyPath ||
!this.vonageWhatsAppNumber
) {
throw new Error('Missing required Vonage configuration in .env file');
}
// Ensure private key file exists
if (!fs.existsSync(privateKeyPath)) {
throw new Error(`Private key file not found at: ${privateKeyPath}`);
}
const privateKey = fs.readFileSync(privateKeyPath);
this.vonageClient = new Vonage(
{
apiKey: apiKey,
apiSecret: apiSecret,
applicationId: applicationId,
privateKey: privateKey,
},
// Optional: Specify API host for sandbox or specific region
// If VONAGE_API_HOST is not set in .env, it defaults to production
apiHost ? { apiHost: apiHost } : {},
);
this.logger.log('Vonage client initialized successfully.');
if (apiHost) {
this.logger.warn(`Vonage client using API host: ${apiHost}`);
} else {
this.logger.log('Vonage client using default production API host.');
}
} catch (error) {
this.logger.error('Failed to initialize Vonage client:', error.message);
throw new Error(`Vonage initialization failed: ${error.message}`);
}
}
/**
* Sends a WhatsApp text message with automatic retry logic.
* @param to The recipient's phone number (E.164 format, without leading '+').
* @param text The message content.
* @returns The message UUID on success.
* @throws Error if sending fails after retries.
*/
async sendWhatsAppTextMessage(to: string, text: string): Promise<string> {
if (!this.vonageClient) {
this.logger.error(
'Vonage client not initialized. Cannot send message.',
);
throw new Error('Vonage client not available.');
}
this.logger.log(`Attempting to send WhatsApp message to ${to}`);
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
try {
const response = await this.vonageClient.messages.send(
new WhatsAppText({
from: this.vonageWhatsAppNumber,
to: to, // E.164 format without '+'
text: text,
}),
);
this.logger.log(
`Message sent successfully to ${to}. Message UUID: ${response.messageUuid}`,
);
return response.messageUuid;
} catch (error) {
// Check for rate limiting
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'];
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : this.RETRY_DELAY_MS * attempt;
this.logger.warn(
`Rate limited. Attempt ${attempt}/${this.MAX_RETRIES}. Waiting ${waitTime}ms before retry.`,
);
if (attempt < this.MAX_RETRIES) {
await this.sleep(waitTime);
continue;
}
}
this.logger.error(`Error sending WhatsApp message to ${to}:`, error);
// Log detailed error info if available
if (error.response?.data) {
this.logger.error('Vonage API Error details:', error.response.data);
}
// Re-throw on final attempt
if (attempt === this.MAX_RETRIES) {
throw new Error(
`Failed to send WhatsApp message after ${this.MAX_RETRIES} attempts: ${
error.response?.data?.title || error.message
}`,
);
}
// Wait before retry
await this.sleep(this.RETRY_DELAY_MS * attempt);
}
}
}
/**
* Sends a WhatsApp message with media attachment.
* @param to The recipient's phone number (E.164 format, without leading '+').
* @param mediaUrl The URL of the media to send (must be publicly accessible).
* @param caption Optional caption for the media.
* @returns The message UUID on success.
*/
async sendWhatsAppMediaMessage(
to: string,
mediaUrl: string,
caption?: string,
): Promise<string> {
if (!this.vonageClient) {
throw new Error('Vonage client not available.');
}
this.logger.log(`Sending WhatsApp media message to ${to}`);
try {
const response = await this.vonageClient.messages.send({
channel: 'whatsapp',
message_type: 'image',
to: to,
from: this.vonageWhatsAppNumber,
image: {
url: mediaUrl,
caption: caption,
},
});
this.logger.log(
`Media message sent successfully to ${to}. Message UUID: ${response.messageUuid}`,
);
return response.messageUuid;
} catch (error) {
this.logger.error(`Error sending media message to ${to}:`, error);
throw new Error(
`Failed to send media message: ${
error.response?.data?.title || error.message
}`,
);
}
}
/**
* Sends a WhatsApp template message.
* @param to The recipient's phone number (E.164 format, without leading '+').
* @param templateName The name of the approved template.
* @param parameters Template parameters (if required).
* @returns The message UUID on success.
*/
async sendWhatsAppTemplateMessage(
to: string,
templateName: string,
parameters: string[] = [],
): Promise<string> {
if (!this.vonageClient) {
throw new Error('Vonage client not available.');
}
this.logger.log(`Sending WhatsApp template "${templateName}" to ${to}`);
try {
const response = await this.vonageClient.messages.send({
channel: 'whatsapp',
message_type: 'template',
to: to,
from: this.vonageWhatsAppNumber,
template: {
name: templateName,
parameters: parameters,
},
});
this.logger.log(
`Template message sent successfully to ${to}. Message UUID: ${response.messageUuid}`,
);
return response.messageUuid;
} catch (error) {
this.logger.error(`Error sending template message to ${to}:`, error);
throw new Error(
`Failed to send template message: ${
error.response?.data?.title || error.message
}`,
);
}
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}Key Features:
@Injectable(): Marks the class for NestJS dependency injection.Logger: NestJS built-in logger for informative console output.ConfigService: Injected to access environment variables safely.OnModuleInit: Interface ensuring theonModuleInitmethod runs once the host module initializes. This sets up theVonageclient.- Initialization Logic: Reads credentials from
.env, validates them, reads the private key file, and creates theVonageinstance. Includes error handling for missing configuration or files. Explicitly checks ifVONAGE_API_HOSTis set and logs whether it's using the sandbox or defaulting to production. sendWhatsAppTextMessage: Anasyncmethod that takes the recipient (to) number and messagetext. Uses the initializedvonageClient.messages.sendmethod with aWhatsAppTextobject. Includes retry logic for rate limiting and robust error handling.- Media and Template Support: Additional methods for sending images, documents, and template messages.
3. Building the Webhook Handler
Create the module and controller to handle incoming webhook requests from Vonage.
3.1. Generate Webhooks Module and Controller
nest g module webhooks
nest g controller webhooks --no-spec3.2. Configure WebhooksModule
This module needs access to VonageService to send replies:
// src/webhooks/webhooks.module.ts
import { Module } from '@nestjs/common';
import { WebhooksController } from './webhooks.controller';
import { ConfigModule } from '@nestjs/config';
import { VonageModule } from '../vonage/vonage.module'; // Import VonageModule
@Module({
imports: [
ConfigModule, // For accessing signature secret
VonageModule, // Make VonageService available
],
controllers: [WebhooksController],
})
export class WebhooksModule {}3.3. Create DTO Classes for Validation
Create Data Transfer Objects (DTOs) to validate webhook payloads:
Inbound Message DTO:
// src/webhooks/dto/inbound-message.dto.ts
import {
IsString,
IsNotEmpty,
IsOptional,
ValidateNested,
IsEnum,
} from 'class-validator';
import { Type } from 'class-transformer';
class FromDto {
@IsString()
@IsNotEmpty()
type: string;
@IsString()
@IsNotEmpty()
number: string;
}
class MessageContentDto {
@IsString()
@IsOptional()
text?: string;
@IsString()
@IsOptional()
image?: string;
@IsString()
@IsOptional()
caption?: string;
}
class MessageDto {
@ValidateNested()
@Type(() => MessageContentDto)
@IsOptional()
content?: MessageContentDto;
}
export class InboundMessageDto {
@IsString()
@IsNotEmpty()
message_uuid: string;
@IsString()
@IsNotEmpty()
@IsEnum(['text', 'image', 'audio', 'video', 'file', 'location', 'button', 'list'])
message_type: string;
@IsString()
@IsNotEmpty()
timestamp: string;
@ValidateNested()
@Type(() => FromDto)
from: FromDto;
@ValidateNested()
@Type(() => MessageDto)
@IsOptional()
message?: MessageDto;
@IsString()
@IsOptional()
channel?: string;
}Status Update DTO:
// src/webhooks/dto/status-update.dto.ts
import {
IsString,
IsNotEmpty,
IsOptional,
ValidateNested,
IsEnum,
} from 'class-validator';
import { Type } from 'class-transformer';
class ToDto {
@IsString()
@IsNotEmpty()
type: string;
@IsString()
@IsNotEmpty()
number: string;
}
class ErrorDto {
@IsString()
@IsOptional()
code?: string;
@IsString()
@IsOptional()
reason?: string;
}
export class StatusUpdateDto {
@IsString()
@IsNotEmpty()
message_uuid: string;
@IsString()
@IsNotEmpty()
@IsEnum(['submitted', 'delivered', 'read', 'failed', 'rejected', 'undeliverable'])
status: string;
@IsString()
@IsNotEmpty()
timestamp: string;
@ValidateNested()
@Type(() => ToDto)
to: ToDto;
@ValidateNested()
@Type(() => ErrorDto)
@IsOptional()
error?: ErrorDto;
@IsString()
@IsOptional()
channel?: string;
}3.4. Implement WebhooksController
This controller defines the /webhooks/inbound and /webhooks/status endpoints that Vonage calls. It handles POST requests, parses the JSON body, and verifies the JWT signature for security.
// src/webhooks/webhooks.controller.ts
import {
Controller,
Post,
Body,
Req,
Res,
HttpStatus,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response } from 'express';
import { verifySignature } from '@vonage/jwt';
import { VonageService } from '../vonage/vonage.service';
import { InboundMessageDto } from './dto/inbound-message.dto';
import { StatusUpdateDto } from './dto/status-update.dto';
@Controller('webhooks') // Base path for all routes in this controller
export class WebhooksController {
private readonly logger = new Logger(WebhooksController.name);
private readonly signatureSecret: string;
private readonly processedMessages = new Set<string>(); // Simple idempotency tracking
constructor(
private configService: ConfigService,
private vonageService: VonageService, // Inject VonageService
) {
this.signatureSecret = this.configService.get<string>(
'VONAGE_API_SIGNATURE_SECRET',
);
if (!this.signatureSecret) {
this.logger.error('VONAGE_API_SIGNATURE_SECRET is not set in .env');
throw new Error('Server configuration error: Missing signature secret.');
}
}
// --- Helper Method for Signature Verification ---
private verifyVonageSignature(req: Request): boolean {
try {
const authorizationHeader =
req.headers['authorization'] || req.headers['Authorization'];
if (!authorizationHeader || !authorizationHeader.startsWith('Bearer ')) {
this.logger.warn('Missing or invalid Authorization header');
return false;
}
const token = authorizationHeader.split(' ')[1];
if (!token) {
this.logger.warn('Bearer token missing in Authorization header');
return false;
}
const rawBody = (req as any).rawBody;
if (!rawBody) {
this.logger.error(
'Raw request body not available for signature verification. ' +
'Ensure rawBody is enabled in main.ts (Section 5.2).',
);
return false;
}
// Verify the JWT signature against the raw request body buffer
const isSignatureValid = verifySignature(
token,
this.signatureSecret,
rawBody,
);
if (!isSignatureValid) {
this.logger.warn('Invalid JWT signature received');
return false;
}
this.logger.log('Valid JWT signature verified using raw body');
return true;
} catch (error) {
this.logger.error('Error during signature verification:', error);
return false;
}
}
// --- Inbound Message Webhook ---
@Post('inbound')
async handleInboundMessage(
@Body() body: InboundMessageDto,
@Req() req: Request,
@Res() res: Response,
) {
this.logger.log('Received inbound message webhook');
// 1. Verify Signature (CRITICAL FOR SECURITY)
if (!this.verifyVonageSignature(req)) {
this.logger.error('Unauthorized inbound request: Invalid signature');
return res.status(HttpStatus.UNAUTHORIZED).send('Invalid signature');
}
// 2. Check for duplicate (idempotency)
if (this.processedMessages.has(body.message_uuid)) {
this.logger.warn(
`Duplicate message received: ${body.message_uuid}. Ignoring.`,
);
return res.status(HttpStatus.OK).send(); // Still return 200 OK
}
// Mark as processed
this.processedMessages.add(body.message_uuid);
// Clean up old entries (simple approach for demo – use Redis in production)
if (this.processedMessages.size > 10000) {
const iterator = this.processedMessages.values();
for (let i = 0; i < 1000; i++) {
this.processedMessages.delete(iterator.next().value);
}
}
// 3. Process the message
const messageType = body.message_type;
const fromNumber = body.from.number;
const messageContent = body.message?.content?.text;
try {
switch (messageType) {
case 'text':
if (fromNumber && messageContent) {
this.logger.log(
`Received text message "${messageContent}" from ${fromNumber}`,
);
// Example: Simple echo reply
const replyText = `You sent: "${messageContent}"`;
const messageUuid = await this.vonageService.sendWhatsAppTextMessage(
fromNumber,
replyText,
);
this.logger.log(
`Reply sent to ${fromNumber}. Message UUID: ${messageUuid}`,
);
}
break;
case 'image':
const imageUrl = body.message?.content?.image;
const caption = body.message?.content?.caption;
this.logger.log(
`Received image from ${fromNumber}: ${imageUrl} (Caption: ${caption || 'none'})`,
);
// Handle image processing here
break;
case 'location':
this.logger.log(`Received location share from ${fromNumber}`);
// Handle location data here
break;
default:
this.logger.warn(
`Received unsupported message type: ${messageType} from ${fromNumber}`,
);
}
} catch (error) {
this.logger.error(`Failed to process message from ${fromNumber}:`, error);
// Still return 200 OK to prevent retries
}
// 4. Respond to Vonage (CRITICAL)
// Always send a 200 OK quickly to acknowledge receipt
res.status(HttpStatus.OK).send();
}
// --- Message Status Webhook ---
@Post('status')
handleMessageStatus(
@Body() body: StatusUpdateDto,
@Req() req: Request,
@Res() res: Response,
) {
this.logger.log('Received message status webhook');
// 1. Verify Signature (CRITICAL FOR SECURITY)
if (!this.verifyVonageSignature(req)) {
this.logger.error('Unauthorized status request: Invalid signature');
return res.status(HttpStatus.UNAUTHORIZED).send('Invalid signature');
}
// 2. Process the status update
const messageUuid = body.message_uuid;
const status = body.status;
const timestamp = body.timestamp;
const toNumber = body.to.number;
this.logger.log(
`Status update for message ${messageUuid} to ${toNumber}: ${status} at ${timestamp}`,
);
// Add logic here: update message status in a database, trigger notifications, etc.
switch (status) {
case 'delivered':
this.logger.log(`Message ${messageUuid} delivered successfully.`);
break;
case 'read':
this.logger.log(`Message ${messageUuid} read by recipient.`);
break;
case 'failed':
case 'rejected':
this.logger.error(
`Message ${messageUuid} ${status}. Reason: ${
body.error?.reason || body.error?.code || 'Unknown'
}`,
body.error,
);
break;
default:
this.logger.log(`Message ${messageUuid} status: ${status}`);
}
// 3. Respond to Vonage (CRITICAL)
res.status(HttpStatus.OK).send();
}
}Key Features:
@Controller('webhooks'): Defines the base route path.@Post('inbound')/@Post('status'): Decorators to handle POST requests to/webhooks/inboundand/webhooks/status.@Body(),@Req(),@Res(): Decorators to inject the validated request body (typed with DTOs), Express request object, and Express response object.- Signature Verification: Retrieves the
Authorization: Bearer <token>header, extracts the JWT token, usesreq.rawBody(configured in Section 5.2), and verifies withverifySignature(token, secret, rawBody). - Idempotency: Tracks processed message UUIDs to prevent duplicate processing.
- Message Type Handling: Switch statement handles different message types (text, image, location).
- Status Processing: Logs and processes different status types with specific handling for failures.
- 200 OK Response: Both handlers always send
200 OKto prevent Vonage from retrying.
4. Running and Testing the Application
4.1. Ensure ngrok is Running
Keep the ngrok http 3000 terminal window open.
4.2. Start the NestJS Application
In your main project terminal:
npm run start:dev
# Or using yarn:
# yarn start:devLook for output indicating the Vonage client initialized and the server is listening:
[Nest] 12345 - 04/20/2025, 10:00:00 AM LOG [NestFactory] Starting Nest application…
[Nest] 12345 - 04/20/2025, 10:00:01 AM LOG [InstanceLoader] ConfigModule dependencies initialized
[Nest] 12345 - 04/20/2025, 10:00:02 AM LOG [VonageService] Vonage client initialized successfully.
[Nest] 12345 - 04/20/2025, 10:00:02 AM LOG [VonageService] Vonage client using default production API host.
[Nest] 12345 - 04/20/2025, 10:00:03 AM LOG [NestApplication] Nest application successfully started4.3. Test Sending an Inbound Message
- Open WhatsApp on the phone number you allowlisted in the Vonage Sandbox.
- Send a message (e.g., "Hello NestJS!") to the Vonage Sandbox WhatsApp number (
VONAGE_WHATSAPP_NUMBERfrom your.env).
4.4. Check Application Logs
You should see logs like:
[Nest] 12345 - 04/20/2025, 10:05:00 AM LOG [WebhooksController] Received inbound message webhook
[Nest] 12345 - 04/20/2025, 10:05:00 AM LOG [WebhooksController] Valid JWT signature verified using raw body
[Nest] 12345 - 04/20/2025, 10:05:00 AM LOG [WebhooksController] Received text message "Hello NestJS!" from 1xxxxxxxxxx
[Nest] 12345 - 04/20/2025, 10:05:00 AM LOG [VonageService] Attempting to send WhatsApp message to 1xxxxxxxxxx
[Nest] 12345 - 04/20/2025, 10:05:01 AM LOG [VonageService] Message sent successfully to 1xxxxxxxxxx. Message UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
[Nest] 12345 - 04/20/2025, 10:05:01 AM LOG [WebhooksController] Reply sent to 1xxxxxxxxxx. Message UUID: a1b2c3d4-e5f6-7890-abcd-ef12345678904.5. Check Your WhatsApp
You should receive the reply message: "You sent: "Hello NestJS!""
4.6. Check Status Updates
You'll see status webhook logs:
[Nest] 12345 - 04/20/2025, 10:05:05 AM LOG [WebhooksController] Received message status webhook
[Nest] 12345 - 04/20/2025, 10:05:05 AM LOG [WebhooksController] Valid JWT signature verified using raw body
[Nest] 12345 - 04/20/2025, 10:05:05 AM LOG [WebhooksController] Status update for message a1b2c3d4-e5f6-7890-abcd-ef1234567890 to 1xxxxxxxxxx: submitted at …
[Nest] 12345 - 04/20/2025, 10:05:06 AM LOG [WebhooksController] Received message status webhook
[Nest] 12345 - 04/20/2025, 10:05:06 AM LOG [WebhooksController] Valid JWT signature verified using raw body
[Nest] 12345 - 04/20/2025, 10:05:06 AM LOG [WebhooksController] Status update for message a1b2c3d4-e5f6-7890-abcd-ef1234567890 to 1xxxxxxxxxx: delivered at …
[Nest] 12345 - 04/20/2025, 10:05:07 AM LOG [WebhooksController] Message a1b2c3d4-e5f6-7890-abcd-ef1234567890 delivered successfully.Troubleshooting Common Issues:
| Issue | Cause | Solution |
|---|---|---|
| "Unauthorized: Invalid signature" | Raw body not configured | Complete Section 5.2 |
| "Private key file not found" | Incorrect path in .env | Verify VONAGE_PRIVATE_KEY_PATH points to private.key |
| No webhook received | ngrok URL not updated | Update webhook URLs in Vonage dashboard |
| Rate limit errors | Too many requests | Implement exponential backoff (already in service) |
| "Vonage client not available" | Initialization failed | Check .env variables and logs |
5. Enhancements for Production Readiness
The current setup works for development, but production applications require additional robustness.
5.1. Enable Request Validation with ValidationPipe
Configure NestJS to automatically validate webhook payloads using the DTOs we created.
Update your src/main.ts file:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
// Enable rawBody parsing for webhook signature verification
const app = await NestFactory.create(AppModule, {
rawBody: true, // <<< Enable Raw Body
});
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT', 3000);
// Enable global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip properties not in DTO
transform: true, // Automatically transform payloads to DTO instances
forbidNonWhitelisted: true, // Throw error if unknown properties are received
transformOptions: {
enableImplicitConversion: true, // Allow basic type conversions
},
}),
);
// Optional: Enable CORS if your setup requires it
// app.enableCors();
await app.listen(port);
Logger.log(`Application is running on: http://localhost:${port}`);
}
bootstrap();What This Does:
rawBody: true: Preserves the raw request body buffer needed for JWT signature verification.whitelist: true: Removes properties not defined in your DTOs, protecting against injection attacks.transform: true: Converts plain JavaScript objects to DTO class instances.forbidNonWhitelisted: true: Rejects requests with unexpected properties.
5.2. Configure Raw Body for Signature Verification
The rawBody: true option in NestFactory.create() automatically makes the raw body available at req.rawBody. This is required for the verifyVonageSignature method to work correctly.
Verification:
Add this log in your WebhooksController constructor to verify raw body is available:
constructor(
private configService: ConfigService,
private vonageService: VonageService,
) {
this.signatureSecret = this.configService.get<string>(
'VONAGE_API_SIGNATURE_SECRET',
);
if (!this.signatureSecret) {
this.logger.error('VONAGE_API_SIGNATURE_SECRET is not set in .env');
throw new Error('Server configuration error: Missing signature secret.');
}
// Verification: This will log when the controller initializes
this.logger.log('WebhooksController initialized with signature verification enabled');
}5.3. Implement Database Persistence
For production, persist message data and status updates to a database.
Install TypeORM and PostgreSQL Driver:
npm install @nestjs/typeorm typeorm pgCreate Message Entity:
// src/entities/message.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('messages')
export class Message {
@PrimaryColumn()
messageUuid: string;
@Column()
from: string;
@Column()
to: string;
@Column()
messageType: string;
@Column('text', { nullable: true })
content: string;
@Column({ default: 'submitted' })
status: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@Column('jsonb', { nullable: true })
metadata: any;
}Configure TypeORM in app.module.ts:
import { TypeOrmModule } from '@nestjs/typeorm';
import { Message } from './entities/message.entity';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: [Message],
synchronize: process.env.NODE_ENV !== 'production', // Disable in production
}),
TypeOrmModule.forFeature([Message]),
VonageModule,
WebhooksModule,
],
// …
})
export class AppModule {}5.4. Implement Queue-Based Processing
For high-volume applications, process webhooks asynchronously using a message queue.
Install Bull Queue:
npm install @nestjs/bull bull
npm install @types/bull --save-devConfigure Bull Module:
// app.module.ts
import { BullModule } from '@nestjs/bull';
@Module({
imports: [
// …
BullModule.forRoot({
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
},
}),
BullModule.registerQueue({
name: 'messages',
}),
// …
],
})
export class AppModule {}Create Queue Processor:
// src/webhooks/message.processor.ts
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
import { Logger } from '@nestjs/common';
import { VonageService } from '../vonage/vonage.service';
@Processor('messages')
export class MessageProcessor {
private readonly logger = new Logger(MessageProcessor.name);
constructor(private vonageService: VonageService) {}
@Process('inbound')
async handleInboundMessage(job: Job) {
const { from, content } = job.data;
this.logger.log(`Processing inbound message from ${from}`);
try {
const replyText = `You sent: "${content}"`;
await this.vonageService.sendWhatsAppTextMessage(from, replyText);
this.logger.log(`Reply sent to ${from}`);
} catch (error) {
this.logger.error(`Failed to process message from ${from}:`, error);
throw error; // Bull will retry
}
}
}5.5. Add Structured Logging
Replace Logger with a structured logging solution like Winston or Pino.
Install Winston:
npm install nest-winston winstonConfigure Winston:
// src/main.ts
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
rawBody: true,
logger: WinstonModule.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
}),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.json(),
}),
new winston.transports.File({
filename: 'logs/combined.log',
format: winston.format.json(),
}),
],
}),
});
// …
}5.6. Implement Rate Limiting
Protect your API from abuse with rate limiting.
Install Rate Limiter:
npm install @nestjs/throttlerConfigure Throttler:
// app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([{
ttl: 60000, // 60 seconds
limit: 10, // 10 requests per minute
}]),
// …
],
})
export class AppModule {}Apply to Webhooks:
// webhooks.controller.ts
import { Throttle } from '@nestjs/throttler';
@Controller('webhooks')
export class WebhooksController {
@Throttle({ default: { limit: 100, ttl: 60000 } }) // 100 requests per minute
@Post('inbound')
async handleInboundMessage(/* … */) {
// …
}
}5.7. Deployment Configuration
Environment Variables for Production:
Add these to your .env for production:
NODE_ENV=production
DB_HOST=your-db-host.com
DB_PORT=5432
DB_USERNAME=your_db_user
DB_PASSWORD=your_db_password
DB_DATABASE=vonage_whatsapp
REDIS_HOST=your-redis-host.com
REDIS_PORT=6379Docker Configuration:
Create Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main"]Docker Compose:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- postgres
- redis
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: vonage_whatsapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:5.8. Health Checks and Monitoring
Add Health Check Endpoint:
npm install @nestjs/terminusCreate Health Controller:
// src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
}5.9. Security Hardening Checklist
✓ Enable webhook signature verification (already implemented) ✓ Use HTTPS in production (remove ngrok) ✓ Validate all input with DTOs ✓ Implement rate limiting ✓ Use environment variables for secrets ✓ Enable CORS with specific origins only ✓ Implement request timeout limits ✓ Add helmet for security headers:
npm install helmet// main.ts
import helmet from 'helmet';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { rawBody: true });
app.use(helmet());
// …
}6. Testing Your Application
6.1. Unit Tests
Create unit tests for your services:
// src/vonage/vonage.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { VonageService } from './vonage.service';
describe('VonageService', () => {
let service: VonageService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
VonageService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
const config = {
VONAGE_API_KEY: 'test_key',
VONAGE_API_SECRET: 'test_secret',
VONAGE_APPLICATION_ID: 'test_app_id',
VONAGE_PRIVATE_KEY_PATH: './test.key',
VONAGE_WHATSAPP_NUMBER: '14155551234',
};
return config[key];
}),
},
},
],
}).compile();
service = module.get<VonageService>(VonageService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});6.2. Integration Tests
Test webhook endpoints with real payloads:
// test/webhooks.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('WebhooksController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/webhooks/inbound (POST)', () => {
return request(app.getHttpServer())
.post('/webhooks/inbound')
.send({
message_uuid: 'test-uuid-123',
message_type: 'text',
timestamp: '2025-04-20T10:00:00.000Z',
from: {
type: 'whatsapp',
number: '14155551234',
},
message: {
content: {
text: 'Test message',
},
},
})
.expect(200);
});
});6.3. Automated Testing with Postman
Create a Postman collection for manual and automated testing:
{
"info": {
"name": "Vonage WhatsApp NestJS",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Inbound Webhook",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"message_uuid\": \"{{$guid}}\",\n \"message_type\": \"text\",\n \"timestamp\": \"{{$timestamp}}\",\n \"from\": {\n \"type\": \"whatsapp\",\n \"number\": \"14155551234\"\n },\n \"message\": {\n \"content\": {\n \"text\": \"Hello World\"\n }\n }\n}"
},
"url": {
"raw": "{{base_url}}/webhooks/inbound",
"host": ["{{base_url}}"],
"path": ["webhooks", "inbound"]
}
}
}
]
}7. Production Deployment Guide
7.1. Deploy to Heroku
Install Heroku CLI:
curl https://cli-assets.heroku.com/install.sh | shDeploy:
heroku login
heroku create vonage-whatsapp-app
heroku addons:create heroku-postgresql:mini
heroku addons:create heroku-redis:mini
heroku config:set VONAGE_API_KEY=your_key
heroku config:set VONAGE_API_SECRET=your_secret
# … set other environment variables
git push heroku main7.2. Deploy to AWS (Elastic Beanstalk)
Install EB CLI:
pip install awsebcliInitialize and Deploy:
eb init -p node.js vonage-whatsapp-app
eb create vonage-whatsapp-env
eb setenv VONAGE_API_KEY=your_key VONAGE_API_SECRET=your_secret
eb deploy7.3. Deploy to Google Cloud Run
Build and Deploy:
gcloud builds submit --tag gcr.io/PROJECT_ID/vonage-whatsapp
gcloud run deploy vonage-whatsapp \
--image gcr.io/PROJECT_ID/vonage-whatsapp \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars "VONAGE_API_KEY=your_key,VONAGE_API_SECRET=your_secret"7.4. CI/CD with GitHub Actions
Create .github/workflows/deploy.yml:
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Deploy to Heroku
uses: akhileshns/heroku-deploy@v3.12.14
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: "vonage-whatsapp-app"
heroku_email: "your-email@example.com"Conclusion
You now have a production-ready WhatsApp integration built with Vonage and NestJS. This implementation includes:
✓ Secure webhook handling with JWT signature verification ✓ Robust error handling and retry logic ✓ Database persistence for message tracking ✓ Queue-based asynchronous processing ✓ Comprehensive logging and monitoring ✓ Rate limiting and security hardening ✓ Deployment configurations for multiple platforms ✓ Automated testing and CI/CD pipelines
Next Steps:
- Implement template messages for marketing campaigns
- Add support for WhatsApp interactive messages (buttons, lists)
- Build analytics dashboard for message metrics
- Implement conversation context management
- Add multi-language support with i18n
- Set up alerting for failed messages
- Implement message scheduling functionality
- Add support for WhatsApp Business API catalog features
For questions or issues, refer to:
Frequently Asked Questions
How to send WhatsApp messages with Node.js?
Use the Vonage Messages API with a Node.js framework like NestJS. This setup allows you to send WhatsApp messages programmatically by initializing the Vonage Node.js SDK and calling the `messages.send` method with a properly formatted payload including the recipient's number and your message content. This guide provides a step-by-step tutorial on how to implement such a service.
What is the Vonage Messages API?
The Vonage Messages API is a service provided by Vonage (formerly Nexmo) for sending and receiving messages across various channels, including WhatsApp. It handles the complexities of communicating with the WhatsApp platform, providing a simplified interface for developers to integrate messaging into their applications. You can send different types of messages, including text, images, files, and templates.
Why use NestJS for a WhatsApp service?
NestJS offers benefits like structured architecture via modules, dependency injection, and tools for building scalable Node.js applications. These features make the WhatsApp service easier to organize, test, and maintain, especially as the project grows more complex.
What are the prerequisites for the WhatsApp tutorial?
You will need Node.js version 18 or higher, a package manager (npm or yarn), a Vonage API account, ngrok for local development, and a WhatsApp-enabled device for testing. The Vonage API account is required for utilizing their service, ngrok creates a public URL for your local server during testing, and a device is needed for end-to-end verification.
How to set up Vonage API credentials?
Obtain your API key and secret from the Vonage API Dashboard, generate a private key when creating a Vonage application, and create an API key signature secret in Settings for webhook security. These credentials should be stored securely, such as in environment variables (.env file) and never exposed in code repositories.
How to receive WhatsApp messages in my app?
Set up webhooks in your Vonage application dashboard and handle inbound messages in NestJS. Vonage forwards incoming messages to your specified endpoint, which you can handle using a controller and service within your application's logic.
What is ngrok used for in WhatsApp development?
ngrok creates a temporary public URL that tunnels to your locally running NestJS server, allowing Vonage webhooks to reach your development environment. This is important because Vonage needs a public HTTPS endpoint to send webhook requests.
How to handle webhook security for WhatsApp?
Vonage uses JWT signatures to ensure webhooks originate from them. Verify this signature using `@vonage/jwt` package to prevent unauthorized requests from reaching your webhook endpoints. This is critical to prevent security vulnerabilities.
How to handle message status updates?
Use the /webhooks/status endpoint to receive updates on message delivery, read status, or failures. By processing status updates, you gain valuable insights into the message lifecycle, allowing you to keep your application informed of successes or issues.
How to create a Vonage application?
Log into the Vonage API Dashboard, go to 'Applications', and click 'Create a new application'. Provide a name, generate public and private keys (download and securely store the private key), enable the Messages capability, and set the webhook URLs for inbound messages and status updates.
How to use DTOs for validating webhook requests?
Install 'class-validator' and 'class-transformer' packages, define DTO classes with validation decorators, and enable a global `ValidationPipe` in your NestJS application. DTOs enhance data integrity and security by ensuring webhook data conforms to your expected structure.
What is the purpose of a `.env` file?
The `.env` file stores sensitive information like API keys, secrets, and application IDs, allowing you to keep these values out of your codebase. It's important for security best practices and should be added to `.gitignore` to prevent it from being accidentally committed to version control.
Where can I find a working example of the NestJS WhatsApp service?
The complete, working project code from this tutorial is available on a public GitHub repository (linked in the article). You can refer to it as a reference implementation or use it as a starting point for your own WhatsApp project.