code examples
code examples
How to Build WhatsApp Integration with Twilio, Node.js, and NestJS in 2025
Complete production-ready guide for integrating WhatsApp with NestJS and Twilio API. Includes webhooks, media messages, security, and deployment best practices.
This guide provides a comprehensive walkthrough for building a production-ready WhatsApp integration using Node.js, the NestJS framework, and the Twilio API. You'll cover everything from initial project setup to deployment and monitoring, enabling your application to send and receive WhatsApp messages, including media.
By following this tutorial, you'll create a NestJS application capable of handling incoming WhatsApp messages via webhooks, replying dynamically, and initiating outbound messages programmatically. This solves the common need for businesses to engage with customers on WhatsApp for notifications, support, and conversational interactions.
Technologies Used:
- Node.js: The underlying JavaScript runtime environment (v22 LTS recommended for production, supported until April 2027).
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications (v10+ recommended). Its modular architecture and use of TypeScript make it ideal for production systems.
- Twilio API for WhatsApp: The third-party service providing the infrastructure to connect with the WhatsApp Business Platform.
- TypeScript: Adds static typing to JavaScript (v5.0+ recommended), improving code quality and maintainability.
- (Optional) Prisma/TypeORM: For database interactions to store message history or state.
- (Optional) Docker: For containerizing the application for deployment.
Quick Reference
| Feature | Implementation |
|---|---|
| Framework | NestJS v10+ with TypeScript v5.0+ |
| Node.js Version | v22 LTS (supported until April 2027) |
| API Service | Twilio WhatsApp Business API |
| Phone Format | E.164: +[country code][number] (1–15 digits) |
| Messaging Window | 24 hours (templates required outside window) |
| Security Method | HMAC SHA1 webhook signature validation |
| Media Support | Images, videos, PDFs (caption limit: 1024 chars) |
| Sandbox vs Production | Sandbox for dev; Production needs Facebook Business Manager |
| Primary Use Cases | Customer support, notifications, two-way messaging |
| Deployment Options | Docker, Heroku, AWS, DigitalOcean, Google Cloud |
Prerequisites:
- Install Node.js v22 LTS (or v20 minimum) and npm/yarn.
- Create a Twilio account with an activated WhatsApp Sandbox (for development) or a registered WhatsApp Business Sender (for production).
- Understand TypeScript, NestJS concepts (modules, controllers, services), and REST APIs.
- Install a tool to expose your local development server to the internet (e.g.,
ngrok). - Access a WhatsApp-enabled mobile device for testing.
Important WhatsApp Business API Limitations:
- 24-Hour Messaging Window: You can only send outbound messages to users who have messaged you within the last 24 hours, unless using approved WhatsApp Message Templates.
- Message Templates Required: For messages outside the 24-hour window, use pre-approved message templates from Twilio/WhatsApp.
- Sandbox vs Production: The Twilio Sandbox is for development only and requires users to opt in with a "join" command. Production requires a Facebook Business Manager account, an approved WhatsApp Business Profile, and can take several weeks for approval.
System Architecture:
The basic flow involves:
- Outbound: Your NestJS application calls the Twilio API to send a message. Twilio delivers it to the user's WhatsApp.
- Inbound: A user sends a message to your Twilio WhatsApp number. Twilio sends an HTTP POST request (webhook) to a predefined endpoint in your NestJS application. Your application processes the request and can optionally send a TwiML response back to Twilio to dictate a reply.
This guide walks you through building the NestJS Application component and configuring its interaction with Twilio.
How Do You Set Up the NestJS Project?
Initialize a new NestJS project and install the necessary dependencies.
-
Create a new NestJS project: Open your terminal and run the Nest CLI command:
bash# Install Nest CLI globally if you haven't already npm i -g @nestjs/cli # Create new project (NestJS v10+ will be installed) nest new nestjs-whatsapp-twilio cd nestjs-whatsapp-twilioChoose your preferred package manager (npm or yarn) when prompted. This creates a standard NestJS project structure.
-
Install Dependencies: Install the official Twilio helper library, a configuration module for environment variables, and a rate limiter for security.
bash# Using npm (installs latest stable versions) npm install twilio @nestjs/config @nestjs/throttler # Or using yarn yarn add twilio @nestjs/config @nestjs/throttlertwilio: The official Node.js SDK for interacting with the Twilio API (v4.x or v5.x compatible with Node.js 18+).@nestjs/config: For managing environment variables securely.@nestjs/throttler: Adds rate limiting to your webhook endpoint, protecting against abuse.
-
Configure Environment Variables: Create a
.envfile in the project root directory. Never commit this file to version control. Add.envto your.gitignore.dotenv# .env # Twilio Credentials – Find these in your Twilio Console # https://www.twilio.com/console TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxx # Your Twilio WhatsApp Number (Sandbox or Registered Sender) # Must include the 'whatsapp:' prefix and be in E.164 format # Example Sandbox Number: whatsapp:+14155238886 # E.164 format: +[country code][number] (no spaces, hyphens, or parentheses) TWILIO_WHATSAPP_NUMBER=whatsapp:+1xxxxxxxxxx # (REQUIRED FOR PRODUCTION) Auth token for securing your webhook endpoint # Generate a strong random string (minimum 32 characters recommended) WEBHOOK_AUTH_TOKEN=a_very_secure_random_string_min_32_chars # (Optional) Application Port PORT=3000TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN: Found on your main Twilio Console dashboard. These authenticate your API requests.TWILIO_WHATSAPP_NUMBER: The specific Twilio number enabled for WhatsApp (either your Sandbox number or a purchased/registered number). Find this in the Twilio Console under Messaging > Senders > WhatsApp Senders or the WhatsApp Sandbox settings. It must start withwhatsapp:followed by the number in E.164 format (e.g.,whatsapp:+14155238886).WEBHOOK_AUTH_TOKEN: Critical for production security. A secret token you define, used to verify that incoming webhook requests genuinely come from Twilio. Use a cryptographically secure random string (minimum 32 characters). Generate one with:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"PORT: The port your NestJS application will listen on.
-
Load Environment Variables: Modify
src/app.module.tsto import and configureConfigModule.typescript// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { AppController } from './app.controller'; import { AppService } from './app.service'; // Import other modules here later (e.g., TwilioModule, WebhookModule) @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make ConfigService available globally envFilePath: '.env', // Specify the env file path }), // Add other modules here ], controllers: [AppController], providers: [AppService], }) export class AppModule {}This setup makes environment variables accessible throughout your application via NestJS's
ConfigService.
How Do You Implement Core Functionality?
Create a dedicated module and service for handling Twilio interactions and another module/controller for the webhook.
How Do You Build the Twilio Service?
This service encapsulates the logic for initializing the Twilio client and sending messages.
-
Generate the Twilio module and service:
bashnest generate module twilio nest generate service twilio -
Implement the
TwilioService:typescript// src/twilio/twilio.service.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import twilio, { Twilio } from 'twilio'; @Injectable() export class TwilioService implements OnModuleInit { private readonly logger = new Logger(TwilioService.name); private client: Twilio; private twilioWhatsAppNumber: string; constructor(private readonly configService: ConfigService) {} onModuleInit() { const accountSid = this.configService.get<string>('TWILIO_ACCOUNT_SID'); const authToken = this.configService.get<string>('TWILIO_AUTH_TOKEN'); this.twilioWhatsAppNumber = this.configService.get<string>( 'TWILIO_WHATSAPP_NUMBER', ); if (!accountSid || !authToken || !this.twilioWhatsAppNumber) { this.logger.error( 'Twilio credentials missing in environment variables.', ); throw new Error( 'Twilio credentials missing. Ensure TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_WHATSAPP_NUMBER are set.', ); } // Validate E.164 format for WhatsApp number if (!this.twilioWhatsAppNumber.match(/^whatsapp:\+[1-9]\d{1,14}$/)) { throw new Error( 'TWILIO_WHATSAPP_NUMBER must be in format whatsapp:+[country code][number] (E.164 format)', ); } this.client = twilio(accountSid, authToken); this.logger.log('Twilio client initialized successfully.'); } async sendWhatsAppMessage(to: string, body: string): Promise<string | null> { // Ensure 'to' number is in E.164 format with whatsapp: prefix const formattedTo = to.startsWith('whatsapp:') ? to : `whatsapp:${to}`; // Validate E.164 format for recipient if (!formattedTo.match(/^whatsapp:\+[1-9]\d{1,14}$/)) { this.logger.error(`Invalid recipient phone number format: ${to}. Must be E.164 format: +[country code][number]`); return null; } try { const message = await this.client.messages.create({ from: this.twilioWhatsAppNumber, to: formattedTo, body: body, }); this.logger.log(`WhatsApp message sent to ${formattedTo}: SID ${message.sid}`); return message.sid; } catch (error) { this.logger.error(`Failed to send WhatsApp message to ${formattedTo}: ${error.message}`); return null; } } async sendWhatsAppMediaMessage(to: string, mediaUrl: string, body?: string): Promise<string | null> { // Ensure 'to' number is in E.164 format with whatsapp: prefix const formattedTo = to.startsWith('whatsapp:') ? to : `whatsapp:${to}`; // Validate E.164 format for recipient if (!formattedTo.match(/^whatsapp:\+[1-9]\d{1,14}$/)) { this.logger.error(`Invalid recipient phone number format: ${to}. Must be E.164 format: +[country code][number]`); return null; } // WhatsApp supports captions with images (up to 1024 characters per WhatsApp API specs) const messageOptions: any = { from: this.twilioWhatsAppNumber, to: formattedTo, mediaUrl: [mediaUrl], // Must be an array }; if (body) { // WhatsApp caption limit is 1024 characters if (body.length > 1024) { this.logger.warn(`Caption exceeds WhatsApp limit of 1024 characters. It may be truncated.`); } messageOptions.body = body; } try { const message = await this.client.messages.create(messageOptions); this.logger.log(`WhatsApp media message sent to ${formattedTo}: SID ${message.sid}`); return message.sid; } catch (error) { this.logger.error(`Failed to send WhatsApp media message to ${formattedTo}: ${error.message}`); return null; } } // Helper to get the initialized client if needed elsewhere getClient(): Twilio { return this.client; } // Helper to get the configured WhatsApp number getWhatsAppNumber(): string { return this.twilioWhatsAppNumber; } }- Uses
OnModuleInitto initialize the client when the module loads. - Injects
ConfigServiceto securely retrieve credentials from environment variables. - E.164 validation ensures both sender and recipient numbers follow the format:
+[country code][number](1–15 digits total, no spaces/hyphens). - Includes error handling for missing credentials and failed API calls.
- Ensures the
tonumber includes thewhatsapp:prefix. - Requires media URLs as an array.
- WhatsApp caption limit (1024 characters) is validated and logged.
- Provides
getWhatsAppNumber()helper.
- Uses
-
Update
TwilioModule: EnsureTwilioServiceis provided and exported, and importConfigModule.typescript// src/twilio/twilio.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TwilioService } from './twilio.service'; @Module({ imports: [ConfigModule], // Import ConfigModule providers: [TwilioService], exports: [TwilioService], // Export service for use in other modules }) export class TwilioModule {} -
Import
TwilioModuleintoAppModule:typescript// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TwilioModule } from './twilio/twilio.module'; // Import TwilioModule @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, // Add TwilioModule ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
How Do You Create the Webhook Controller?
This controller handles incoming messages from Twilio.
-
Generate the Webhook module and controller:
bashnest generate module webhook nest generate controller webhook -
Implement the
WebhookController:typescript// src/webhook/webhook.controller.ts import { Controller, Post, Body, Res, Logger, Req, Headers, HttpCode, HttpStatus } from '@nestjs/common'; import { Response, Request } from 'express'; import { twiml } from 'twilio'; import { ConfigService } from '@nestjs/config'; import * as crypto from 'crypto'; @Controller('webhooks') export class WebhookController { private readonly logger = new Logger(WebhookController.name); private readonly webhookAuthToken: string; constructor(private readonly configService: ConfigService) { this.webhookAuthToken = this.configService.get<string>('WEBHOOK_AUTH_TOKEN'); if (!this.webhookAuthToken) { this.logger.warn( 'SECURITY WARNING: WEBHOOK_AUTH_TOKEN is not set. Twilio signature validation will be skipped. ' + 'This is UNSAFE for production! Generate a secure token with: ' + 'node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"' ); } } @Post('twilio/whatsapp') @HttpCode(HttpStatus.OK) async handleIncomingWhatsApp( @Body() body: any, // Twilio sends form-urlencoded data @Res() res: Response, @Req() req: Request, @Headers('X-Twilio-Signature') twilioSignature: string, ) { // CRITICAL SECURITY: Validate Twilio Signature // Prevents malicious actors from spoofing webhook requests // Reference: https://www.twilio.com/docs/usage/webhooks/webhooks-security if (this.webhookAuthToken && !this.validateTwilioRequest(req, twilioSignature)) { this.logger.warn('Invalid Twilio signature received. Rejecting request.'); res.status(HttpStatus.FORBIDDEN).send('Invalid Twilio Signature'); return; } else if (!this.webhookAuthToken && !twilioSignature) { this.logger.log('No WEBHOOK_AUTH_TOKEN set and no X-Twilio-Signature header received. Proceeding without validation (UNSAFE for production).'); } else if (!this.webhookAuthToken && twilioSignature) { this.logger.warn('Received X-Twilio-Signature, but skipping validation because WEBHOOK_AUTH_TOKEN is not set (UNSAFE for production).'); } const sender = body.From; // e.g., whatsapp:+15551234567 const messageBody = body.Body; const numMedia = parseInt(body.NumMedia || '0', 10); const messageSid = body.MessageSid; this.logger.log(`Received WhatsApp message SID: ${messageSid} from ${sender}`); if (messageBody) { this.logger.log(`Message Body: ${messageBody}`); } // Handle Media if (numMedia > 0) { const mediaUrl = body.MediaUrl0; // WhatsApp typically sends one media item const mediaContentType = body.MediaContentType0; this.logger.log(`Received media (${mediaContentType}) from ${sender} at ${mediaUrl}`); // Add your media processing logic here if needed } // Generate TwiML Reply (Example: Echo Bot) const twimlResponse = new twiml.MessagingResponse(); if (numMedia > 0) { twimlResponse.message(`Thanks for the media! We received ${numMedia} item(s).`); } else if (messageBody?.toLowerCase().trim() === 'hello') { twimlResponse.message(`Hi there! You said: ${messageBody}`); } else if (messageBody) { twimlResponse.message(`Echo: ${messageBody}`); } else { twimlResponse.message('Received your message.'); } // Send Response res.setHeader('Content-Type', 'text/xml'); res.send(twimlResponse.toString()); } // Twilio Signature Validation Helper // Implements Twilio's webhook security validation // Reference: https://www.twilio.com/docs/usage/webhooks/webhooks-security private validateTwilioRequest(req: Request, twilioSignature: string): boolean { if (!this.webhookAuthToken) { this.logger.error('validateTwilioRequest called without webhookAuthToken being set.'); return false; } if (!twilioSignature) { this.logger.warn('Validation failed: X-Twilio-Signature header missing.'); return false; } // Construct the full URL Twilio used for the request // Use X-Forwarded-Proto if behind a proxy (common in production) const protocol = req.headers['x-forwarded-proto'] || req.protocol; const fullUrl = `${protocol}://${req.get('host')}${req.originalUrl}`; // Twilio calculates signature based on URL + sorted POST parameters // Reference: https://www.twilio.com/docs/usage/security#validating-requests const params = req.body || {}; const sortedKeys = Object.keys(params).sort(); const paramString = sortedKeys.reduce((acc, key) => acc + key + params[key], ''); const expectedSignature = crypto .createHmac('sha1', this.webhookAuthToken) .update(Buffer.from(fullUrl + paramString, 'utf-8')) .digest('base64'); // Compare signatures using timing-safe comparison (prevents timing attacks) let isValid = false; try { isValid = crypto.timingSafeEqual( Buffer.from(twilioSignature), Buffer.from(expectedSignature) ); } catch (e) { this.logger.warn(`Error during timingSafeEqual comparison: ${e.message}`); isValid = false; } if (!isValid) { this.logger.warn(`Signature validation failed. Expected: ${expectedSignature}, Received: ${twilioSignature}`); } return isValid; } }- Listens for POST requests at
/webhooks/twilio/whatsapp. - CRITICAL SECURITY: Implements Twilio signature validation using HMAC SHA1 per Twilio's webhook security documentation.
- Uses
crypto.timingSafeEqualto prevent timing attacks during signature comparison. - Logs explicit warnings if
WEBHOOK_AUTH_TOKENis not set (unsafe for production). - Parses standard Twilio webhook parameters.
- Uses
twilio.twiml.MessagingResponseto generate TwiML for replies. - Sets response
Content-Typetotext/xml.
- Listens for POST requests at
-
Update
WebhookModule:typescript// src/webhook/webhook.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { WebhookController } from './webhook.controller'; // Import PrismaModule if you inject PrismaService into the controller // import { PrismaModule } from '../prisma/prisma.module'; @Module({ imports: [ ConfigModule, // Make ConfigService available // PrismaModule // Import if needed by controller/services ], controllers: [WebhookController], providers: [], // Add any webhook-specific services here if needed }) export class WebhookModule {} -
Import
WebhookModuleintoAppModule:typescript// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TwilioModule } from './twilio/twilio.module'; import { WebhookModule } from './webhook/webhook.module'; // Import WebhookModule // Other imports... // import { PrismaModule } from './prisma/prisma.module'; // Import if using Prisma // import { MessageModule } from './message/message.module'; // Import if using API layer @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, WebhookModule, // Add WebhookModule // PrismaModule, // Add if using Prisma // MessageModule, // Add if using API layer // Other modules... ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
How Do You Build a Complete API Layer (Optional)?
While the core functionality is driven by the webhook, you might want an API endpoint to trigger outbound messages from other parts of your system or external clients.
-
Generate a Message module, controller, and service:
bashnest generate module message nest generate controller message nest generate service message -
Create Data Transfer Objects (DTOs) for validation: Install class-validator and class-transformer:
bashnpm install class-validator class-transformer # or yarn add class-validator class-transformerEnable
ValidationPipeglobally insrc/main.ts:typescript// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ConfigService } from '@nestjs/config'; import { Logger, ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); const port = configService.get<number>('PORT', 3000); const logger = new Logger('Bootstrap'); // Enable global validation pipe with strict settings app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO transform: true, // Automatically transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if unknown properties are present transformOptions: { enableImplicitConversion: true, // Allow automatic type conversion }, })); await app.listen(port); logger.log(`Application listening on port ${port}`); } bootstrap();Create DTO files:
typescript// src/message/dto/send-message.dto.ts import { IsNotEmpty, IsString, Matches, IsOptional, IsUrl, ValidateIf } from 'class-validator'; export class SendMessageDto { @IsNotEmpty() @IsString() // E.164 format: +[country code][number] (1–15 digits total, no spaces/hyphens) // Reference: ITU-T Recommendation E.164 @Matches(/^\+?[1-9]\d{1,14}$/, { message: 'Phone number must be in E.164 format: +[country code][number] (e.g., +15551234567)' }) to: string; @ValidateIf(o => !o.mediaUrl) // body required if mediaUrl not provided @IsNotEmpty({ message: 'Either body or mediaUrl must be provided' }) @IsString() @IsOptional() body?: string; @ValidateIf(o => !o.body) // mediaUrl required if body not provided @IsNotEmpty({ message: 'Either body or mediaUrl must be provided' }) @IsString() @IsUrl({}, { message: 'mediaUrl must be a valid URL' }) @IsOptional() mediaUrl?: string; } -
Implement
MessageService: This service usesTwilioServiceto send messages.typescript// src/message/message.service.ts import { Injectable, Logger, BadRequestException, InternalServerErrorException } from '@nestjs/common'; import { TwilioService } from '../twilio/twilio.service'; import { SendMessageDto } from './dto/send-message.dto'; @Injectable() export class MessageService { private readonly logger = new Logger(MessageService.name); constructor( private readonly twilioService: TwilioService, ) {} async sendWhatsApp(sendMessageDto: SendMessageDto): Promise<{ sid: string }> { const { to, body, mediaUrl } = sendMessageDto; // DTO validation should prevent both being empty, but double-check for safety if (!body && !mediaUrl) { throw new BadRequestException('Either body or mediaUrl must be provided.'); } let sid: string | null = null; let errorOccurred = false; try { if (mediaUrl) { // TwilioService adds the 'whatsapp:' prefix internally sid = await this.twilioService.sendWhatsAppMediaMessage(to, mediaUrl, body); } else if (body) { // TwilioService adds the 'whatsapp:' prefix internally sid = await this.twilioService.sendWhatsAppMessage(to, body); } } catch (error) { this.logger.error(`Error sending WhatsApp message to ${to}: ${error.message}`, error.stack); errorOccurred = true; } if (!sid || errorOccurred) { throw new InternalServerErrorException(`Failed to send WhatsApp message to ${to} via Twilio.`); } this.logger.log(`Successfully queued WhatsApp message to ${to}. SID: ${sid}`); return { sid }; } } -
Implement
MessageController:typescript// src/message/message.controller.ts import { Controller, Post, Body, Logger, HttpCode, HttpStatus } from '@nestjs/common'; import { MessageService } from './message.service'; import { SendMessageDto } from './dto/send-message.dto'; @Controller('messages') export class MessageController { private readonly logger = new Logger(MessageController.name); constructor(private readonly messageService: MessageService) {} @Post('whatsapp/send') @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as sending is asynchronous async sendWhatsAppMessage(@Body() sendMessageDto: SendMessageDto): Promise<{ message: string; sid: string }> { this.logger.log(`Received request to send WhatsApp message to ${sendMessageDto.to}`); const result = await this.messageService.sendWhatsApp(sendMessageDto); return { message: 'WhatsApp message accepted for delivery.', sid: result.sid, }; } }- Uses the DTO for request body validation.
- Delegates sending logic to
MessageService. - Consider adding authentication/authorization (
@UseGuards) to protect this endpoint. - Returns
202 Acceptedas the message is queued, not necessarily delivered instantly.
-
Update Modules: Ensure
MessageServiceis inprovidersandMessageControlleris incontrollerswithinsrc/message/message.module.ts. ImportTwilioModuleintoMessageModuleto makeTwilioServiceavailable for injection. Finally, importMessageModuleintosrc/app.module.ts.typescript// src/message/message.module.ts import { Module } from '@nestjs/common'; import { MessageService } from './message.service'; import { MessageController } from './message.controller'; import { TwilioModule } from '../twilio/twilio.module'; // Import TwilioModule // Import PrismaModule if MessageService uses it // import { PrismaModule } from '../prisma/prisma.module'; // Import ConfigModule if MessageService uses it // import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ TwilioModule, // Make TwilioService available // PrismaModule // Import if needed // ConfigModule // Import if needed ], controllers: [MessageController], providers: [MessageService], }) export class MessageModule {} // src/app.module.ts (ensure MessageModule is added) import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TwilioModule } from './twilio/twilio.module'; import { WebhookModule } from './webhook/webhook.module'; import { MessageModule } from './message/message.module'; // Import MessageModule // import { PrismaModule } from './prisma/prisma.module'; // Import if using Prisma @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, WebhookModule, MessageModule, // Add MessageModule // PrismaModule, // Add if using Prisma ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
Testing the API Endpoint:
Test the /messages/whatsapp/send endpoint using curl or Postman (replace YOUR_PORT and YOUR_PHONE_NUMBER):
# Send a text message
curl -X POST http://localhost:YOUR_PORT/messages/whatsapp/send \
-H 'Content-Type: application/json' \
-d '{
"to": "+1YOUR_PHONE_NUMBER",
"body": "Hello from NestJS API!"
}'
# Send a media message (replace URL)
curl -X POST http://localhost:YOUR_PORT/messages/whatsapp/send \
-H 'Content-Type: application/json' \
-d '{
"to": "+1YOUR_PHONE_NUMBER",
"mediaUrl": "https://demo.twilio.com/owl.png",
"body": "Optional caption for image"
}'Frequently Asked Questions
What is the WhatsApp 24-hour messaging window?
The 24-hour messaging window is a WhatsApp Business API policy that allows you to send free-form messages to users only within 24 hours after they last messaged you. Outside this window, you must use pre-approved WhatsApp Message Templates. This policy ensures businesses don't spam users and maintains WhatsApp's user experience standards.
How do I use WhatsApp Message Templates?
WhatsApp Message Templates are pre-approved message formats required for messaging outside the 24-hour window. You create templates in your Twilio Console under WhatsApp > Message Templates, submit them for WhatsApp approval (typically 24–48 hours), and then reference the approved template SID when sending messages via the Twilio API. Templates support variables for personalization.
What's the difference between Twilio Sandbox and Production?
The Twilio WhatsApp Sandbox is a development environment where users must opt in by sending a "join" command to your sandbox number. It's free but limited to testing. Production requires a Facebook Business Manager account, an approved WhatsApp Business Profile, and a registered phone number. Production setup can take several weeks for approval but allows unrestricted messaging to any WhatsApp user.
How do I validate E.164 phone numbers?
E.164 is the international phone number format: +[country code][number] with 1–15 digits total, no spaces, hyphens, or parentheses. Use the regex pattern /^\+?[1-9]\d{1,14}$/ to validate. For example, +15551234567 (US) and +447911123456 (UK) are valid. Always prepend whatsapp: when sending to Twilio (e.g., whatsapp:+15551234567).
What are WhatsApp caption limits for media messages?
WhatsApp allows captions of up to 1024 characters when sending media (images, videos, PDFs). If you exceed this limit, WhatsApp may truncate the caption. The TwilioService implementation in this guide includes validation that logs a warning when captions exceed 1024 characters, helping you catch issues during development.
How do I secure Twilio webhooks with signature validation?
Twilio sends an X-Twilio-Signature header with each webhook request, calculated using HMAC SHA1 with your Auth Token. Your application reconstructs the signature using the full webhook URL plus sorted POST parameters, then compares it with the received signature using crypto.timingSafeEqual() to prevent timing attacks. This ensures requests genuinely come from Twilio. Reference: Twilio Webhook Security.
Can I send media messages with captions?
Yes, WhatsApp supports sending media (images, videos, PDFs, audio) with optional captions up to 1024 characters. Use the sendWhatsAppMediaMessage() method in the TwilioService, providing the media URL and optional body text. The media URL must be publicly accessible, and Twilio will download and forward it to WhatsApp. Supported formats include JPEG, PNG, MP4, PDF, and more.
What Node.js version should I use for production?
Node.js v22 LTS is recommended for production deployments. It's the current Active LTS release, supported until October 2025 and maintained until April 2027. This ensures long-term stability, security updates, and compatibility with modern libraries like Twilio SDK v5.x and NestJS v10+. Minimum requirement is Node.js v20, but v22 LTS provides the best production experience.
How do I handle webhook signature validation failures?
Signature validation failures indicate either configuration issues or potential security threats. Common causes include: incorrect WEBHOOK_AUTH_TOKEN (must match Twilio Auth Token), URL mismatch (Twilio uses the exact webhook URL), or proxy/HTTPS issues. Log the expected vs received signatures for debugging. In production, always reject invalid signatures with HTTP 403 to prevent spoofing attacks.
What are the production deployment requirements?
For production deployment, ensure: (1) Node.js v22 LTS installed, (2) Environment variables configured securely (use secrets management, not .env files), (3) HTTPS endpoint for webhooks (Twilio requires HTTPS in production), (4) WEBHOOK_AUTH_TOKEN configured for security, (5) Facebook Business Manager account with approved WhatsApp Business Profile, (6) Registered phone number with WhatsApp Business API access, and (7) Rate limiting enabled (@nestjs/throttler) to prevent abuse.
Conclusion
You've successfully built a production-ready WhatsApp integration using NestJS, TypeScript, and the Twilio API. This implementation provides:
Key Implementation Points:
- Secure webhook handling with HMAC SHA1 signature validation to prevent spoofing attacks
- E.164 phone number validation ensuring proper international format for all messages
- Comprehensive error handling with detailed logging for debugging and monitoring
- Media message support with caption validation (1024-character limit)
- Modular architecture following NestJS best practices for maintainability and scalability
- Environment-based configuration using ConfigService for secure credential management
- Production-ready code with TypeScript type safety and validation pipes
Next Steps for Production:
- Register for WhatsApp Business API through Facebook Business Manager and complete the approval process (typically 2–4 weeks)
- Implement message template management for messaging outside the 24-hour window using Twilio's template API
- Add database persistence using Prisma or TypeORM to store message history, conversation state, and user preferences
- Configure rate limiting with
@nestjs/throttlerto protect webhook endpoints from abuse - Set up monitoring and alerting using services like Sentry, DataDog, or New Relic for error tracking
- Deploy with HTTPS to a cloud provider (AWS, Google Cloud, DigitalOcean, Heroku) as Twilio requires secure webhook endpoints
- Implement automated testing with Jest for unit tests and E2E tests to ensure reliability
Additional Resources:
- Twilio WhatsApp API Documentation – Official Twilio WhatsApp API reference
- NestJS Documentation – Comprehensive NestJS framework documentation
- WhatsApp Business Platform Policies – Official WhatsApp Business policies and guidelines
- Twilio Webhook Security Guide – Best practices for securing Twilio webhooks
- E.164 Number Format Specification – ITU-T international phone number standard
- Node.js LTS Release Schedule – Official Node.js release and support timeline
This integration serves as a solid foundation for building customer support systems, notification services, conversational AI applications, and two-way messaging solutions at scale.
Frequently Asked Questions
how to integrate whatsapp with node.js
Integrate WhatsApp with Node.js using the Twilio API and the NestJS framework. This involves setting up a NestJS project, installing the Twilio Node.js library, and configuring your application to handle incoming and outgoing WhatsApp messages via webhooks and the Twilio API.
what is nestjs used for in whatsapp integration
NestJS provides a robust and scalable framework for building the server-side logic of your WhatsApp integration. Its modular architecture and TypeScript support enhance code organization and maintainability for production-level applications.
why use twilio for whatsapp integration
Twilio acts as the bridge between your Node.js application and the WhatsApp Business Platform. It handles the complex infrastructure and communication required to send and receive WhatsApp messages, simplifying the integration process.
when should I use prisma in whatsapp integration
Consider using Prisma or TypeORM if you need to persist data, such as message history or user interactions, within a database. These ORMs streamline database operations within your NestJS application.
can I send media messages with whatsapp twilio
Yes, you can send media messages (images, audio, video) through the Twilio API for WhatsApp. The `sendWhatsAppMediaMessage` function within the provided `TwilioService` handles this, accepting a media URL as a parameter. Ensure the URL is correct and publicly accessible.
how to set up twilio whatsapp sandbox
To set up a Twilio WhatsApp Sandbox, create a Twilio account and navigate to the WhatsApp Sandbox settings in the Twilio Console. You'll receive a dedicated Sandbox number and instructions for connecting your mobile device for testing purposes.
what is a twilio webhook for whatsapp
A Twilio webhook is an HTTP endpoint in your NestJS application that receives incoming WhatsApp messages. When a user sends a message to your Twilio WhatsApp number, Twilio forwards it to your specified webhook URL as an HTTP POST request.
how to validate twilio webhook signature
Validating the Twilio webhook signature ensures that incoming requests originate from Twilio. Use the `validateTwilioRequest` function provided in the `WebhookController`, using a strong secret `WEBHOOK_AUTH_TOKEN`. This is critical to prevent unauthorized access or malicious actions. The code will generate warnings if this token is missing.
why is the whatsapp number prefixed with whatsapp:
The `whatsapp:` prefix in the `TWILIO_WHATSAPP_NUMBER` environment variable and in the `to` parameter of sending functions explicitly identifies the destination as a WhatsApp number, distinguishing it from other communication channels Twilio supports.
how to send whatsapp message from nestjs api
Create an API endpoint in your NestJS application that utilizes the `TwilioService` to send outbound WhatsApp messages. The `MessageController` and `MessageService` examples show how to structure this, including DTO validation and error handling.
what is the x-twilio-signature header
The `X-Twilio-Signature` header, included in Twilio's webhook requests, contains a cryptographic signature of the request. This signature allows you to verify the request's authenticity and confirm it came from Twilio, essential for security.
how to reply to incoming whatsapp messages with twilio
To reply to incoming WhatsApp messages, use the Twilio Messaging Response TwiML (XML) format. The `WebhookController` example demonstrates constructing a `MessagingResponse` object and sending a TwiML reply back to Twilio, which then delivers it to the user.
when to use ngrok with twilio whatsapp
Use ngrok during local development to create a publicly accessible URL for your webhook endpoint. This allows Twilio to send webhook requests to your application even when it's running on your local machine.
what are twilio account sid and auth token
Your Twilio Account SID and Auth Token are unique credentials that authenticate your application with the Twilio API. Find these credentials in your Twilio account dashboard; keep them secure and never expose them in public code repositories.