Plivo WhatsApp Integration with NestJS: Complete TypeScript Guide (2025)
Learn how to integrate Plivo WhatsApp API with NestJS in 2025. Complete guide covering WhatsApp Business messages, webhook validation, interactive buttons, template messages, and the 24-hour window policy with real Node.js code examples.
Learn how to integrate Plivo WhatsApp API with NestJS to build a production-ready messaging backend. This comprehensive tutorial covers everything from sending WhatsApp Business messages to handling incoming webhooks with signature validation.
You'll build a fully functional NestJS WhatsApp integration that:
Sends templated WhatsApp messages to initiate business conversations
Sends free-form text and media messages within the 24-hour conversation window
Implements interactive WhatsApp messages with lists, buttons, and call-to-action URLs
Receives and validates incoming messages via Plivo webhooks with HMAC-SHA256 signature verification
Follows TypeScript best practices with DTOs, dependency injection, and error handling
This guide assumes you're familiar with Node.js and basic NestJS concepts.
How to Integrate Plivo WhatsApp with NestJS: Project Overview
Goal: Build a reliable NestJS backend service that sends and receives WhatsApp messages via the Plivo API. This integration works seamlessly with customer support platforms, notification systems, chatbots, and other business applications requiring WhatsApp Business API functionality.
Problem Solved: Manage WhatsApp communications programmatically in a structured, scalable, and maintainable way. The service abstracts Plivo API complexities within a dedicated NestJS module.
Technologies Used:
Node.js: JavaScript runtime environment
NestJS: Progressive Node.js framework for building efficient, scalable server-side applications. Provides modular architecture, dependency injection, and built-in configuration and validation support
Plivo Node.js SDK: Simplifies interaction with the Plivo REST API
Plivo Communications Platform: API infrastructure for sending and receiving WhatsApp messages
Plivo Auth ID and Auth Token: Find these on your Plivo Console dashboard homepage
WhatsApp-Enabled Plivo Number: Purchase a Plivo number and enable it for WhatsApp in the Plivo console (Messaging → WhatsApp → Senders). Link your WhatsApp Business Account (WABA) following Plivo's onboarding process
Approved WhatsApp Templates: Business-initiated conversations require pre-approved templates. Create and submit these via the Plivo console or WhatsApp Manager
(Optional) ngrok:Download here for testing incoming webhooks locally
1. Setting Up Your NestJS Project for Plivo WhatsApp
Initialize your NestJS project and install the Plivo SDK and required dependencies to enable WhatsApp messaging functionality.
Step 1: Create a New NestJS Project
Open your terminal and run:
Bash
nest new plivo-whatsapp-integration
cd plivo-whatsapp-integration
This creates a standard NestJS project structure.
Step 2: Install Dependencies
Install the Plivo Node.js SDK and NestJS configuration module:
@nestjs/config: Environment variables and configuration management
dotenv: Loads environment variables from .env into process.env
Step 3: Configure Environment Variables
Create a .env file in the project root directory:
.env
#.env
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
PLIVO_WHATSAPP_SENDER_NUMBER=+14155551234 # Your WhatsApp-enabled Plivo number
# Optional: Base URL for exposing webhook (useful with ngrok)
BASE_URL=http://localhost:3000
Important:
Replace YOUR_PLIVO_AUTH_ID and YOUR_PLIVO_AUTH_TOKEN with your actual Plivo console credentials
Replace +14155551234 with your WhatsApp-enabled Plivo number in E.164 format
Security: Never commit .env to version control. Add .env to your .gitignore file
Step 4: Setup Configuration Module
Modify src/app.module.ts to load and manage environment variables using @nestjs/config:
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { ConfigModule } from'@nestjs/config';
import { PlivoModule } from'./plivo/plivo.module'; // We will create this nextimport { WhatsappModule } from'./whatsapp/whatsapp.module'; // We will create this next@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Makes ConfigModule available globallyenvFilePath: '.env',
}),
PlivoModule, // Import Plivo ModuleWhatsappModule, // Import WhatsApp Module
],
controllers: [AppController],
providers: [AppService],
})
exportclassAppModule {}
ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }): Loads the .env file and makes configuration accessible throughout the application via ConfigService.
Project Structure (Initial):
Your project structure should now look something like this:
Text
plivo-whatsapp-integration/
├── node_modules/
├── src/
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── plivo/ <-- To be created
│ └── whatsapp/ <-- To be created
├── test/
├── .env <-- Your credentials
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── nest-cli.json
├── package.json
├── package-lock.json
├── README.md
├── tsconfig.build.json
└── tsconfig.json
2. Creating the Plivo WhatsApp Service in NestJS
Create a dedicated NestJS module and service to encapsulate all Plivo WhatsApp API interactions. This modular approach promotes code reusability and follows NestJS best practices for dependency injection.
Step 1: Generate the Plivo Module and Service
Use the NestJS CLI to generate the module and service files:
Bash
nest generate module plivo
nest generate service plivo --no-spec # --no-spec skips test file generation for now
This creates src/plivo/plivo.module.ts and src/plivo/plivo.service.ts.
Step 2: Implement the Plivo Service
Open src/plivo/plivo.service.ts and implement the Plivo client initialization:
TypeScript
// src/plivo/plivo.service.tsimport { Injectable, OnModuleInit, Logger } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import * as plivo from'plivo';
@Injectable()
exportclassPlivoServiceimplementsOnModuleInit {
privateclient: plivo.Client;
privatereadonly logger = newLogger(PlivoService.name);
privatesenderNumber: string;
constructor(privateconfigService: ConfigService) {}
onModuleInit() {
const authId = this.configService.get<string>('PLIVO_AUTH_ID');
const authToken = this.configService.get<string>('PLIVO_AUTH_TOKEN');
this.senderNumber = this.configService.get<string>('PLIVO_WHATSAPP_SENDER_NUMBER');
if (!authId || !authToken || !this.senderNumber) {
this.logger.error('Plivo Auth ID, Auth Token, or Sender Number is missing in environment variables. Plivo integration will not function.');
// Throw an error to prevent application startup if Plivo is criticalthrownewError('Plivo credentials or sender number missing. Application cannot start.');
// Alternatively, allow startup but log error (less safe if Plivo is essential):// return;
}
try {
this.client = new plivo.Client(authId, authToken);
this.logger.log('Plivo client initialized successfully.');
} catch (error) {
this.logger.error('Failed to initialize Plivo client:', error);
// Consider throwing here as well, as the app might be non-functionalthrownewError(`Plivo client initialization failed: ${error.message}`);
}
}
// Expose the client for advanced use cases if needed, or create specific methodsgetPlivoClient(): plivo.Client {
if (!this.client) {
// This should ideally not happen if onModuleInit throws on failurethis.logger.error('Attempted to get Plivo client, but it was not initialized. Check configuration and startup logs.');
thrownewError('Plivo client is not initialized. Check configuration.');
}
returnthis.client;
}
getSenderNumber(): string {
if (!this.senderNumber) {
// This should ideally not happen if onModuleInit throws on failurethrownewError('Plivo sender number is not configured.');
}
returnthis.senderNumber;
}
// We will add methods here to send different types of messages
}
Inject ConfigService to access environment variables
OnModuleInit initializes the Plivo client when the module loads
Retrieve PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and PLIVO_WHATSAPP_SENDER_NUMBER from configuration
Crucially: The service throws an error if credentials or the sender number are missing, preventing the application from starting in a non-functional state
Error handling covers initialization failures
Getters getPlivoClient() and getSenderNumber() provide access to the initialized client and sender number with availability checks
Step 3: Configure the Plivo Module
Open src/plivo/plivo.module.ts and ensure the service is provided and exported:
TypeScript
// src/plivo/plivo.module.tsimport { Module } from'@nestjs/common';
import { PlivoService } from'./plivo.service';
import { ConfigModule } from'@nestjs/config'; // Import ConfigModule if not global@Module({
imports: [ConfigModule], // Import ConfigModule here if it's not set globally in app.moduleproviders: [PlivoService],
exports: [PlivoService], // Export the service so other modules can use it
})
exportclassPlivoModule {}
Remember we already imported PlivoModule into AppModule in the previous section.
3. Sending WhatsApp Messages with Plivo in NestJS
Implement a controller and service methods to send various types of WhatsApp Business messages including templates, text, media, and interactive messages through the Plivo API.
Step 1: Generate the WhatsApp Module and Controller
Global ValidationPipe automatically validates incoming data against DTOs
CORS is enabled via app.enableCors()
Port is set from environment variable PORT or defaults to 3000
Expected webhook URL is logged for convenience
Step 5: Create DTOs for Sending Messages
Create a directory src/whatsapp/dto and add the following DTO files:
TypeScript
// src/whatsapp/dto/send-template.dto.tsimport { IsNotEmpty, IsString, IsPhoneNumber, IsObject, IsOptional, IsUrl, ValidateNested, IsArray } from'class-validator';
import { Type } from'class-transformer';
// NOTE: Define more specific component DTOs if needed, or rely on IsObject for flexibility.// Refer to Plivo documentation for the exact structure required for template components.classTemplateComponentHeader {
@IsString()
type: 'header'; // Example, adjust based on Plivo spec// Add other header properties and validation
}
classTemplateComponentBody {
@IsString()
type: 'body'; // Example// Add other body properties and validation
}
// Define ButtonComponent etc.exportclassSendTemplateDto {
@IsNotEmpty()
@IsPhoneNumber(null) // Use null for generic E.164 format validationreadonlydst: string; // Destination WhatsApp number@IsNotEmpty()
@IsString()
readonlytemplateName: string; // Name of the approved Plivo WhatsApp template@IsOptional()
@IsString()
readonlylanguage?: string = 'en_US'; // Language code (default: en_US)@IsOptional()
@IsArray() // Ensure it's an array@ValidateNested({ each: true }) // Validate each object in the array if you define nested DTOs@Type(() =>Object) // Basic type hint for transformation, replace Object with specific DTO if defined// Plivo expects a specific structure for template components based on the template definition.// Accepting a generic object array provides flexibility but less compile-time safety.// Consider creating detailed nested DTOs for components for stricter validation if desired.// Refer to Plivo's documentation for the required 'components' array structure.// Example: components: [{ type: 'body', parameters: [...] }, { type: 'header', ...}]readonlytemplateComponents?: Record<string, any>[]; // Expects an array of component objects@IsOptional()
@IsUrl()
readonlycallbackUrl?: string; // Optional status callback URL
}
Open src/whatsapp/whatsapp.controller.ts and define the API endpoints that will use the PlivoService.
TypeScript
// src/whatsapp/whatsapp.controller.tsimport { Controller, Post, Body, HttpCode, HttpStatus, Logger, UseFilters, Req, UnauthorizedException, RawBodyRequest } from'@nestjs/common';
import { PlivoService } from'../plivo/plivo.service';
import { SendTemplateDto } from'./dto/send-template.dto';
import { SendTextDto } from'./dto/send-text.dto';
import { SendMediaDto } from'./dto/send-media.dto';
import { SendInteractiveDto } from'./dto/send-interactive.dto';
import { ConfigService } from'@nestjs/config';
import * as plivo from'plivo';
import { Request } from'express'; // Import Request for webhook signature validation// Import an exception filter if you create one (See Section 5)// import { AllExceptionsFilter } from '../common/filters/all-exceptions.filter';@Controller('whatsapp')
// @UseFilters(new AllExceptionsFilter()) // Apply custom filter if created (Update path if needed)exportclassWhatsappController {
privatereadonly logger = newLogger(WhatsappController.name);
constructor(privatereadonlyplivoService: PlivoService,
privatereadonlyconfigService: ConfigService// Inject ConfigService for Auth Token) {}
@Post('/send/template')
@HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as sending is asynchronousasyncsendTemplateMessage(@Body() sendTemplateDto: SendTemplateDto) {
this.logger.log(`Received request to send template: ${sendTemplateDto.templateName} to ${sendTemplateDto.dst}`);
try {
const result = awaitthis.plivoService.sendWhatsAppTemplate(
sendTemplateDto.dst,
sendTemplateDto.templateName,
sendTemplateDto.language,
sendTemplateDto.templateComponents,
sendTemplateDto.callbackUrl,
);
// Return minimal confirmation, status updates via webhook are betterreturn { messageId: result.messageUuid[0], status: 'queued' };
} catch (error) {
// Error is logged in the service, controller can re-throw or return specific HTTP error// Consider using an Exception Filter for cleaner error handling (Section 5)this.logger.error(`Error in /send/template endpoint: ${error.message}`, error.stack);
throw error; // Let NestJS handle the error (default 500 or specific if thrown/filtered)
}
}
@Post('/send/text')
@HttpCode(HttpStatus.ACCEPTED)
asyncsendTextMessage(@Body() sendTextDto: SendTextDto) {
this.logger.log(`Received request to send text to ${sendTextDto.dst}`);
try {
const result = awaitthis.plivoService.sendWhatsAppText(
sendTextDto.dst,
sendTextDto.text,
sendTextDto.callbackUrl,
);
return { messageId: result.messageUuid[0], status: 'queued' };
} catch (error) {
this.logger.error(`Error in /send/text endpoint: ${error.message}`, error.stack);
throw error;
}
}
@Post('/send/media')
@HttpCode(HttpStatus.ACCEPTED)
asyncsendMediaMessage(@Body() sendMediaDto: SendMediaDto) {
this.logger.log(`Received request to send media to ${sendMediaDto.dst}`);
try {
const result = awaitthis.plivoService.sendWhatsAppMedia(
sendMediaDto.dst,
sendMediaDto.mediaUrls,
sendMediaDto.caption,
sendMediaDto.callbackUrl,
);
return { messageId: result.messageUuid[0], status: 'queued' };
} catch (error) {
this.logger.error(`Error in /send/media endpoint: ${error.message}`, error.stack);
throw error;
}
}
@Post('/send/interactive')
@HttpCode(HttpStatus.ACCEPTED)
asyncsendInteractiveMessage(@Body() sendInteractiveDto: SendInteractiveDto) {
this.logger.log(`Received request to send interactive message to ${sendInteractiveDto.dst}`);
try {
const result = awaitthis.plivoService.sendWhatsAppInteractive(
sendInteractiveDto.dst,
sendInteractiveDto.interactive,
sendInteractiveDto.callbackUrl,
);
return { messageId: result.messageUuid[0], status: 'queued' };
} catch (error) {
this.logger.error(`Error in /send/interactive endpoint: ${error.message}`, error.stack);
throw error;
}
}
// --- INCOMING WEBHOOK HANDLER ---@Post('/webhook/incoming')
@HttpCode(HttpStatus.OK) // Plivo expects a 200 OK for webhookshandleIncomingMessage(@Req() req: RawBodyRequest<Request>, @Body() payload: any) {
this.logger.log('Received incoming WhatsApp message webhook request.');
// --- Webhook Signature Validation ---// Plivo uses V3 signature validation with HMAC-SHA256// See: https://www.plivo.com/docs/voice/concepts/signature-validationconst signature = req.headers['x-plivo-signature-v3'] asstring;
const nonce = req.headers['x-plivo-signature-v3-nonce'] asstring;
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
const authToken = this.configService.get<string>('PLIVO_AUTH_TOKEN');
// IMPORTANT: Plivo's validateV3Signature requires the raw body.// Ensure rawBody: true is set in main.ts NestFactory.create() optionsconst rawBody = req.rawBody;
if (!rawBody) {
this.logger.error('Raw body not available for webhook signature validation. Ensure rawBody: true is configured in main.ts.');
thrownewError('Webhook validation configuration error: Raw body missing.');
}
if (!signature || !nonce) {
this.logger.warn('Missing Plivo signature headers. Rejecting request.');
thrownewUnauthorizedException('Missing signature');
}
try {
const valid = plivo.validateV3Signature(url, nonce, signature, rawBody.toString('utf8'), authToken);
if (!valid) {
this.logger.warn('Invalid Plivo signature. Rejecting request.');
thrownewUnauthorizedException('Invalid signature');
}
this.logger.log('Plivo webhook signature validated successfully.');
} catch (error) {
this.logger.error(`Error during signature validation: ${error.message}`);
thrownewUnauthorizedException('Signature validation failed');
}
// --- Process Payload (Only if signature is valid) ---this.logger.log('Processing incoming WhatsApp message payload:');
this.logger.debug(`Payload: ${JSON.stringify(payload, null, 2)}`);
// Basic Payload Parsing (Consult Plivo docs for definitive structure)const fromNumber = payload.From;
const toNumber = payload.To;
const messageUuid = payload.MessageUUID;
consttype = payload.Type; // e.g., 'text', 'media', 'location'const eventType = payload.EventType; // e.g., 'message_delivered', 'message_read', 'interactive'this.logger.log(`Incoming event ${eventType || type} (${messageUuid}) from ${fromNumber} to ${toNumber}`);
// Handle Different Message Types/Events (Add your business logic)if (type === 'text') {
const text = payload.Text;
this.logger.log(`Text received: "${text}"`);
// ** ACTION: Implement your business logic here **// Example: Route to a chatbot, save to DB, trigger a reply
} elseif (type === 'media') {
const mediaUrl = payload.MediaUrl;
const mediaContentType = payload.MediaContentType;
const caption = payload.Text; // Caption might be in Text fieldthis.logger.log(`Media received: ${mediaContentType} at ${mediaUrl}, Caption: "${caption || ''}"`);
// ** ACTION: Implement logic to handle media (e.g., download, analyze) **
} elseif (type === 'location') {
const latitude = payload.Latitude;
const longitude = payload.Longitude;
this.logger.log(`Location received: Lat ${latitude}, Lon ${longitude}`);
// ** ACTION: Handle location data **
} elseif (eventType === 'interactive' && payload.Interactive) {
// Handle replies from interactive messages (buttons, lists)const interactiveData = payload.Interactive;
const interactiveType = interactiveData.type; // 'button_reply' or 'list_reply'this.logger.log(`Interactive reply received: Type - ${interactiveType}`);
if (interactiveType === 'button_reply') {
const buttonId = interactiveData.button_reply?.id;
const buttonTitle = interactiveData.button_reply?.title;
this.logger.log(`Button Clicked: ID='${buttonId}', Title='${buttonTitle}'`);
// ** ACTION: Handle button click based on ID **
} elseif (interactiveType === 'list_reply') {
const listItemId = interactiveData.list_reply?.id;
const listItemTitle = interactiveData.list_reply?.title;
const listItemDescription = interactiveData.list_reply?.description;
this.logger.log(`List Item Selected: ID='${listItemId}', Title='${listItemTitle}', Desc='${listItemDescription}'`);
// ** ACTION: Handle list selection based on ID **
}
} elseif (eventType && eventType.startsWith('message_')) {
// Handle status updates (delivered, read, failed, etc.)this.logger.log(`Message status update: ${eventType} for UUID ${messageUuid}`);
// ** ACTION: Update message status in your database if tracking **
} else {
this.logger.log(`Received unhandled message type '${type}' or event type '${eventType}'.`);
}
// ** IMPORTANT: Always return 200 OK quickly to Plivo **// Perform time-consuming actions asynchronously (e.g., using queues or background jobs)// If you don't return 200 OK promptly, Plivo might retry the webhook.
}
}
WhatsApp Business Message Templates: A Complete Guide
WhatsApp Business API requires pre-approved message templates for business-initiated conversations outside the 24-hour customer service window. Understanding these templates is essential for proper implementation and cost optimization.
Meta categorizes templates into three types, each with different pricing and use cases:
Utility Templates: Used for specific transaction or account updates (order confirmations, shipping notifications, appointment reminders, account alerts). These messages provide important information users have opted to receive.
Authentication Templates: Used for one-time passwords (OTP) and two-factor authentication. These must include security disclaimers and are typically the lowest cost option.
Marketing Templates: Used for promotional content, offers, announcements, newsletters, and other marketing communications. These have higher pricing and stricter approval requirements.
Template Creation: Create templates via WhatsApp Manager or the Plivo console. Meta must approve templates before use – this typically takes a few hours but can take up to 24 hours.
Important Notes:
Template names and language codes are case-sensitive
Components (header, body, buttons) support dynamic variables via the parameters array
Incorrectly categorized templates may be rejected during the approval process
WhatsApp Pricing Model (Updated July 2025)
As of July 1, 2025, WhatsApp transitioned from conversation-based pricing to a per-message pricing model. This significantly changed how businesses are charged for WhatsApp communications.
Key Pricing Changes:
Per-Message Billing: Each template message delivered is charged based on:
24-Hour Customer Service Window: When a customer messages your business, a 24-hour window opens. During this period:
Free: All free-form messages (text, media replies, bot responses)
Free: Utility template messages (newly made free within this window)
Charged: Marketing and Authentication templates (always charged)
72-Hour Free Entry Point: Messages from Click-to-WhatsApp Ads or Facebook Page buttons open a 72-hour free window for all message types (you must respond within 24 hours for the window to activate).
No More Free Tier: The previous 1,000 free conversations per month no longer exists.
How do I send WhatsApp messages with Plivo in NestJS?
To send WhatsApp messages with Plivo in NestJS: Install the Plivo Node.js SDK (npm install plivo), create a PlivoService that initializes the client with your Auth ID and Token, then use client.messages.create() with type: 'whatsapp' parameter. Inject the PlivoService into your controller using NestJS dependency injection and call your service methods from API endpoints.
What is the WhatsApp 24-hour conversation window and how does it work?
The WhatsApp 24-hour conversation window is a policy that allows businesses to send free-form messages (text, media, interactive) only within 24 hours after a user initiates contact or replies. Outside this window, you must use pre-approved message templates to start new conversations. As of July 2025, utility templates are free within this window, while marketing and authentication templates are always charged.
How do I validate Plivo webhook signatures in NestJS for WhatsApp messages?
To validate Plivo webhook signatures in NestJS, call Plivo's validateV3Signature() function with the raw request body, nonce from X-Plivo-Signature-V3-Nonce header, signature from X-Plivo-Signature-V3 header, full webhook URL, and your Auth Token. You must preserve the raw body by setting rawBody: true in NestFactory.create() options in main.ts. This HMAC-SHA256 validation ensures webhooks are authentic and from Plivo. See the NestJS raw body documentation for implementation details.
What types of WhatsApp messages can I send with Plivo API?
Plivo WhatsApp API supports multiple message types including: templated messages for business-initiated conversations, plain text messages, media messages (images, videos, documents, audio files), interactive messages with quick reply buttons, selection lists, and call-to-action URLs, plus location messages. All message types work through the WhatsApp Business API and require proper authentication.
Do I need a WhatsApp Business Account to use Plivo WhatsApp API?
Yes, you need a WhatsApp Business Account (WABA) to use Plivo WhatsApp API. First, purchase a Plivo phone number, then enable it for WhatsApp messaging in the Plivo console under Messaging → WhatsApp → Senders. Link this number to your WhatsApp Business Account following Plivo's onboarding process. You must also create and get Meta approval for message templates before sending business-initiated messages outside the 24-hour window.
How do I handle incoming WhatsApp messages in NestJS with Plivo?
To handle incoming WhatsApp messages in NestJS: Create a webhook endpoint (e.g., /whatsapp/webhook/incoming) decorated with @Post() that accepts POST requests from Plivo. Validate the webhook signature using plivo.validateV3Signature() with the raw request body, parse the payload to extract message content and type (text, media, interactive reply, location), then implement your business logic to process and respond to messages. Always return HTTP 200 OK quickly to prevent webhook retries.
What's the difference between template and free-form WhatsApp messages?
Template messages use pre-approved formats with placeholders and initiate conversations outside the 24-hour window. Free-form messages allow custom text, media, and interactive elements but can only be sent as replies within active conversation windows.
How much does it cost to send WhatsApp messages through Plivo?
As of July 1, 2025, WhatsApp charges per message delivered rather than per conversation. Messages within the 24-hour customer service window are free (including utility templates). Template pricing varies by country and category (utility, authentication, marketing), ranging from $0.0016 to $0.1570+ per message. See the pricing section for detailed rates.
What are the character limits for WhatsApp messages?
Integrate the Plivo Node.js SDK and use the provided methods within the Plivo service to send various WhatsApp messages. You'll need a Plivo account, WhatsApp-enabled number, and approved templates for business-initiated conversations. The code examples demonstrate sending templated, text, media, and interactive messages via function calls handling the Plivo API interaction details.
The Plivo Node.js SDK simplifies interaction with the Plivo communications API. It provides convenient functions for sending messages, making API calls, and managing other communication tasks within your NestJS application, abstracting away low-level details.
WhatsApp enforces pre-approved message templates for business-initiated conversations to prevent spam and unwanted messages. Businesses must submit templates for review and approval by WhatsApp before sending initial outbound messages to customers. Text, media, and interactive messages are allowed only in the 24-hour response window following a customer-initiated message.
WhatsApp template messages are required for any business-initiated conversations outside the 24-hour customer service window. They are essential for sending notifications, alerts, or initiating contact where the user hasn't messaged your business first.
Set up a webhook endpoint in your NestJS application to receive incoming WhatsApp messages. Configure Plivo to send webhook notifications to this endpoint. Then implement request handling logic to process incoming message data.
Obtain Plivo credentials (Auth ID and Auth Token) from the Plivo console, install the Plivo Node.js SDK (npm install plivo), initialize a Plivo client, and use the client's methods to send and receive WhatsApp messages via the API.
The example project creates a modular structure with a dedicated plivo module containing a service for Plivo interactions and a whatsapp module with a controller for handling endpoints. This design promotes code organization and separation of concerns.
Use NestJS's ConfigModule along with the dotenv package to load environment variables. Store sensitive credentials like your Plivo Auth ID, Auth Token, and WhatsApp Sender Number in a .env file, ensuring this file is not committed to version control.
Validate the Plivo webhook signature to ensure requests are genuinely from Plivo. Use plivo.validateV3Signature to validate against the signature and nonce provided in the X-Plivo-Signature-V3 and X-Plivo-Signature-V3-Nonce headers of the webhook requests.
Yes, the Plivo API supports sending various WhatsApp message types, including templated messages, free-form text and media messages (images, videos, documents), and interactive messages with buttons, lists, and call-to-actions. The provided tutorial shows how to send different message types using functions like sendWhatsAppTemplate, sendWhatsAppText, sendWhatsAppMedia, and sendWhatsAppInteractive.
Implement proper error handling using try-catch blocks around Plivo API calls. Handle Plivo-specific errors, such as invalid numbers or unapproved templates. Consider creating custom exception filters in NestJS to handle and format error responses consistently.
Use the sendWhatsAppText function after initializing the Plivo client. Ensure free-form messages are sent only as replies within a 24-hour window initiated by the user. Provide the destination number and message text as parameters.
The key components are the user/client application, the NestJS backend application, the Plivo API, and the WhatsApp network. Data flows from the user to your backend, then to Plivo, and finally to WhatsApp. Webhooks from Plivo notify your backend about message status updates and incoming messages.
Use a tool like ngrok to create a secure tunnel that exposes your local development server to the internet. This allows Plivo webhooks to reach your application during testing. Configure the BASE_URL in your .env file to match the ngrok URL.
Learn how to integrate Plivo WhatsApp API with NestJS to build a production-ready messaging backend. This comprehensive tutorial covers everything from sending WhatsApp Business messages to handling incoming webhooks with signature validation.
You'll build a fully functional NestJS WhatsApp integration that:
This guide assumes you're familiar with Node.js and basic NestJS concepts.
How to Integrate Plivo WhatsApp with NestJS: Project Overview
Goal: Build a reliable NestJS backend service that sends and receives WhatsApp messages via the Plivo API. This integration works seamlessly with customer support platforms, notification systems, chatbots, and other business applications requiring WhatsApp Business API functionality.
Problem Solved: Manage WhatsApp communications programmatically in a structured, scalable, and maintainable way. The service abstracts Plivo API complexities within a dedicated NestJS module.
Technologies Used:
System Architecture:
Prerequisites:
npm install -g @nestjs/cli1. Setting Up Your NestJS Project for Plivo WhatsApp
Initialize your NestJS project and install the Plivo SDK and required dependencies to enable WhatsApp messaging functionality.
Step 1: Create a New NestJS Project
Open your terminal and run:
nest new plivo-whatsapp-integration cd plivo-whatsapp-integrationThis creates a standard NestJS project structure.
Step 2: Install Dependencies
Install the Plivo Node.js SDK and NestJS configuration module:
npm install plivo @nestjs/config dotenv npm install --save-dev @types/node # Ensure latest Node typesplivo: Official Plivo SDK for Node.js@nestjs/config: Environment variables and configuration managementdotenv: Loads environment variables from.envintoprocess.envStep 3: Configure Environment Variables
Create a
.envfile in the project root directory:Important:
YOUR_PLIVO_AUTH_IDandYOUR_PLIVO_AUTH_TOKENwith your actual Plivo console credentials+14155551234with your WhatsApp-enabled Plivo number in E.164 format.envto version control. Add.envto your.gitignorefileStep 4: Setup Configuration Module
Modify
src/app.module.tsto load and manage environment variables using@nestjs/config:// src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import { PlivoModule } from './plivo/plivo.module'; // We will create this next import { WhatsappModule } from './whatsapp/whatsapp.module'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Makes ConfigModule available globally envFilePath: '.env', }), PlivoModule, // Import Plivo Module WhatsappModule, // Import WhatsApp Module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }): Loads the.envfile and makes configuration accessible throughout the application viaConfigService.Project Structure (Initial):
Your project structure should now look something like this:
2. Creating the Plivo WhatsApp Service in NestJS
Create a dedicated NestJS module and service to encapsulate all Plivo WhatsApp API interactions. This modular approach promotes code reusability and follows NestJS best practices for dependency injection.
Step 1: Generate the Plivo Module and Service
Use the NestJS CLI to generate the module and service files:
nest generate module plivo nest generate service plivo --no-spec # --no-spec skips test file generation for nowThis creates
src/plivo/plivo.module.tsandsrc/plivo/plivo.service.ts.Step 2: Implement the Plivo Service
Open
src/plivo/plivo.service.tsand implement the Plivo client initialization:// src/plivo/plivo.service.ts import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as plivo from 'plivo'; @Injectable() export class PlivoService implements OnModuleInit { private client: plivo.Client; private readonly logger = new Logger(PlivoService.name); private senderNumber: string; constructor(private configService: ConfigService) {} onModuleInit() { const authId = this.configService.get<string>('PLIVO_AUTH_ID'); const authToken = this.configService.get<string>('PLIVO_AUTH_TOKEN'); this.senderNumber = this.configService.get<string>('PLIVO_WHATSAPP_SENDER_NUMBER'); if (!authId || !authToken || !this.senderNumber) { this.logger.error('Plivo Auth ID, Auth Token, or Sender Number is missing in environment variables. Plivo integration will not function.'); // Throw an error to prevent application startup if Plivo is critical throw new Error('Plivo credentials or sender number missing. Application cannot start.'); // Alternatively, allow startup but log error (less safe if Plivo is essential): // return; } try { this.client = new plivo.Client(authId, authToken); this.logger.log('Plivo client initialized successfully.'); } catch (error) { this.logger.error('Failed to initialize Plivo client:', error); // Consider throwing here as well, as the app might be non-functional throw new Error(`Plivo client initialization failed: ${error.message}`); } } // Expose the client for advanced use cases if needed, or create specific methods getPlivoClient(): plivo.Client { if (!this.client) { // This should ideally not happen if onModuleInit throws on failure this.logger.error('Attempted to get Plivo client, but it was not initialized. Check configuration and startup logs.'); throw new Error('Plivo client is not initialized. Check configuration.'); } return this.client; } getSenderNumber(): string { if (!this.senderNumber) { // This should ideally not happen if onModuleInit throws on failure throw new Error('Plivo sender number is not configured.'); } return this.senderNumber; } // We will add methods here to send different types of messages }ConfigServiceto access environment variablesOnModuleInitinitializes the Plivo client when the module loadsPLIVO_AUTH_ID,PLIVO_AUTH_TOKEN, andPLIVO_WHATSAPP_SENDER_NUMBERfrom configurationgetPlivoClient()andgetSenderNumber()provide access to the initialized client and sender number with availability checksStep 3: Configure the Plivo Module
Open
src/plivo/plivo.module.tsand ensure the service is provided and exported:// src/plivo/plivo.module.ts import { Module } from '@nestjs/common'; import { PlivoService } from './plivo.service'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule if not global @Module({ imports: [ConfigModule], // Import ConfigModule here if it's not set globally in app.module providers: [PlivoService], exports: [PlivoService], // Export the service so other modules can use it }) export class PlivoModule {}Remember we already imported
PlivoModuleintoAppModulein the previous section.3. Sending WhatsApp Messages with Plivo in NestJS
Implement a controller and service methods to send various types of WhatsApp Business messages including templates, text, media, and interactive messages through the Plivo API.
Step 1: Generate the WhatsApp Module and Controller
This creates
src/whatsapp/whatsapp.module.tsandsrc/whatsapp/whatsapp.controller.ts.Step 2: Configure the WhatsApp Module
Open
src/whatsapp/whatsapp.module.tsand import thePlivoModule:// src/whatsapp/whatsapp.module.ts import { Module } from '@nestjs/common'; import { WhatsappController } from './whatsapp.controller'; import { PlivoModule } from '../plivo/plivo.module'; // Import PlivoModule import { ConfigModule } from '@nestjs/config'; // Import if needed @Module({ imports: [PlivoModule, ConfigModule], // Make PlivoService available controllers: [WhatsappController], }) export class WhatsappModule {}Remember we already imported
WhatsappModuleintoAppModule.Step 3: Install Validation Dependencies
Use DTOs (Data Transfer Objects) with decorators to validate incoming request bodies.
Step 4: Enable Global Validation Pipe and Raw Body Support
Enable automatic validation for all incoming requests and configure raw body access for webhook signature validation in
src/main.ts:// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import type { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { // Enable rawBody option for webhook signature validation // See: https://docs.nestjs.com/faq/raw-body const app = await NestFactory.create<NestExpressApplication>(AppModule, { rawBody: true, // Required for Plivo webhook signature validation }); const logger = new Logger('Bootstrap'); // Enable CORS if your frontend is on a different domain app.enableCors(); // Global Validation Pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties that don't have decorators forbidNonWhitelisted: true, // Throw errors for non-whitelisted properties transform: true, // Automatically transform payloads to DTO instances transformOptions: { enableImplicitConversion: true, // Allow basic type conversions }, })); const configService = app.get(ConfigService); const port = configService.get<number>('PORT') || 3000; await app.listen(port); logger.log(`Application listening on port ${port}`); const baseUrl = configService.get<string>('BASE_URL') || `http://localhost:${port}`; logger.log(`Expected WhatsApp Webhook URL for Plivo: ${baseUrl}/whatsapp/webhook/incoming`); } bootstrap();rawBody: truewhen creating the application. This is required for Plivo webhook signature validation usingplivo.validateV3Signature(). See NestJS raw body documentation for detailsValidationPipeautomatically validates incoming data against DTOsapp.enableCors()PORTor defaults to 3000Step 5: Create DTOs for Sending Messages
Create a directory
src/whatsapp/dtoand add the following DTO files:// src/whatsapp/dto/send-template.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber, IsObject, IsOptional, IsUrl, ValidateNested, IsArray } from 'class-validator'; import { Type } from 'class-transformer'; // NOTE: Define more specific component DTOs if needed, or rely on IsObject for flexibility. // Refer to Plivo documentation for the exact structure required for template components. class TemplateComponentHeader { @IsString() type: 'header'; // Example, adjust based on Plivo spec // Add other header properties and validation } class TemplateComponentBody { @IsString() type: 'body'; // Example // Add other body properties and validation } // Define ButtonComponent etc. export class SendTemplateDto { @IsNotEmpty() @IsPhoneNumber(null) // Use null for generic E.164 format validation readonly dst: string; // Destination WhatsApp number @IsNotEmpty() @IsString() readonly templateName: string; // Name of the approved Plivo WhatsApp template @IsOptional() @IsString() readonly language?: string = 'en_US'; // Language code (default: en_US) @IsOptional() @IsArray() // Ensure it's an array @ValidateNested({ each: true }) // Validate each object in the array if you define nested DTOs @Type(() => Object) // Basic type hint for transformation, replace Object with specific DTO if defined // Plivo expects a specific structure for template components based on the template definition. // Accepting a generic object array provides flexibility but less compile-time safety. // Consider creating detailed nested DTOs for components for stricter validation if desired. // Refer to Plivo's documentation for the required 'components' array structure. // Example: components: [{ type: 'body', parameters: [...] }, { type: 'header', ...}] readonly templateComponents?: Record<string, any>[]; // Expects an array of component objects @IsOptional() @IsUrl() readonly callbackUrl?: string; // Optional status callback URL }// src/whatsapp/dto/send-text.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber, IsOptional, IsUrl } from 'class-validator'; export class SendTextDto { @IsNotEmpty() @IsPhoneNumber(null) readonly dst: string; @IsNotEmpty() @IsString() readonly text: string; @IsOptional() @IsUrl() readonly callbackUrl?: string; }// src/whatsapp/dto/send-media.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber, IsUrl, IsArray, IsOptional } from 'class-validator'; export class SendMediaDto { @IsNotEmpty() @IsPhoneNumber(null) readonly dst: string; @IsNotEmpty() @IsArray() @IsUrl({}, { each: true }) // Validate each item in the array is a URL readonly mediaUrls: string[]; // Array containing URL(s) of the media @IsOptional() @IsString() readonly caption?: string; // Optional caption for the media @IsOptional() @IsUrl() readonly callbackUrl?: string; }// src/whatsapp/dto/send-interactive.dto.ts import { IsNotEmpty, IsPhoneNumber, IsObject, IsOptional, IsUrl } from 'class-validator'; export class SendInteractiveDto { @IsNotEmpty() @IsPhoneNumber(null) readonly dst: string; @IsNotEmpty() @IsObject() // Plivo expects a specific structure for the interactive payload (buttons, lists, etc.). // Accepting a generic object provides flexibility. For stricter validation, create detailed // nested DTOs reflecting Plivo's interactive message structure. // Refer to Plivo's documentation for the required 'interactive' object structure. readonly interactive: Record<string, any>; @IsOptional() @IsUrl() readonly callbackUrl?: string; }Step 6: Add Sending Methods to PlivoService
Now, add the methods to
src/plivo/plivo.service.tsto handle the actual API calls using the Plivo client.// src/plivo/plivo.service.ts import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as plivo from 'plivo'; // Keep existing constructor, onModuleInit, getPlivoClient, getSenderNumber @Injectable() export class PlivoService implements OnModuleInit { private client: plivo.Client; private readonly logger = new Logger(PlivoService.name); private senderNumber: string; constructor(private configService: ConfigService) {} onModuleInit() { const authId = this.configService.get<string>('PLIVO_AUTH_ID'); const authToken = this.configService.get<string>('PLIVO_AUTH_TOKEN'); this.senderNumber = this.configService.get<string>('PLIVO_WHATSAPP_SENDER_NUMBER'); if (!authId || !authToken || !this.senderNumber) { this.logger.error('Plivo Auth ID, Auth Token, or Sender Number is missing in environment variables. Plivo integration will not function.'); throw new Error('Plivo credentials or sender number missing. Application cannot start.'); } try { this.client = new plivo.Client(authId, authToken); this.logger.log('Plivo client initialized successfully.'); } catch (error) { this.logger.error('Failed to initialize Plivo client:', error); throw new Error(`Plivo client initialization failed: ${error.message}`); } } getPlivoClient(): plivo.Client { if (!this.client) { this.logger.error('Attempted to get Plivo client, but it was not initialized. Check configuration and startup logs.'); throw new Error('Plivo client is not initialized. Check configuration.'); } return this.client; } getSenderNumber(): string { if (!this.senderNumber) { throw new Error('Plivo sender number is not configured.'); } return this.senderNumber; } // --- MESSAGE SENDING METHODS --- async sendWhatsAppTemplate( dst: string, templateName: string, language: string = 'en_US', components?: Record<string, any>[], // Expecting array of components callbackUrl?: string, ): Promise<any> { const client = this.getPlivoClient(); const src = this.getSenderNumber(); this.logger.log(`Sending template '${templateName}' to ${dst} from ${src}`); // Construct the template object per Plivo SDK specification // See: https://www.plivo.com/docs/messaging/api/message/send-a-message const templatePayload = { name: templateName, language: language, ...(components && { components: components }), // Only add components if provided }; try { const response = await client.messages.create({ src: src, dst: dst, type: 'whatsapp', // Ensure type is set for WhatsApp template: templatePayload, ...(callbackUrl && { url: callbackUrl }), // Add callback URL if provided }); this.logger.log(`Template message queued successfully: ${response.messageUuid[0]}`); return response; } catch (error) { this.logger.error(`Failed to send template message to ${dst}:`, error.message || error); throw error; } } async sendWhatsAppText( dst: string, text: string, callbackUrl?: string, ): Promise<any> { const client = this.getPlivoClient(); const src = this.getSenderNumber(); this.logger.log(`Sending text message to ${dst} from ${src}`); // IMPORTANT: Free-form messages only allowed within 24-hour customer service window // Text messages may be up to 4,096 characters per Plivo documentation try { const response = await client.messages.create({ src: src, dst: dst, type: 'whatsapp', text: text, ...(callbackUrl && { url: callbackUrl }), }); this.logger.log(`Text message queued successfully: ${response.messageUuid[0]}`); return response; } catch (error) { this.logger.error(`Failed to send text message to ${dst}:`, error.message || error); throw error; } } async sendWhatsAppMedia( dst: string, mediaUrls: string[], caption?: string, callbackUrl?: string, ): Promise<any> { const client = this.getPlivoClient(); const src = this.getSenderNumber(); this.logger.log(`Sending media message to ${dst} from ${src}`); // IMPORTANT: Free-form messages only allowed within 24-hour customer service window // Plivo supports single media URL per WhatsApp message (image, video, document, audio) // Caption limited to 1,024 characters when sent with media try { const response = await client.messages.create({ src: src, dst: dst, type: 'whatsapp', media_urls: mediaUrls, // Plivo SDK uses 'media_urls' for WhatsApp ...(caption && { text: caption }), // Caption goes in 'text' field for media messages ...(callbackUrl && { url: callbackUrl }), }); this.logger.log(`Media message queued successfully: ${response.messageUuid[0]}`); return response; } catch (error) { this.logger.error(`Failed to send media message to ${dst}:`, error.message || error); throw error; } } async sendWhatsAppInteractive( dst: string, interactivePayload: Record<string, any>, // Expects the structured interactive object callbackUrl?: string, ): Promise<any> { const client = this.getPlivoClient(); const src = this.getSenderNumber(); this.logger.log(`Sending interactive message to ${dst} from ${src}`); // IMPORTANT: Free-form messages only allowed within 24-hour customer service window // Interactive types: 'list', 'reply', 'cta_url' // See: https://www.plivo.com/docs/messaging/api/message/send-a-message try { const response = await client.messages.create({ src: src, dst: dst, type: 'whatsapp', interactive: interactivePayload, // Pass the interactive object directly ...(callbackUrl && { url: callbackUrl }), }); this.logger.log(`Interactive message queued successfully: ${response.messageUuid[0]}`); return response; } catch (error) { this.logger.error(`Failed to send interactive message to ${dst}:`, error.message || error); throw error; } } // Add methods for other message types (Location, Contact, etc.) as needed // Example: sendWhatsAppLocation(...) }client.messages.create()methodtry...catchStep 7: Implement Controller Endpoints
Open
src/whatsapp/whatsapp.controller.tsand define the API endpoints that will use thePlivoService.// src/whatsapp/whatsapp.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, Logger, UseFilters, Req, UnauthorizedException, RawBodyRequest } from '@nestjs/common'; import { PlivoService } from '../plivo/plivo.service'; import { SendTemplateDto } from './dto/send-template.dto'; import { SendTextDto } from './dto/send-text.dto'; import { SendMediaDto } from './dto/send-media.dto'; import { SendInteractiveDto } from './dto/send-interactive.dto'; import { ConfigService } from '@nestjs/config'; import * as plivo from 'plivo'; import { Request } from 'express'; // Import Request for webhook signature validation // Import an exception filter if you create one (See Section 5) // import { AllExceptionsFilter } from '../common/filters/all-exceptions.filter'; @Controller('whatsapp') // @UseFilters(new AllExceptionsFilter()) // Apply custom filter if created (Update path if needed) export class WhatsappController { private readonly logger = new Logger(WhatsappController.name); constructor( private readonly plivoService: PlivoService, private readonly configService: ConfigService // Inject ConfigService for Auth Token ) {} @Post('/send/template') @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as sending is asynchronous async sendTemplateMessage(@Body() sendTemplateDto: SendTemplateDto) { this.logger.log(`Received request to send template: ${sendTemplateDto.templateName} to ${sendTemplateDto.dst}`); try { const result = await this.plivoService.sendWhatsAppTemplate( sendTemplateDto.dst, sendTemplateDto.templateName, sendTemplateDto.language, sendTemplateDto.templateComponents, sendTemplateDto.callbackUrl, ); // Return minimal confirmation, status updates via webhook are better return { messageId: result.messageUuid[0], status: 'queued' }; } catch (error) { // Error is logged in the service, controller can re-throw or return specific HTTP error // Consider using an Exception Filter for cleaner error handling (Section 5) this.logger.error(`Error in /send/template endpoint: ${error.message}`, error.stack); throw error; // Let NestJS handle the error (default 500 or specific if thrown/filtered) } } @Post('/send/text') @HttpCode(HttpStatus.ACCEPTED) async sendTextMessage(@Body() sendTextDto: SendTextDto) { this.logger.log(`Received request to send text to ${sendTextDto.dst}`); try { const result = await this.plivoService.sendWhatsAppText( sendTextDto.dst, sendTextDto.text, sendTextDto.callbackUrl, ); return { messageId: result.messageUuid[0], status: 'queued' }; } catch (error) { this.logger.error(`Error in /send/text endpoint: ${error.message}`, error.stack); throw error; } } @Post('/send/media') @HttpCode(HttpStatus.ACCEPTED) async sendMediaMessage(@Body() sendMediaDto: SendMediaDto) { this.logger.log(`Received request to send media to ${sendMediaDto.dst}`); try { const result = await this.plivoService.sendWhatsAppMedia( sendMediaDto.dst, sendMediaDto.mediaUrls, sendMediaDto.caption, sendMediaDto.callbackUrl, ); return { messageId: result.messageUuid[0], status: 'queued' }; } catch (error) { this.logger.error(`Error in /send/media endpoint: ${error.message}`, error.stack); throw error; } } @Post('/send/interactive') @HttpCode(HttpStatus.ACCEPTED) async sendInteractiveMessage(@Body() sendInteractiveDto: SendInteractiveDto) { this.logger.log(`Received request to send interactive message to ${sendInteractiveDto.dst}`); try { const result = await this.plivoService.sendWhatsAppInteractive( sendInteractiveDto.dst, sendInteractiveDto.interactive, sendInteractiveDto.callbackUrl, ); return { messageId: result.messageUuid[0], status: 'queued' }; } catch (error) { this.logger.error(`Error in /send/interactive endpoint: ${error.message}`, error.stack); throw error; } } // --- INCOMING WEBHOOK HANDLER --- @Post('/webhook/incoming') @HttpCode(HttpStatus.OK) // Plivo expects a 200 OK for webhooks handleIncomingMessage(@Req() req: RawBodyRequest<Request>, @Body() payload: any) { this.logger.log('Received incoming WhatsApp message webhook request.'); // --- Webhook Signature Validation --- // Plivo uses V3 signature validation with HMAC-SHA256 // See: https://www.plivo.com/docs/voice/concepts/signature-validation const signature = req.headers['x-plivo-signature-v3'] as string; const nonce = req.headers['x-plivo-signature-v3-nonce'] as string; const url = req.protocol + '://' + req.get('host') + req.originalUrl; const authToken = this.configService.get<string>('PLIVO_AUTH_TOKEN'); // IMPORTANT: Plivo's validateV3Signature requires the raw body. // Ensure rawBody: true is set in main.ts NestFactory.create() options const rawBody = req.rawBody; if (!rawBody) { this.logger.error('Raw body not available for webhook signature validation. Ensure rawBody: true is configured in main.ts.'); throw new Error('Webhook validation configuration error: Raw body missing.'); } if (!signature || !nonce) { this.logger.warn('Missing Plivo signature headers. Rejecting request.'); throw new UnauthorizedException('Missing signature'); } try { const valid = plivo.validateV3Signature(url, nonce, signature, rawBody.toString('utf8'), authToken); if (!valid) { this.logger.warn('Invalid Plivo signature. Rejecting request.'); throw new UnauthorizedException('Invalid signature'); } this.logger.log('Plivo webhook signature validated successfully.'); } catch (error) { this.logger.error(`Error during signature validation: ${error.message}`); throw new UnauthorizedException('Signature validation failed'); } // --- Process Payload (Only if signature is valid) --- this.logger.log('Processing incoming WhatsApp message payload:'); this.logger.debug(`Payload: ${JSON.stringify(payload, null, 2)}`); // Basic Payload Parsing (Consult Plivo docs for definitive structure) const fromNumber = payload.From; const toNumber = payload.To; const messageUuid = payload.MessageUUID; const type = payload.Type; // e.g., 'text', 'media', 'location' const eventType = payload.EventType; // e.g., 'message_delivered', 'message_read', 'interactive' this.logger.log(`Incoming event ${eventType || type} (${messageUuid}) from ${fromNumber} to ${toNumber}`); // Handle Different Message Types/Events (Add your business logic) if (type === 'text') { const text = payload.Text; this.logger.log(`Text received: "${text}"`); // ** ACTION: Implement your business logic here ** // Example: Route to a chatbot, save to DB, trigger a reply } else if (type === 'media') { const mediaUrl = payload.MediaUrl; const mediaContentType = payload.MediaContentType; const caption = payload.Text; // Caption might be in Text field this.logger.log(`Media received: ${mediaContentType} at ${mediaUrl}, Caption: "${caption || ''}"`); // ** ACTION: Implement logic to handle media (e.g., download, analyze) ** } else if (type === 'location') { const latitude = payload.Latitude; const longitude = payload.Longitude; this.logger.log(`Location received: Lat ${latitude}, Lon ${longitude}`); // ** ACTION: Handle location data ** } else if (eventType === 'interactive' && payload.Interactive) { // Handle replies from interactive messages (buttons, lists) const interactiveData = payload.Interactive; const interactiveType = interactiveData.type; // 'button_reply' or 'list_reply' this.logger.log(`Interactive reply received: Type - ${interactiveType}`); if (interactiveType === 'button_reply') { const buttonId = interactiveData.button_reply?.id; const buttonTitle = interactiveData.button_reply?.title; this.logger.log(`Button Clicked: ID='${buttonId}', Title='${buttonTitle}'`); // ** ACTION: Handle button click based on ID ** } else if (interactiveType === 'list_reply') { const listItemId = interactiveData.list_reply?.id; const listItemTitle = interactiveData.list_reply?.title; const listItemDescription = interactiveData.list_reply?.description; this.logger.log(`List Item Selected: ID='${listItemId}', Title='${listItemTitle}', Desc='${listItemDescription}'`); // ** ACTION: Handle list selection based on ID ** } } else if (eventType && eventType.startsWith('message_')) { // Handle status updates (delivered, read, failed, etc.) this.logger.log(`Message status update: ${eventType} for UUID ${messageUuid}`); // ** ACTION: Update message status in your database if tracking ** } else { this.logger.log(`Received unhandled message type '${type}' or event type '${eventType}'.`); } // ** IMPORTANT: Always return 200 OK quickly to Plivo ** // Perform time-consuming actions asynchronously (e.g., using queues or background jobs) // If you don't return 200 OK promptly, Plivo might retry the webhook. } }WhatsApp Business Message Templates: A Complete Guide
WhatsApp Business API requires pre-approved message templates for business-initiated conversations outside the 24-hour customer service window. Understanding these templates is essential for proper implementation and cost optimization.
Meta categorizes templates into three types, each with different pricing and use cases:
Template Categories (Meta documentation):
Utility Templates: Used for specific transaction or account updates (order confirmations, shipping notifications, appointment reminders, account alerts). These messages provide important information users have opted to receive.
Authentication Templates: Used for one-time passwords (OTP) and two-factor authentication. These must include security disclaimers and are typically the lowest cost option.
Marketing Templates: Used for promotional content, offers, announcements, newsletters, and other marketing communications. These have higher pricing and stricter approval requirements.
Template Creation: Create templates via WhatsApp Manager or the Plivo console. Meta must approve templates before use – this typically takes a few hours but can take up to 24 hours.
Important Notes:
parametersarrayWhatsApp Pricing Model (Updated July 2025)
As of July 1, 2025, WhatsApp transitioned from conversation-based pricing to a per-message pricing model. This significantly changed how businesses are charged for WhatsApp communications.
Key Pricing Changes:
Per-Message Billing: Each template message delivered is charged based on:
24-Hour Customer Service Window: When a customer messages your business, a 24-hour window opens. During this period:
72-Hour Free Entry Point: Messages from Click-to-WhatsApp Ads or Facebook Page buttons open a 72-hour free window for all message types (you must respond within 24 hours for the window to activate).
No More Free Tier: The previous 1,000 free conversations per month no longer exists.
Pricing Examples (USD, effective October 2025):
Source: Gallabox Per-Message Pricing Documentation
Cost Optimization Best Practices:
Plivo WhatsApp NestJS Integration: Frequently Asked Questions
How do I send WhatsApp messages with Plivo in NestJS?
To send WhatsApp messages with Plivo in NestJS: Install the Plivo Node.js SDK (
npm install plivo), create a PlivoService that initializes the client with your Auth ID and Token, then useclient.messages.create()withtype: 'whatsapp'parameter. Inject the PlivoService into your controller using NestJS dependency injection and call your service methods from API endpoints.What is the WhatsApp 24-hour conversation window and how does it work?
The WhatsApp 24-hour conversation window is a policy that allows businesses to send free-form messages (text, media, interactive) only within 24 hours after a user initiates contact or replies. Outside this window, you must use pre-approved message templates to start new conversations. As of July 2025, utility templates are free within this window, while marketing and authentication templates are always charged.
How do I validate Plivo webhook signatures in NestJS for WhatsApp messages?
To validate Plivo webhook signatures in NestJS, call Plivo's
validateV3Signature()function with the raw request body, nonce fromX-Plivo-Signature-V3-Nonceheader, signature fromX-Plivo-Signature-V3header, full webhook URL, and your Auth Token. You must preserve the raw body by settingrawBody: trueinNestFactory.create()options inmain.ts. This HMAC-SHA256 validation ensures webhooks are authentic and from Plivo. See the NestJS raw body documentation for implementation details.What types of WhatsApp messages can I send with Plivo API?
Plivo WhatsApp API supports multiple message types including: templated messages for business-initiated conversations, plain text messages, media messages (images, videos, documents, audio files), interactive messages with quick reply buttons, selection lists, and call-to-action URLs, plus location messages. All message types work through the WhatsApp Business API and require proper authentication.
Do I need a WhatsApp Business Account to use Plivo WhatsApp API?
Yes, you need a WhatsApp Business Account (WABA) to use Plivo WhatsApp API. First, purchase a Plivo phone number, then enable it for WhatsApp messaging in the Plivo console under Messaging → WhatsApp → Senders. Link this number to your WhatsApp Business Account following Plivo's onboarding process. You must also create and get Meta approval for message templates before sending business-initiated messages outside the 24-hour window.
How do I handle incoming WhatsApp messages in NestJS with Plivo?
To handle incoming WhatsApp messages in NestJS: Create a webhook endpoint (e.g.,
/whatsapp/webhook/incoming) decorated with@Post()that accepts POST requests from Plivo. Validate the webhook signature usingplivo.validateV3Signature()with the raw request body, parse the payload to extract message content and type (text, media, interactive reply, location), then implement your business logic to process and respond to messages. Always return HTTP 200 OK quickly to prevent webhook retries.What's the difference between template and free-form WhatsApp messages?
Template messages use pre-approved formats with placeholders and initiate conversations outside the 24-hour window. Free-form messages allow custom text, media, and interactive elements but can only be sent as replies within active conversation windows.
How much does it cost to send WhatsApp messages through Plivo?
As of July 1, 2025, WhatsApp charges per message delivered rather than per conversation. Messages within the 24-hour customer service window are free (including utility templates). Template pricing varies by country and category (utility, authentication, marketing), ranging from $0.0016 to $0.1570+ per message. See the pricing section for detailed rates.
What are the character limits for WhatsApp messages?
According to Plivo's documentation:
Related Resources