code examples
code examples
Plivo WhatsApp Integration with Node.js and Fastify: Complete Guide 2025
Learn how to build a production-ready WhatsApp API using Plivo, Node.js, and Fastify. Complete guide covering authentication, webhooks, TypeScript, and Docker deployment for enterprise messaging.
This comprehensive guide shows you how to integrate Plivo WhatsApp messaging with Node.js and Fastify to build a production-ready API. Learn to send and receive WhatsApp Business messages, handle webhooks, implement authentication, and deploy with Docker—everything you need for enterprise-grade WhatsApp integration.
Build a Fastify backend service that exposes API endpoints to send text and template-based WhatsApp messages via Plivo. Include a webhook endpoint to receive incoming messages and status updates from Plivo, ensuring two-way communication. This setup solves the common need for programmatic WhatsApp interaction for customer support, notifications, and engagement campaigns.
This tutorial covers everything from initializing your Node.js project with TypeScript to implementing secure webhook handlers and deploying with Docker. Whether you're building a customer support chatbot, sending transactional notifications, or creating a WhatsApp marketing platform, this guide provides the foundation you need. For similar integrations, see our guides on Plivo SMS with Express and Twilio WhatsApp with Fastify.
Technologies Used:
- Node.js: The JavaScript runtime environment.
- Fastify: A high-performance, low-overhead web framework for Node.js. Chosen for its speed, extensibility, and developer-friendly features like built-in validation and logging. Note: Fastify v5 (current version as of October 2025) requires Node.js v20 or higher. [Source: Fastify GitHub]
- TypeScript: Adds static typing for improved code quality and maintainability.
- Plivo: The Communications Platform as a Service (CPaaS) provider for sending and receiving WhatsApp messages via their official API.
- Plivo Node SDK: Simplifies interaction with the Plivo API. Note: Use the
plivopackage (v4.74.0 as of October 2025), not the deprecatedplivo-nodepackage. [Source: npm plivo package] - Prisma (Optional but Recommended): A modern ORM for database access (we'll use PostgreSQL) to log messages.
- Docker: For containerizing the application for consistent deployment.
dotenv/@fastify/env: For managing environment variables securely.pino-pretty: For readable development logs.@fastify/sensible: Adds sensible defaults and utility decorators.@fastify/rate-limit: For API rate limiting.@fastify/type-provider-typebox: For schema validation using TypeBox.
System Architecture:
graph LR
subgraph Your Infrastructure
Client[Client Application] --> FAPI[Fastify API Service];
FAPI -- Send Request --> PlivoSDK[Plivo Node.js SDK];
FAPI -- Store/Retrieve --> DB[(Database)];
PlivoCallback[Plivo Webhook Handler] --> FAPI;
end
subgraph Plivo Cloud
PlivoSDK -- API Call --> PlivoAPI[Plivo WhatsApp API];
PlivoAPI -- Send/Receive --> WhatsApp;
WhatsApp -- Incoming Msg/Status --> PlivoAPI;
PlivoAPI -- POST Request --> PlivoCallback;
end
subgraph External
WhatsApp[WhatsApp Platform];
end
style DB fill:#f9f,stroke:#333,stroke-width:2pxPrerequisites:
- Node.js (v20 or v22 recommended for Fastify v5 compatibility) and npm/yarn. Use
nvmfor managing Node versions. Note: Node.js v18 reached End-of-Life in April 2025 and should not be used. [Source: Node.js Release Schedule] - A Plivo account with credits.
- A WhatsApp Business Account (WABA) successfully onboarded and linked to Plivo.
- A phone number enabled for WhatsApp via Plivo.
- Access to a terminal or command prompt.
- An IDE like VS Code.
- (Optional) Docker installed.
- (Optional) PostgreSQL database running (locally or cloud-hosted).
- (For local development testing of webhooks)
ngrokor a similar tunneling service.
Final Outcome:
A containerized Fastify application with API endpoints (/send/text, /send/template) and a webhook receiver (/webhooks/plivo/whatsapp) for seamless WhatsApp communication via Plivo, complete with logging, basic security, and deployment readiness.
1. Set up your Plivo WhatsApp Node.js project
Initialize your Node.js project using TypeScript and install Fastify along with essential dependencies.
1.1 Install Node.js and configure your environment:
Use Node Version Manager (nvm) to manage Node.js versions for optimal compatibility with Fastify v5.
-
macOS/Linux:
bash# Install nvm (check nvm GitHub repo for the absolute latest install script) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash # Activate nvm export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Install and use Node.js v22 (current LTS version as of 2025) nvm install 22 nvm use 22 # Alternative: Use Node.js v20 # nvm install 20 # nvm use 20 -
Windows: Download the installer from the Node.js website or use
nvm-windows. [Source: nvm-sh GitHub, latest version v0.40.3 as of October 2025]
1.2 Project Initialization:
# Create project directory and navigate into it
mkdir fastify-plivo-whatsapp
cd fastify-plivo-whatsapp
# Initialize npm project
npm init -y
# Install core dependencies
# Note: Use 'plivo' (not 'plivo-node' which is deprecated)
npm install fastify @fastify/env @fastify/sensible @fastify/type-provider-typebox plivo
# Install development dependencies
npm install typescript @types/node ts-node nodemon pino-pretty --save-dev
# Initialize TypeScript configuration
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib esnext --module commonjs --allowJs true --noImplicitAny true1.3 Project Structure:
Create the following directory structure:
fastify-plivo-whatsapp/
├── dist/ # Compiled JavaScript output (from tsc)
├── src/ # TypeScript source code
│ ├── routes/ # API route definitions
│ ├── services/ # Business logic (Plivo interaction)
│ ├── schemas/ # Request/response validation schemas
│ ├── utils/ # Utility functions
│ └── server.ts # Fastify server setup and entry point
├── .env # Environment variables (DO NOT COMMIT)
├── .env.example # Example environment variables (commit this)
├── .gitignore
├── package.json
└── tsconfig.jsonExplanation:
src/: Contains all application logic written in TypeScript.dist/: Contains the compiled JavaScript code generated bytsc. Run the code from here in production.routes/: Separates API endpoint definitions for better organization.services/: Encapsulates logic for interacting with external services like Plivo or the database.schemas/: Holds JSON schema definitions used by Fastify for request validation.utils/: Contains reusable helper functions.server.ts: The main application file where the Fastify server is configured and started..env: Stores sensitive information like API keys. Use@fastify/envto load these..gitignore: Prevents committing sensitive files (.env,node_modules,dist).tsconfig.json: Configures the TypeScript compiler.
1.4 Configuration Files:
-
.gitignore:text# Dependencies node_modules/ # Build output dist/ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pids *.pid *.seed *.pid.lock # Environment variables .env .env.*.local # Optional editor directories .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .idea # OSX .DS_Store -
.env.example(Create this file):dotenv# Plivo Credentials (Get from Plivo Console → Account → Auth Credentials) PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN # Plivo WhatsApp Number (The number you registered with Plivo for WhatsApp) PLIVO_WHATSAPP_NUMBER=+14155551234 # Use E.164 format # Plivo Webhook Validation (Optional but Highly Recommended) # Generate a strong, random secret string here and configure it in Plivo console PLIVO_WEBHOOK_SECRET=your-strong-random-secret # e.g., use `openssl rand -hex 32` # Application Settings PORT=3000 HOST=0.0.0.0 LOG_LEVEL=info # trace, debug, info, warn, error, fatal # API Key for simple endpoint protection (Generate a secure random string) API_KEY=your-secure-api-key # e.g., use `openssl rand -hex 32` # Database URL (if using Prisma/DB – uncomment and configure if needed) # Example for PostgreSQL: postgresql://user:password@host:port/database # DATABASE_URL=- Important: Create a
.envfile by copying.env.exampleand fill in your actual credentials. Ensure.envis listed in.gitignore. Generate strong, unique values forPLIVO_WEBHOOK_SECRETandAPI_KEY.
- Important: Create a
-
tsconfig.json(Modify the generated one):json{ "compilerOptions": { "target": "es2022", "module": "commonjs", "rootDir": "src", "outDir": "dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "noImplicitAny": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"] }
1.5 Run Scripts (package.json):
Add these scripts to your package.json file:
{
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "nodemon --watch src --ext ts --exec ts-node src/server.ts",
"test": "echo \"Error: no test specified\" && exit 1"
}
}build: Compiles TypeScript to JavaScript in thedistdirectory.start: Runs the compiled JavaScript application (for production).dev: Runs the application usingts-nodeandnodemonfor development, automatically restarting on file changes.test: Placeholder script. Implement a proper testing strategy (e.g., using Jest or Vitest).
2. Implement Plivo WhatsApp messaging with Fastify
Set up the Fastify server and create the service responsible for interacting with Plivo.
2.1 Configure your Fastify server for WhatsApp messaging (src/server.ts):
// src/server.ts
import Fastify, { FastifyInstance, FastifyServerOptions } from 'fastify';
import sensible from '@fastify/sensible';
import envPlugin from '@fastify/env';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { S } from '@fastify/type-provider-typebox';
// Import routes
import whatsappRoutes from './routes/whatsappRoutes';
// Import Plivo initializer
import { initializePlivoClient } from './services/plivoService';
// Define environment variable schema
const envSchema = S.Object({
PORT: S.Number({ default: 3000 }),
HOST: S.String({ default: '0.0.0.0' }),
LOG_LEVEL: S.String({ default: 'info' }),
PLIVO_AUTH_ID: S.String(),
PLIVO_AUTH_TOKEN: S.String(),
PLIVO_WHATSAPP_NUMBER: S.String(),
PLIVO_WEBHOOK_SECRET: S.String({ default: '' }),
API_KEY: S.String(),
});
// Declare module augmentation for FastifyInstance
declare module 'fastify' {
interface FastifyInstance {
config: {
PORT: number;
HOST: string;
LOG_LEVEL: string;
PLIVO_AUTH_ID: string;
PLIVO_AUTH_TOKEN: string;
PLIVO_WHATSAPP_NUMBER: string;
PLIVO_WEBHOOK_SECRET: string;
API_KEY: string;
};
}
}
async function buildServer(): Promise<FastifyInstance> {
const isProduction = process.env.NODE_ENV === 'production';
const serverOptions: FastifyServerOptions = {
logger: {
level: process.env.LOG_LEVEL || 'info',
// Use pino-pretty only in development for better readability
transport: isProduction
? undefined
: { target: 'pino-pretty', options: { translateTime: 'SYS:standard', ignore: 'pid,hostname' } },
},
ajv: {
customOptions: {
removeAdditional: 'all',
useDefaults: true,
coerceTypes: true,
allErrors: true,
}
}
};
const server = Fastify(serverOptions).withTypeProvider<TypeBoxTypeProvider>();
try {
// Register environment variables plugin
await server.register(envPlugin, {
dotenv: true,
schema: envSchema,
});
// Initialize Plivo Client *after* config is loaded
initializePlivoClient(server);
// Register sensible plugin (adds useful decorators like httpErrors)
await server.register(sensible);
// Register Routes
await server.register(whatsappRoutes, { prefix: '/api/whatsapp' });
// Default Root Route
server.get('/', async (request, reply) => {
return { message: 'Fastify Plivo WhatsApp Service Running', timestamp: new Date().toISOString() };
});
// Health Check Route
server.get('/health', async (request, reply) => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
server.log.info('Server plugins and routes registered successfully.');
} catch (err) {
server.log.error(err, 'Error during server setup');
process.exit(1);
}
return server;
}
async function start() {
let server: FastifyInstance | null = null;
try {
server = await buildServer();
await server.listen({ port: server.config.PORT, host: server.config.HOST });
server.log.info(`Server listening at http://${server.config.HOST}:${server.config.PORT}`);
server.log.info(`Plivo WhatsApp Number configured: ${server.config.PLIVO_WHATSAPP_NUMBER}`);
setupGracefulShutdown(server);
} catch (err) {
if (server) {
server.log.error(err, 'Error starting server');
} else {
console.error('Error during server build/start:', err);
}
process.exit(1);
}
}
// Graceful shutdown handler
const setupGracefulShutdown = (server: FastifyInstance) => {
const shutdown = async (signal: string) => {
server.log.warn(`Received ${signal}. Shutting down gracefully…`);
try {
await server.close();
server.log.info('Server closed successfully.');
process.exit(0);
} catch (err) {
server.log.error(err, 'Error during graceful shutdown');
process.exit(1);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
};
// Start the server
start().catch(err => {
console.error('Unhandled error during startup process:', err);
process.exit(1);
});Explanation:
- Imports: Import Fastify, plugins, routes, and the
initializePlivoClientfunction. envSchema: Defines expected environment variables using TypeBox for validation and type safety.- Module Augmentation: Extends
FastifyInstanceto include the loadedconfigobject for type-safe access. buildServerfunction:- Configures Fastify logger (
pino-prettyin dev, JSON in prod). - Configures AJV for schema validation.
- Registers
@fastify/envto load.envand validate variables. - Calls
initializePlivoClient(server)immediately after config is loaded. - Registers
@fastify/sensible. - Registers application routes under
/api/whatsapp. - Includes basic
/and/healthroutes.
- Configures Fastify logger (
startfunction:- Calls
buildServer. - Starts the server using
HOSTandPORTfrom config. - Calls
setupGracefulShutdownafter the server successfully starts listening. - Handles potential startup errors.
- Calls
- Graceful Shutdown (
setupGracefulShutdown): Implements handlers forSIGINTandSIGTERMto allow graceful server closure. - Execution: Calls
start()to run the application.
2.2 Create the Plivo WhatsApp service (src/services/plivoService.ts):
This service encapsulates all logic for interacting with the Plivo WhatsApp API, including sending text messages and template messages.
// src/services/plivoService.ts
import { Client, MessageCreateParams, Template } from 'plivo-node';
import { FastifyInstance } from 'fastify';
let plivoClient: Client;
// Initialize the Plivo client using config from Fastify instance
export function initializePlivoClient(server: FastifyInstance): void {
if (!plivoClient) {
if (!server.config.PLIVO_AUTH_ID || !server.config.PLIVO_AUTH_TOKEN) {
server.log.error('Plivo Auth ID or Auth Token missing in configuration. Cannot initialize Plivo client.');
throw new Error('Plivo credentials missing.');
}
plivoClient = new Client(
server.config.PLIVO_AUTH_ID,
server.config.PLIVO_AUTH_TOKEN
);
server.log.info('Plivo client initialized successfully.');
}
}
// Send a standard WhatsApp text message
export async function sendWhatsAppTextMessage(
server: FastifyInstance,
to: string,
text: string
): Promise<any> {
if (!plivoClient) {
server.log.error('Plivo client accessed before initialization.');
throw new Error('Plivo client not initialized. Call initializePlivoClient first.');
}
const params: MessageCreateParams = {
src: server.config.PLIVO_WHATSAPP_NUMBER,
dst: to,
text: text,
type: 'whatsapp',
};
server.log.info({ dst: to, type: 'text' }, 'Sending WhatsApp text message via Plivo');
try {
const response = await plivoClient.messages.create(params);
server.log.info({ message_uuid: response.messageUuid, api_id: response.apiId }, 'WhatsApp text message sent successfully via Plivo');
return response;
} catch (error: any) {
server.log.error({ error: error.message, stack: error.stack, params }, 'Error sending WhatsApp text message via Plivo');
throw new Error(`Plivo API Error: ${error.message || 'Unknown error'}`);
}
}
// Send a WhatsApp template message
export async function sendWhatsAppTemplateMessage(
server: FastifyInstance,
to: string,
template: Template
): Promise<any> {
if (!plivoClient) {
server.log.error('Plivo client accessed before initialization.');
throw new Error('Plivo client not initialized. Call initializePlivoClient first.');
}
if (!template || !template.name || !template.language) {
server.log.warn({ templateReceived: template }, 'Invalid template object provided to sendWhatsAppTemplateMessage');
throw new Error('Invalid template object: name and language are required.');
}
const params: MessageCreateParams = {
src: server.config.PLIVO_WHATSAPP_NUMBER,
dst: to,
template: template,
type: 'whatsapp',
};
server.log.info({ dst: to, templateName: template.name }, 'Sending WhatsApp template message via Plivo');
try {
const response = await plivoClient.messages.create(params);
server.log.info({ message_uuid: response.messageUuid, api_id: response.apiId }, 'WhatsApp template message sent successfully via Plivo');
return response;
} catch (error: any) {
server.log.error({ error: error.message, stack: error.stack, params }, 'Error sending WhatsApp template message via Plivo');
throw new Error(`Plivo API Error: ${error.message || 'Unknown error'}`);
}
}Explanation:
- Initialization:
initializePlivoClientcreates a singleton Plivo client using credentials fromserver.config. It includes a check to ensure credentials exist. This is called once during server startup inserver.ts. sendWhatsAppTextMessage:- Takes
server(for logging/config), recipientto, and messagetext. - Constructs
paramsfor the Plivo API call. - Logs attempt and success/failure. Includes
try…catchfor Plivo API errors.
- Takes
sendWhatsAppTemplateMessage:- Similar structure, takes a
templateobject conforming to Plivo'sTemplatetype. - Includes basic validation for the template object.
- Similar structure, takes a
- Error Handling: Logs errors with context and throws a new Error for route handlers to catch. Added checks for client initialization.
3. Build WhatsApp API endpoints with TypeScript validation
Define the API endpoints for sending messages and receiving webhooks.
3.1 Define WhatsApp message schemas with TypeScript (src/schemas/whatsappSchemas.ts):
Using @fastify/type-provider-typebox for runtime validation and type safety.
// src/schemas/whatsappSchemas.ts
import { Type, Static } from '@fastify/type-provider-typebox';
// Schema for sending a plain text message
export const SendTextBodySchema = Type.Object({
to: Type.String({
description: 'Recipient WhatsApp number in E.164 format (e.g., +14155551234)',
pattern: '^\\+[1-9]\\d{1,14}$'
}),
text: Type.String({ description: 'The text content of the message', minLength: 1, maxLength: 4096 })
});
export type SendTextBody = Static<typeof SendTextBodySchema>;
// Schema for Plivo Template Parameter component
const TemplateParameterSchema = Type.Object({
type: Type.Union([Type.Literal('text'), Type.Literal('media'), Type.Literal('payload')]),
text: Type.Optional(Type.String()),
media: Type.Optional(Type.String({ format: 'uri', description: 'URL of the media file' })),
payload: Type.Optional(Type.String())
});
// Schema for Plivo Template Component
const TemplateComponentSchema = Type.Object({
type: Type.Union([Type.Literal('header'), Type.Literal('body'), Type.Literal('footer'), Type.Literal('button')]),
sub_type: Type.Optional(Type.String()),
index: Type.Optional(Type.Number()),
parameters: Type.Optional(Type.Array(TemplateParameterSchema))
});
// Schema for Plivo Template object
export const PlivoTemplateSchema = Type.Object({
name: Type.String({ description: 'The registered name of the template' }),
language: Type.String({ description: 'The language code of the template (e.g., en_US)' }),
components: Type.Optional(Type.Array(TemplateComponentSchema))
});
export type PlivoTemplate = Static<typeof PlivoTemplateSchema>;
// Schema for sending a template message
export const SendTemplateBodySchema = Type.Object({
to: Type.String({
description: 'Recipient WhatsApp number in E.164 format',
pattern: '^\\+[1-9]\\d{1,14}$'
}),
template: PlivoTemplateSchema
});
export type SendTemplateBody = Static<typeof SendTemplateBodySchema>;
// Generic Success Response Schema
export const SuccessResponseSchema = Type.Object({
message: Type.String(),
message_uuid: Type.Optional(Type.String()),
api_id: Type.Optional(Type.String())
});
export type SuccessResponse = Static<typeof SuccessResponseSchema>;
// Schema for incoming Plivo WhatsApp message webhook
export const PlivoIncomingWebhookSchema = Type.Object({
From: Type.String(),
To: Type.String(),
Type: Type.String(),
Text: Type.Optional(Type.String()),
MediaUrl0: Type.Optional(Type.String()),
MediaContentType0: Type.Optional(Type.String()),
NumMedia: Type.Optional(Type.String()),
Latitude: Type.Optional(Type.String()),
Longitude: Type.Optional(Type.String()),
LocationAddress: Type.Optional(Type.String()),
ContactName: Type.Optional(Type.String()),
ContactNumber: Type.Optional(Type.String()),
ButtonPayload: Type.Optional(Type.String()),
ListResponseTitle: Type.Optional(Type.String()),
InteractiveType: Type.Optional(Type.String()),
MessageUUID: Type.String()
});
export type PlivoIncomingWebhook = Static<typeof PlivoIncomingWebhookSchema>;
// Schema for incoming Plivo WhatsApp status webhook
export const PlivoStatusWebhookSchema = Type.Object({
MessageUUID: Type.String(),
Status: Type.String(),
To: Type.String(),
From: Type.String(),
ErrorCode: Type.Optional(Type.String()),
ErrorMessage: Type.Optional(Type.String()),
});
export type PlivoStatusWebhook = Static<typeof PlivoStatusWebhookSchema>;Explanation:
- Uses TypeBox for defining request/response structures and types.
- Includes E.164 pattern validation for phone numbers.
- Defines schemas for sending text, sending templates (based on Plivo's structure), incoming messages, status updates, and a generic success response.
- Exports both schemas and inferred TypeScript types.
3.2 Create WhatsApp API routes with authentication (src/routes/whatsappRoutes.ts):
// src/routes/whatsappRoutes.ts
import { FastifyInstance, FastifyPluginOptions, FastifyRequest, FastifyReply } from 'fastify';
import { Type } from '@fastify/type-provider-typebox';
import { PlivoIncomingWebhook, PlivoStatusWebhook, PlivoTemplate, SendTemplateBody, SendTextBody, SendTemplateBodySchema, SendTextBodySchema, SuccessResponse, SuccessResponseSchema, PlivoIncomingWebhookSchema, PlivoStatusWebhookSchema } from '../schemas/whatsappSchemas';
import { sendWhatsAppTemplateMessage, sendWhatsAppTextMessage } from '../services/plivoService';
// Simple Authentication Hook (API Key)
async function authenticate(request: FastifyRequest, reply: FastifyReply) {
const apiKey = request.headers['x-api-key'];
if (!apiKey || apiKey !== request.server.config.API_KEY) {
request.log.warn('Authentication failed: Invalid or missing API Key');
reply.unauthorized('Invalid or missing API Key');
return reply;
}
}
// Plivo Webhook Validation Hook (Partial Implementation)
async function validatePlivoWebhook(request: FastifyRequest, reply: FastifyReply) {
const secret = request.server.config.PLIVO_WEBHOOK_SECRET;
// Skip validation if no secret is configured
if (!secret) {
request.log.warn('Skipping Plivo webhook validation: PLIVO_WEBHOOK_SECRET not configured.');
return;
}
const signature = request.headers['x-plivo-signature-v3'] as string;
const nonce = request.headers['x-plivo-signature-v3-nonce'] as string;
if (!signature || !nonce) {
request.log.warn('Plivo webhook validation failed: Missing X-Plivo-Signature-V3 or X-Plivo-Signature-V3-Nonce headers.');
reply.badRequest('Missing Plivo signature headers');
return reply;
}
// TODO: Implement signature verification using crypto.
// The signature is calculated using HMAC-SHA256(URL + Nonce + RawBody, WebhookSecret).
// Use libraries like `fastify-raw-body` to access the raw payload.
// See Plivo documentation: https://www.plivo.com/docs/getting-started/concepts/webhooks#validate-requests-from-plivo
request.log.info('Plivo webhook headers present. TODO: Implement full signature validation.');
}
// Define the routes
export default async function whatsappRoutes(server: FastifyInstance, options: FastifyPluginOptions) {
// Send Text Message Endpoint
server.post<{ Body: SendTextBody; Reply: SuccessResponse | { error: string } }>(
'/send/text',
{
schema: {
description: 'Sends a plain text WhatsApp message via Plivo.',
tags: ['WhatsApp'],
summary: 'Send Text Message',
headers: {
type: 'object',
properties: { 'x-api-key': { type: 'string' } },
required: ['x-api-key']
},
body: SendTextBodySchema,
response: {
200: SuccessResponseSchema,
},
},
preHandler: [authenticate]
},
async (request, reply) => {
try {
const { to, text } = request.body;
const plivoResponse = await sendWhatsAppTextMessage(server, to, text);
reply.send({
message: 'WhatsApp text message sent successfully.',
message_uuid: plivoResponse.messageUuid?.[0],
api_id: plivoResponse.apiId
});
} catch (error: any) {
request.log.error(error, 'Error in /send/text handler');
reply.internalServerError(error.message || 'Failed to send WhatsApp text message.');
}
}
);
// Send Template Message Endpoint
server.post<{ Body: SendTemplateBody; Reply: SuccessResponse | { error: string } }>(
'/send/template',
{
schema: {
description: 'Sends a WhatsApp template message via Plivo.',
tags: ['WhatsApp'],
summary: 'Send Template Message',
headers: {
type: 'object',
properties: { 'x-api-key': { type: 'string' } },
required: ['x-api-key']
},
body: SendTemplateBodySchema,
response: {
200: SuccessResponseSchema,
},
},
preHandler: [authenticate]
},
async (request, reply) => {
try {
const { to, template } = request.body;
const plivoResponse = await sendWhatsAppTemplateMessage(server, to, template as PlivoTemplate);
reply.send({
message: 'WhatsApp template message sent successfully.',
message_uuid: plivoResponse.messageUuid?.[0],
api_id: plivoResponse.apiId
});
} catch (error: any) {
request.log.error(error, 'Error in /send/template handler');
reply.internalServerError(error.message || 'Failed to send WhatsApp template message.');
}
}
);
// Plivo Webhook Endpoint (Incoming Messages & Status Updates)
server.post<{ Body: PlivoIncomingWebhook | PlivoStatusWebhook }>(
'/webhooks/plivo',
{
schema: {
description: 'Receives incoming WhatsApp messages and status updates from Plivo.',
tags: ['Webhooks', 'WhatsApp'],
summary: 'Plivo Webhook Handler',
response: {
200: Type.Object({ status: Type.String() }),
},
},
preHandler: [validatePlivoWebhook]
},
async (request, reply) => {
const payload = request.body;
request.log.info({ payload }, 'Received Plivo webhook');
try {
// Differentiate between Message Status and Incoming Message
if ('Status' in payload && 'MessageUUID' in payload && !('Type' in payload)) {
const statusPayload = payload as PlivoStatusWebhook;
request.log.info({ uuid: statusPayload.MessageUUID, status: statusPayload.Status }, 'Processing Plivo status update webhook');
// TODO: Handle status update (e.g., update message status in DB)
} else if ('Type' in payload && 'From' in payload && 'To' in payload && 'MessageUUID' in payload) {
const incomingPayload = payload as PlivoIncomingWebhook;
request.log.info({ uuid: incomingPayload.MessageUUID, from: incomingPayload.From, type: incomingPayload.Type }, 'Processing Plivo incoming message webhook');
// TODO: Handle incoming message (e.g., log message, trigger auto-reply)
} else {
request.log.warn({ payload }, 'Received unknown Plivo webhook payload structure.');
}
reply.code(200).send({ status: 'received' });
} catch (error: any) {
request.log.error(error, 'Error processing Plivo webhook');
reply.code(200).send({ status: 'error processing' });
}
}
);
}Explanation:
- Authentication Hook: Validates the
x-api-keyheader against the configuredAPI_KEY. - Webhook Validation Hook: Checks for Plivo signature headers. Full signature verification requires implementing crypto-based validation (TODO).
/send/textEndpoint: Accepts a text message request, validates input, calls the Plivo service, and returns a success response./send/templateEndpoint: Similar to text endpoint, but for template messages./webhooks/plivoEndpoint: Receives webhooks from Plivo. Differentiates between status updates and incoming messages based on payload structure. Acknowledges receipt immediately to prevent retries.
4. Test your Plivo WhatsApp integration
Test your endpoints locally using curl or an API client like Postman.
4.1 Start the development server:
npm run dev4.2 Send a text message:
curl -X POST http://localhost:3000/api/whatsapp/send/text \
-H "Content-Type: application/json" \
-H "x-api-key: your-secure-api-key" \
-d '{
"to": "+14155552345",
"text": "Hello from Fastify and Plivo!"
}'4.3 Send a template message:
curl -X POST http://localhost:3000/api/whatsapp/send/template \
-H "Content-Type: application/json" \
-H "x-api-key: your-secure-api-key" \
-d '{
"to": "+14155552345",
"template": {
"name": "welcome_template",
"language": "en_US",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "John Doe"
}
]
}
]
}
}'4.4 Test webhooks locally using ngrok:
Install and run ngrok to expose your local server:
ngrok http 3000Copy the HTTPS URL provided by ngrok (e.g., https://abc123.ngrok.io) and configure it in your Plivo console:
- Message URL:
https://abc123.ngrok.io/api/whatsapp/webhooks/plivo - Method: POST
Send a WhatsApp message to your Plivo number and check your server logs for the incoming webhook.
5. Deploy your WhatsApp API with Docker
Containerize your application for consistent deployment across environments.
5.1 Create a Dockerfile:
# Use official Node.js LTS image
FROM node:22-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build TypeScript
RUN npm run build
# Expose port
EXPOSE 3000
# Start the application
CMD ["node", "dist/server.js"]5.2 Create a .dockerignore file:
node_modules
dist
.env
.git
.gitignore
*.md5.3 Build and run the Docker image:
# Build the image
docker build -t fastify-plivo-whatsapp .
# Run the container
docker run -p 3000:3000 --env-file .env fastify-plivo-whatsapp5.4 Docker Compose (Optional):
Create a docker-compose.yml for orchestrating multiple services:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: whatsapp
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:Run with:
docker-compose up -d6. WhatsApp integration best practices for production
6.1 Security:
- Use environment variables for all secrets.
- Implement rate limiting using
@fastify/rate-limit. - Enable CORS only for trusted origins using
@fastify/cors. - Implement full webhook signature validation.
- Use HTTPS in production.
6.2 Logging and Monitoring:
- Use structured logging with Pino.
- Integrate with monitoring tools (e.g., Datadog, New Relic).
- Set up alerting for failed message deliveries.
6.3 Database Integration:
- Use Prisma for database access.
- Log all messages and status updates to track delivery.
- Implement message queuing for high-volume scenarios.
6.4 Error Handling:
- Implement retry logic for failed API calls.
- Handle Plivo-specific error codes appropriately.
- Return meaningful error messages to clients.
6.5 Performance:
- Use connection pooling for database access.
- Implement caching where appropriate.
- Monitor and optimize API response times.
Troubleshooting
Common Issues:
| Issue | Solution |
|---|---|
| "Plivo credentials missing" error | Verify .env file contains PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN |
| Webhooks not received | Check ngrok is running, URL is configured in Plivo console, firewall allows inbound traffic |
| "Invalid API Key" error | Ensure x-api-key header matches API_KEY in .env |
| TypeScript compilation errors | Run npm run build and check for missing types or syntax errors |
| Messages not sending | Verify WhatsApp number is enabled in Plivo, recipient number is correct, sufficient Plivo credits |
Debug Steps:
- Check server logs for detailed error messages
- Verify environment variables are loaded correctly
- Test Plivo credentials using Plivo console
- Ensure phone numbers are in E.164 format
- Check Plivo API documentation for error codes
Next steps
- Add database persistence: Implement Prisma with PostgreSQL to store messages and status updates.
- Implement full webhook signature validation: Complete the
validatePlivoWebhookhook with crypto-based verification. - Add comprehensive testing: Write unit and integration tests using Jest or Vitest.
- Implement rate limiting: Use
@fastify/rate-limitto protect your API endpoints. - Set up CI/CD: Automate testing and deployment using GitHub Actions or similar tools.
- Add monitoring and alerting: Integrate with observability platforms for production monitoring.
Related Resources:
Frequently Asked Questions
How to send WhatsApp messages using Plivo and Node.js?
Use the provided Fastify backend service, which exposes API endpoints like `/send/text` and `/send/template` to send WhatsApp messages via Plivo. These endpoints allow you to send both simple text messages and more complex template-based messages using the Plivo Node.js SDK within a structured, maintainable Node.js application built with Fastify.
What is Fastify and why was it chosen for this integration?
Fastify is a high-performance web framework for Node.js, known for its speed and extensibility. It was selected for this WhatsApp integration because it offers a developer-friendly experience with features like built-in validation, logging, and support for TypeScript, making it ideal for robust application development.
Why use TypeScript with Node.js for WhatsApp integration?
TypeScript adds static typing to JavaScript, which enhances code quality, maintainability, and developer experience. Using it with Node.js for this integration helps prevent errors during development due to type checking, thus increasing overall reliability of the service.
When should I use a template message vs. a text message?
Use template messages when you need structured, pre-approved content for specific use cases, such as appointment reminders or order confirmations. Text messages are suitable for ad-hoc communication or simpler notifications. Plivo and WhatsApp have guidelines for template message approval.
Can I receive WhatsApp messages with this Fastify setup?
Yes, the service includes a webhook endpoint (`/webhooks/plivo`) designed to receive incoming WhatsApp messages and status updates from Plivo. This enables two-way communication, which is essential for features like customer support and interactive messaging.
How to set up Plivo for WhatsApp integration?
You need a Plivo account, a WhatsApp Business Account (WABA) linked to Plivo, and a WhatsApp-enabled phone number on your Plivo account. Once set up, you can use the Plivo API and the provided Node.js code to send and receive WhatsApp messages.
How to handle incoming WhatsApp messages in Fastify?
The `/webhooks/plivo` endpoint in the Fastify application receives incoming messages from Plivo's webhooks. The code then processes these messages, allowing you to implement actions like auto-replies, logging, or database updates. The example shows how to differentiate between incoming messages and message statuses.
What is the purpose of the Plivo Node SDK?
The Plivo Node SDK simplifies interaction with the Plivo API, making it easier to send messages, manage numbers, and handle other communication tasks directly from your Node.js applications without complex API calls.
What database is recommended for logging WhatsApp messages?
While not strictly required, Prisma, a modern ORM, is recommended along with PostgreSQL. Using a database helps store and retrieve WhatsApp messages, making logging and management functionalities within the application simpler and more organized.
How to run this Fastify WhatsApp application in development?
Use the `npm run dev` command. This script uses `nodemon` to automatically restart the application whenever code changes are made. It uses `ts-node` to execute the application without needing to compile it manually.
How to deploy this Fastify and Plivo integration?
The application is designed for containerization using Docker, which makes it easy to deploy it to various environments. The clear project structure, along with configurations for Docker, logging, and env variables, sets it up for deployment readiness.
How to secure the WhatsApp API endpoints?
The code provides a simple API key authentication mechanism using the `x-api-key` header. It's important to generate a strong, secure API key and keep it confidential, similar to the webhook secret. More robust authentication methods can be implemented based on your security needs.
How to validate Plivo webhook requests for security?
The code includes a `validatePlivoWebhook` function which uses Plivo's signature validation (V3) method to verify that incoming webhook requests are from Plivo. The guide provides further instructions to enable the verification.
What is the role of environment variables in this project?
Environment variables store sensitive configuration data like Plivo API credentials, database URL, port number, and the API key for endpoint protection. The `.env` file is used to manage these locally, and should never be committed to version control. `@fastify/env` is used for safe loading and validation of environment variables.