Comprehensive step-by-step guide for integrating Infobip's WhatsApp API into NestJS applications. Learn to send messages, handle webhooks, implement security, and deploy production-ready WhatsApp communication features.
This guide provides a comprehensive, step-by-step walkthrough for integrating Infobip's WhatsApp API into a NestJS application using Node.js. We'll cover everything from initial project setup and sending messages to handling incoming webhooks, error management, security, and deployment best practices. By the end, you'll have a robust foundation for production-level WhatsApp communication.
We assume minimal prior knowledge beyond basic NestJS and Node.js concepts, explaining each step and decision clearly. We prioritize practical, real-world implementation details to ensure your integration is reliable and scalable.
Project Overview and Goals
What We're Building:
We will build a NestJS application capable of:
Sending WhatsApp Messages: Exposing an API endpoint to trigger sending text messages via the Infobip WhatsApp API.
Receiving WhatsApp Messages: Setting up a webhook endpoint to receive incoming messages sent to our designated Infobip WhatsApp number.
Basic Message Logging: Storing basic information about sent and received messages (optional, but good practice).
Problem Solved:
This integration enables applications to leverage WhatsApp for various communication needs, such as notifications, customer support interactions, OTP delivery, or marketing messages (respecting WhatsApp policies), automating processes previously handled manually or through less direct channels.
Technologies Used:
Node.js: The JavaScript runtime environment.
NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Its modular architecture and built-in features (dependency injection, validation pipes, configuration management) accelerate development.
TypeScript: Superset of JavaScript adding static types, enhancing code quality and maintainability.
Infobip: The Communication Platform as a Service (CPaaS) provider whose API and Node.js SDK we will use for WhatsApp communication.
@infobip-api/sdk: The official Infobip Node.js SDK, simplifying interactions with the Infobip API.
@nestjs/config: For managing environment variables securely.
class-validator & class-transformer: For robust request validation using Data Transfer Objects (DTOs).
(Optional) TypeORM / Prisma: For database interactions if message persistence is required.
(Optional) Docker: For containerizing the application for deployment.
System Architecture:
graph LR
Client[API Client / Frontend] -- HTTPS Request --> NestJS_API[NestJS Application];
NestJS_API -- Send Message Request (SDK) --> Infobip[Infobip Platform];
Infobip -- Sends Message --> WhatsApp[WhatsApp User];
WhatsApp -- Sends Reply --> Infobip;
Infobip -- Incoming Message (Webhook POST) --> NestJS_API;
NestJS_API -- (Optional) Log Message --> DB[(Database)];
subgraph NestJS Application
direction LR
Controller[WhatsappController] --> Service[WhatsappService];
Service -- Uses --> InfobipSDK[@infobip-api/sdk];
Service -- Uses --> Config[ConfigService];
Service -- (Optional) Interacts --> Repo[MessageRepository];
end
subgraph Infobip
direction TB
APIs[Infobip APIs]
WebhookService[Webhook Service]
end
Prerequisites:
Node.js: v20 or later installed (Node.js 20 LTS is recommended as of 2025; Node.js 16 and 18 reached End-of-Life in 2023 and April 2025, respectively).
Infobip Account: A registered Infobip account (a free trial is available).
Access to your Infobip API Key and Base URL.
A configured WhatsApp Sender number within your Infobip account.
(Optional) ngrok or similar: For testing webhooks locally.
(Optional) Docker Desktop: For containerization.
(Optional) Postman or curl: For testing API endpoints.
1. Setting up the Project
Let's initialize our NestJS project and install necessary dependencies.
Create a New NestJS Project:
Open your terminal and run:
Bash
nest new infobip-whatsapp-app
cd infobip-whatsapp-app
Choose your preferred package manager (npm or yarn) when prompted.
Install Dependencies:
We need the Infobip SDK and NestJS configuration module.
Bash
# Using npm
npm install @infobip-api/sdk @nestjs/config dotenv class-validator class-transformer
# Using yarn
yarn add @infobip-api/sdk @nestjs/config dotenv class-validator class-transformer
@infobip-api/sdk: The core library for interacting with Infobip.
@nestjs/config: Manages environment variables.
dotenv: Loads environment variables from a .env file for local development.
class-validator & class-transformer: Enable validation using DTOs.
Configure Environment Variables:
Create a .env file in the project root:
Bash
touch .env
Add your Infobip credentials and sender number to .env. Never commit this file to version control.
ini
#.env# Infobip CredentialsINFOBIP_BASE_URL=your_infobip_base_url.api.infobip.com
INFOBIP_API_KEY=YourInfobipApiKeyGoesHere
INFOBIP_WHATSAPP_SENDER=YourInfobipWhatsAppSenderNumber # e.g., 447860099299# Application Port (Optional)PORT=3000# Webhook Secret (Add if implementing signature verification)# INFOBIP_WEBHOOK_SECRET=YourInfobipWebhookSecret
How to find these values:
INFOBIP_BASE_URL & INFOBIP_API_KEY: Log in to your Infobip Portal. Navigate to the homepage or API Keys management section. Your unique Base URL and API Key will be displayed.
INFOBIP_WHATSAPP_SENDER: Navigate to Channels and Numbers -> WhatsApp -> Numbers. Copy the registered sender number you intend to use. Ensure it's approved and active.
Load Environment Variables using ConfigModule:
Import and configure ConfigModule in your main application module (src/app.module.ts). Making it global simplifies access in other modules.
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule } from'@nestjs/config';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { WhatsappModule } from'./whatsapp/whatsapp.module'; // We will create this next@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Makes ConfigService available globallyenvFilePath: '.env', // Specifies the env file path
}),
WhatsappModule, // Import the feature module
],
controllers: [AppController],
providers: [AppService],
})
exportclassAppModule {}
Why ConfigModule? It provides a structured and secure way to handle environment-specific configurations, preventing hardcoding sensitive data like API keys directly in the code. isGlobal: true avoids needing to import ConfigModule into every feature module that requires access to environment variables.
Enable Validation Pipe Globally:
Configure the ValidationPipe in src/main.ts to automatically validate incoming request payloads against DTOs.
TypeScript
// src/main.tsimport { NestFactory } from'@nestjs/core';
import { AppModule } from'./app.module';
import { ValidationPipe } from'@nestjs/common'; // Import ValidationPipeimport { ConfigService } from'@nestjs/config'; // Import ConfigServiceasyncfunctionbootstrap() {
const app = awaitNestFactory.create(AppModule);
const configService = app.get(ConfigService); // Get ConfigService instance// Enable global validation pipe
app.useGlobalPipes(newValidationPipe({
whitelist: true, // Strip properties that do not have any decoratorsforbidNonWhitelisted: true, // Throw error if non-whitelisted values are providedtransform: true, // Automatically transform payloads to DTO instances
}));
const port = configService.get<number>('PORT') || 3000; // Get port from envawait app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
Create the WhatsApp Feature Module:
Generate a module, controller, and service for our WhatsApp functionality using the NestJS CLI.
Bash
nest generate module whatsapp
nest generate controller whatsapp --no-spec # Optional: skip test file
nest generate service whatsapp --no-spec # Optional: skip test file
This creates a src/whatsapp directory with whatsapp.module.ts, whatsapp.controller.ts, and whatsapp.service.ts. The CLI automatically updates src/app.module.ts to import WhatsappModule.
We'll implement the logic to send a WhatsApp message via the Infobip SDK within our WhatsappService.
Initialize Infobip Client in the Service:
Inject ConfigService into WhatsappService and initialize the Infobip client in the constructor.
TypeScript
// src/whatsapp/whatsapp.service.tsimport { Injectable, Logger, InternalServerErrorException, BadRequestException } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import { Infobip, AuthType } from'@infobip-api/sdk'; // Import Infobip SDK elements@Injectable()
exportclassWhatsappService {
privatereadonly logger = newLogger(WhatsappService.name);
privateinfobipClient: Infobip;
privatewhatsappSender: string;
constructor(privateconfigService: ConfigService) {
const apiKey = this.configService.get<string>('INFOBIP_API_KEY');
const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL');
this.whatsappSender = this.configService.get<string>('INFOBIP_WHATSAPP_SENDER');
if (!apiKey || !baseUrl || !this.whatsappSender) {
this.logger.error('Infobip API Key, Base URL, or Sender Number not configured in environment variables.');
thrownewInternalServerErrorException('WhatsApp Service is not configured correctly.');
}
this.infobipClient = newInfobip({
baseUrl: baseUrl,
apiKey: apiKey,
authType: AuthType.ApiKey, // Specify API Key authentication
});
this.logger.log('Infobip client initialized successfully.');
}
// Method to send a text message will go here
}
Why initialize here? The constructor is the ideal place to set up dependencies like the Infobip client. NestJS manages the service as a singleton by default, so the client is created only once. We also perform essential configuration checks.
Implement the sendTextMessage Method:
Add a method to handle sending the message payload.
TypeScript
// src/whatsapp/whatsapp.service.tsimport { Injectable, Logger, InternalServerErrorException, BadRequestException } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import { Infobip, AuthType } from'@infobip-api/sdk';
@Injectable()
exportclassWhatsappService {
privatereadonly logger = newLogger(WhatsappService.name);
privateinfobipClient: Infobip;
privatewhatsappSender: string;
constructor(privateconfigService: ConfigService) {
// ... (constructor logic as above) ...
}
asyncsendTextMessage(to: string, text: string): Promise<any> {
this.logger.log(`Attempting to send message to ${to}`);
// Basic validation (more robust validation happens at controller level via DTOs)if (!to || !text) {
thrownewBadRequestException('Recipient number (to) and message text cannot be empty.');
}
// Basic check for phone number format. Infobip usually expects international format without '+'.// WARNING: This regex is very basic. For production, use a robust library like 'libphonenumber-js'// or `class-validator`'s `@IsPhoneNumber(region)` in the DTO for proper validation.if (!/^\d{10,15}$/.test(to)) {
this.logger.warn(`Potential invalid phone number format for recipient: ${to}. Expected format like 447123456789 (international without '+').`);
// Depending on requirements, you might throw BadRequestException here:// throw new BadRequestException('Invalid recipient phone number format. Use international format without ""+"".');
}
const payload = {
type: 'text', // Specify message type as textfrom: this.whatsappSender,
to: to, // Recipient number (validated format is crucial)content: {
text: text, // The message content
},
};
try {
const response = awaitthis.infobipClient.channels.whatsapp.send(payload);
this.logger.log(`Message sent successfully to ${to}. Message ID: ${response.data.messages[0]?.messageId}`);
// Consider logging the full response for detailed tracking if needed// this.logger.debug(`Infobip API Response: ${JSON.stringify(response.data)}`);// Optional: Persist message status to database here// await this.messageRepository.save({ ... });return response.data; // Return the relevant part of the Infobip response
} catch (error) {
this.logger.error(`Failed to send message to ${to}: ${error.message}`, error.stack);
// Enhance error handling based on Infobip error structure if available// Example: Check error.response?.data for specific API error codes/messagesif (error.response?.data) {
this.logger.error(`Infobip API Error Details: ${JSON.stringify(error.response.data)}`);
}
// Re-throw a more specific HTTP exception if possible, or a generic onethrownewInternalServerErrorException(`Failed to send WhatsApp message: ${error.message}`);
}
}
// Method to handle incoming messages will go here
}
Why this structure? We encapsulate the Infobip interaction within the service, keeping the controller thin. The try...catch block handles potential API errors gracefully, logs them, and throws appropriate NestJS HTTP exceptions. Basic input validation is added as a safety measure, though the primary validation occurs via DTOs in the controller. We return the response.data which contains useful information like messageId.
3. Building the API Layer (Sending Messages)
Now, let's expose an endpoint in WhatsappController to trigger the sendTextMessage method.
Create a Data Transfer Object (DTO):
Define a DTO to specify the expected shape and validation rules for the request body.
TypeScript
// src/whatsapp/dto/send-message.dto.tsimport { IsNotEmpty, IsString, Length, IsPhoneNumber/* Recommended */ } from'class-validator';
exportclassSendMessageDto {
@IsNotEmpty()
@IsString()
// Basic length validation. For production, strongly consider using:// @IsPhoneNumber(null) // or specify region e.g. @IsPhoneNumber('GB')// Ensure the format matches Infobip expectations (usually international without '+', e.g., 447123456789)@Length(10, 15) // Adjust length constraints as needed based on expected formatto: string;
@IsNotEmpty()
@IsString()
@Length(1, 4096) // WhatsApp text message limit is 4,096 characters (Meta WhatsApp Cloud API)text: string;
}
Why DTOs? They define a clear contract for your API and, combined with ValidationPipe, provide automatic request validation, improving security and data integrity. Using @IsPhoneNumber is preferred for robust validation.
Implement the Controller Endpoint:
Inject WhatsappService and create a POST endpoint.
TypeScript
// src/whatsapp/whatsapp.controller.tsimport { Controller, Post, Body, Logger, HttpCode, HttpStatus } from'@nestjs/common';
import { WhatsappService } from'./whatsapp.service';
import { SendMessageDto } from'./dto/send-message.dto'; // Import the DTO@Controller('whatsapp') // Route prefix for this controllerexportclassWhatsappController {
privatereadonly logger = newLogger(WhatsappController.name);
constructor(privatereadonlywhatsappService: WhatsappService) {}
@Post('send') // Route: POST /whatsapp/send@HttpCode(HttpStatus.ACCEPTED) // Indicate request accepted, processing initiatedasyncsendMessage(@Body() sendMessageDto: SendMessageDto) {
this.logger.log(`Received request to send message: ${JSON.stringify(sendMessageDto)}`);
try {
// The 'to' number format should align with expectations set in the service/DTO commentsconst result = awaitthis.whatsappService.sendTextMessage(
sendMessageDto.to,
sendMessageDto.text,
);
// Return a minimal success response, including the message ID might be usefulreturn {
message: 'Message accepted for delivery.',
infobipResponse: {
bulkId: result.bulkId,
messageId: result.messages[0]?.messageId,
status: result.messages[0]?.status// Example status from response
}
};
} catch (error) {
this.logger.error(`Error in sendMessage endpoint: ${error.message}`, error.stack);
// Exceptions thrown from the service (like BadRequestException, InternalServerErrorException)// will be automatically handled by NestJS's exception filter.throw error; // Re-throw the error for the default exception handler
}
}
// Webhook endpoint will go here
}
Why @Body() sendMessageDto: SendMessageDto? This tells NestJS to parse the request body and validate it against the SendMessageDto using the globally configured ValidationPipe.
Why HttpStatus.ACCEPTED? Sending a message is often asynchronous. Returning 202 Accepted signifies the request is valid and processing has started, but doesn't guarantee immediate delivery.
Why re-throw error? NestJS has built-in exception handling. By re-throwing errors caught from the service, we let NestJS map them to appropriate HTTP error responses (e.g., 400 for BadRequestException, 500 for InternalServerErrorException).
Testing the Endpoint:
Start your application: npm run start:dev
Use curl or Postman to send a POST request:
Bash
curl -X POST http://localhost:3000/whatsapp/send \
-H 'Content-Type: application/json' \
-d '{
""to"": ""YOUR_REGISTERED_TEST_NUMBER"",
""text"": ""Hello from NestJS and Infobip!""
}'
Replace YOUR_REGISTERED_TEST_NUMBER with your registered test number (e.g., 447123456789). You should receive a 202 Accepted response and the message on your WhatsApp (if using your registered trial number). Check the application logs and the Infobip portal for status updates.
4. Integrating with Third-Party Services (Infobip Webhook)
To receive messages sent to your Infobip WhatsApp number, you need to configure a webhook.
Configure Webhook in Infobip Portal:
Log in to your Infobip Portal.
Navigate to Channels and Numbers -> WhatsApp -> Numbers.
Find your sender number and click Configure or similar option.
Locate the Webhook URL or Incoming Messages URL section.
Enter the publicly accessible URL of your NestJS application's webhook endpoint. For local development, use ngrok to expose your local port:
Run ngrok http 3000 (if your app runs on port 3000).
Copy the https://<your-ngrok-id>.ngrok.io URL.
Your webhook URL will be https://<your-ngrok-id>.ngrok.io/whatsapp/webhook (we'll create /whatsapp/webhook next).
Important Security: Note down any secret or authentication token Infobip provides for webhook verification. This is crucial for ensuring requests genuinely come from Infobip (see Section 7). Add this secret to your .env file (e.g., INFOBIP_WEBHOOK_SECRET).
Save the configuration in the Infobip portal.
Create DTO for Incoming Webhook Payload:
Define a DTO based on the expected structure of Infobip's incoming WhatsApp message webhook. Note: This structure might vary slightly; always refer to the official Infobip documentation for the most accurate payload format.
TypeScript
// src/whatsapp/dto/incoming-message.dto.tsimport { Type } from'class-transformer';
import { IsArray, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested, IsDateString } from'class-validator';
// Define nested structures based on Infobip's documentationclassMessageContent {
@IsString()
text: string;
// Add other potential content types like 'imageUrl', 'documentUrl', etc. if needed
}
classMessagePrice {
// Define price properties if needed (check Infobip docs)
}
classMessageStatus {
// Define status properties if needed (check Infobip docs)
}
classMessageContext {
// Define context properties if needed (e.g., for replies)@IsString() @IsOptional() from?: string;
@IsString() @IsOptional() id?: string;
}
classMessageResult {
@IsString() from: string;
@IsString() to: string;
@IsString() @IsOptional() integrationType?: string;
@IsString() messageId: string;
@IsObject() @ValidateNested() @Type(() =>MessageContent) content: MessageContent;
@IsString() // Consider using @IsDateString() if format is consistently ISO8601receivedAt: string;
@IsOptional() @IsObject() @ValidateNested() @Type(() =>MessagePrice) price?: MessagePrice;
@IsOptional() @IsObject() @ValidateNested() @Type(() =>MessageStatus) status?: MessageStatus;
@IsOptional() @IsObject() @ValidateNested() @Type(() =>MessageContext) context?: MessageContext;
// Add other potential properties like 'callbackData'
}
exportclassIncomingWhatsappDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() =>MessageResult)
results: MessageResult[];
// These counts are often strings in webhook payloads, verify with Infobip docs@IsOptional() @IsString() messageCount?: string;
@IsOptional() @IsString() pendingMessageCount?: string;
}
Why this DTO? It validates the structure of incoming data from Infobip, ensuring your handler receives correctly formatted information. @ValidateNested and @Type are crucial for validating nested objects and arrays. Using @IsDateString() for receivedAt is safer if you know the format is ISO8601.
Implement the Webhook Handler in Controller:
Add a new POST endpoint in WhatsappController to receive webhook events.
TypeScript
// src/whatsapp/whatsapp.controller.tsimport { Controller, Post, Body, Logger, HttpCode, HttpStatus, Req, ForbiddenException } from'@nestjs/common'; // Added Req, ForbiddenExceptionimport { WhatsappService } from'./whatsapp.service';
import { SendMessageDto } from'./dto/send-message.dto';
import { IncomingWhatsappDto } from'./dto/incoming-message.dto'; // Import the DTOimport { Request } from'express'; // Import Request type if using Express@Controller('whatsapp')
exportclassWhatsappController {
privatereadonly logger = newLogger(WhatsappController.name);
constructor(privatereadonlywhatsappService: WhatsappService) {}
// ... (sendMessage endpoint) ...@Post('webhook') // Route: POST /whatsapp/webhook@HttpCode(HttpStatus.OK) // Acknowledge receipt immediatelyhandleIncomingMessage(@Body() incomingDto: IncomingWhatsappDto, @Req() req: Request) { // Inject Request objectthis.logger.log(`Webhook received: ${JSON.stringify(incomingDto)}`);
// TODO: IMPORTANT SECURITY STEP - Implement signature verification here// Use the secret/token from Infobip portal to verify the request authenticity.// This requires access to the raw request body. Configure NestJS for raw body parsing if needed.// Example conceptual check:// const signature = req.headers['x-infobip-signature'] as string; // Get signature header (actual name may vary)// const rawBody = (req as any).rawBody; // Access raw body (requires middleware setup)// if (!rawBody || !this.whatsappService.verifyWebhookSignature(rawBody, signature)) {// this.logger.warn('Invalid webhook signature attempt.');// throw new ForbiddenException('Invalid webhook signature');// }// this.logger.log('Webhook signature verified successfully.');// Hand off processing to the service to keep the controller thinthis.whatsappService.processIncomingMessages(incomingDto.results);
// Return 200 OK quickly to Infobip to prevent retriesreturn { message: 'Webhook received successfully.' };
}
}
Why HttpStatus.OK? Webhook providers like Infobip expect a quick 2xx response to acknowledge receipt. Long processing should happen asynchronously.
Why hand off to service? Keeps the controller focused on request/response handling and validation. Business logic belongs in the service.
Webhook Security: Implementing signature verification (as noted in the TODO comment) is essential to prevent malicious actors from sending fake data to your endpoint. This typically requires accessing the raw request body before it's parsed by NestJS, which might involve adding middleware (e.g., bodyParser.raw({ type: 'application/json' })) in main.ts.
Implement Processing Logic in Service:
Add a method in WhatsappService to handle the incoming message data. You'll also need to add the verification logic stubbed in the controller if you implement it.
TypeScript
// src/whatsapp/whatsapp.service.tsimport { Injectable, Logger, InternalServerErrorException, BadRequestException } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import { Infobip, AuthType } from'@infobip-api/sdk';
import { IncomingWhatsappDto } from'./dto/incoming-message.dto'; // Import the DTO typeimport * as crypto from'crypto'; // Import crypto for verification// Define the type for a single message result based on DTO for claritytypeIncomingMessageResult = IncomingWhatsappDto['results'][0];
@Injectable()
exportclassWhatsappService {
privatereadonly logger = newLogger(WhatsappService.name);
privateinfobipClient: Infobip;
privatewhatsappSender: string;
constructor(privateconfigService: ConfigService) {
// ... (constructor logic) ...
}
// ... (sendTextMessage method) ...// Conceptual method for webhook signature verification (implement based on Infobip docs)verifyWebhookSignature(rawBody: Buffer, signatureHeader: string): boolean {
const secret = this.configService.get<string>('INFOBIP_WEBHOOK_SECRET');
if (!secret || !signatureHeader || !rawBody) {
this.logger.warn('Webhook signature verification failed: Missing secret, signature, or body.');
returnfalse;
}
// Implement actual HMAC SHA comparison based on Infobip's method// Example (pseudo-code, consult Infobip docs for exact algorithm and header format):// Assuming signatureHeader is just the hex digesttry {
const computedSignature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const receivedSignature = Buffer.from(signatureHeader);
const expectedSignature = Buffer.from(computedSignature);
if (receivedSignature.length !== expectedSignature.length) {
this.logger.warn('Webhook signature length mismatch.');
returnfalse;
}
// Use timingSafeEqual to prevent timing attacksreturn crypto.timingSafeEqual(receivedSignature, expectedSignature);
} catch (error) {
this.logger.error(`Error during webhook signature verification: ${error.message}`, error.stack);
returnfalse;
}
// this.logger.warn('Webhook signature verification logic needs confirmation with Infobip documentation.');// return true; // Placeholder - REMOVE THIS IN PRODUCTION AFTER VERIFYING LOGIC
}
asyncprocessIncomingMessages(messages: IncomingMessageResult[]): Promise<void> {
for (const message of messages) {
try {
this.logger.log(`Processing incoming message from ${message.from}: ""${message.content?.text}"" (ID: ${message.messageId})`);
// Example: Log message details// Optional: Persist incoming message to database// await this.messageRepository.save({ from: message.from, body: message.content?.text, ... });// Example: Implement basic auto-reply logicif (message.content?.text?.toLowerCase().includes('hello')) {
awaitthis.sendTextMessage(message.from, 'Hi there! How can I help?');
}
// Add more complex routing or processing logic here based on message content, context, etc.
} catch (error) {
this.logger.error(`Error processing incoming message ${message.messageId}: ${error.message}`, error.stack);
// Implement error tracking or alerting here
}
}
}
}
Why process asynchronously (conceptually)? For high volume, you'd typically push incoming messages onto a queue (like BullMQ) and process them with background workers to avoid blocking the webhook response and handle load spikes. For this guide, we process directly but acknowledge the pattern.
Webhook Secret: Ensure you add INFOBIP_WEBHOOK_SECRET to your .env file and retrieve it via ConfigService when implementing signature verification.
Testing the Webhook:
Ensure your application and ngrok tunnel are running.
Send a WhatsApp message from your personal phone to your registered Infobip WhatsApp sender number.
Check your application logs. You should see the ""Webhook received"" log in the controller and the ""Processing incoming message"" log in the service. If you implemented the auto-reply, you should receive it back on your phone.
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are crucial for production.
Error Handling Strategy:
Validation Errors: Handled by ValidationPipe -> 400 Bad Request.
Service Logic Errors: Use specific BadRequestException, NotFoundException, etc., where applicable within the service.
Infobip API Errors: Catch errors during SDK calls (try...catch). Log detailed error information (including error.response?.data if available). Throw InternalServerErrorException or a more specific custom exception if you can map Infobip errors.
Webhook Signature Errors: Throw ForbiddenException (403) if signature verification fails.
Unhandled Errors: NestJS's default exception filter catches unhandled errors and returns a generic 500 Internal Server Error. Customize this filter for production logging/reporting.
Logging:
Use NestJS's built-in Logger. Inject it where needed (private readonly logger = new Logger(ClassName.name)).
Log Levels:
log: General information (request received, action started, webhook received).
warn: Potential issues (invalid format detected but handled, retry attempt, signature failure).
debug: Detailed information for troubleshooting (full API responses/payloads) – disable in production unless needed.
Production Logging: Consider using a more advanced library like pino or winston for structured logging (JSON format), log rotation, and transport to external logging services (e.g., Datadog, ELK Stack). Configure log levels via environment variables.
Retry Mechanisms (Conceptual):
Outgoing Messages: If an Infobip API call fails due to transient network issues or temporary Infobip unavailability (e.g., 429, 5xx errors), implement a retry strategy.
Simple: A for loop with delays (shown below - use with caution).
Better: Use libraries like async-retry or integrate with a queue system (e.g., BullMQ) that has built-in retry logic with exponential backoff. Queues are strongly recommended for production reliability.
Webhook Processing: If processing an incoming message fails temporarily (e.g., database connection issue), a queue system with retries is the best approach. Infobip may retry sending the webhook if it doesn't receive a 2xx response quickly, but relying on this isn't ideal for application-level processing failures.
TypeScript
// Example: Conceptual retry in service (simple version - prefer queues for production)asyncsendTextMessageWithRetry(to: string, text: string, retries = 3): Promise<any> {
for (let i = 0; i < retries; i++) {
try {
// Note: This calls the original sendTextMessage which includes its own try/catch.// You might refactor to have a core sending logic method called by both.returnawaitthis.sendTextMessage(to, text);
} catch (error) {
// WARNING: This is a basic check. Refine based on specific Infobip error codes or types.// Check for specific status codes (e.g., 429, 500, 502, 503, 504) or network error types.const isRetryable = error instanceofInternalServerErrorException || (error.response && [429, 500, 502, 503, 504].includes(error.response.status)); // Example refinementif (isRetryable && i < retries - 1) {
const delay = Math.pow(2, i) * 1000; // Exponential backoff (1s, 2s, 4s...)this.logger.warn(`Retrying message send to ${to} (attempt ${i + 1}/${retries}) after ${delay}ms delay. Error: ${error.message}`);
awaitnewPromise(resolve =>setTimeout(resolve, delay)); // Wait before retrying
} else {
this.logger.error(`Final attempt failed or error not retryable for message to ${to}. Error: ${error.message}`);
throw error; // Re-throw the error after final attempt or if not retryable
}
}
}
}
Frequently asked questions
Integrate Infobip's WhatsApp API into your NestJS project by installing the necessary dependencies like '@infobip-api/sdk', '@nestjs/config', 'dotenv', 'class-validator', and 'class-transformer'. Then, set up environment variables for your Infobip credentials and configure the 'ConfigModule' to load them. Finally, create a WhatsApp feature module with a controller and service to handle message sending and receiving.
The Infobip WhatsApp API is a communication platform service that allows you to send and receive WhatsApp messages programmatically. This guide uses the Infobip Node.js SDK to simplify interaction with the API, enabling features like sending text messages and receiving incoming messages via webhooks.
NestJS provides a robust and structured framework for building scalable server-side applications. Its features like dependency injection, validation pipes, and modular architecture accelerate development and improve code maintainability, making it ideal for integrating with external APIs like Infobip's WhatsApp API.
Data Transfer Objects (DTOs) are essential for defining the expected structure and validation rules for incoming requests. They improve code clarity and security by ensuring data integrity. Use DTOs with NestJS's 'ValidationPipe' for automatic request validation.
To send a WhatsApp message, create a service method that uses the Infobip SDK. Inject the 'ConfigService' to access your API credentials, initialize the Infobip client, and use the 'channels.whatsapp.send' method with a properly formatted payload. Expose this functionality through a controller endpoint, validating input using a DTO.
The '@infobip-api/sdk' is the official Infobip Node.js SDK. It simplifies interaction with the Infobip API by providing convenient methods for sending messages, managing webhooks, and other functionalities. It streamlines the integration process compared to making direct HTTP requests.
To set up a webhook, configure the webhook URL in the Infobip portal to point to your NestJS application's endpoint. For local development, use a tool like 'ngrok'. Create a DTO to define the structure of incoming messages and a controller endpoint to handle webhook requests. Importantly, implement security measures like signature verification using a secret key from Infobip to authenticate webhook requests.
The '@nestjs/config' module provides a structured and secure approach to manage environment-specific configurations, such as API keys, database credentials, and other sensitive data. It prevents hardcoding such values in your application code, making it easy to switch configurations between development, testing, and production environments.
Yes, you can test webhooks locally by using a tunneling tool like 'ngrok'. 'ngrok' creates a temporary public URL that forwards requests to your local development server. Configure this URL as your webhook URL in the Infobip portal, allowing you to receive webhook requests even during local development.
Handle webhook errors by implementing thorough error handling in your webhook handler method. Use try-catch blocks to catch potential errors during message processing, including signature verification failures, and log detailed error information. Consider using a queue system for asynchronous processing and retries to handle transient failures and load spikes.
'class-validator' and 'class-transformer' work together for request validation and transformation in NestJS. 'class-validator' provides decorators like '@IsNotEmpty', '@IsString', and '@IsPhoneNumber' to define validation rules for DTO properties, and class-transformer' handles the actual transformation of the request body into a DTO instance.
Secure your webhook endpoint by implementing signature verification. Obtain a webhook secret from your Infobip portal and use it to compute an HMAC-SHA256 hash of the incoming request body. Compare the computed hash with the signature provided in the request header ('x-infobip-signature' or similar) to ensure the request originated from Infobip.
Manage environment variables securely using the '@nestjs/config' module along with 'dotenv'. Create a '.env' file to store your environment-specific variables, and configure the 'ConfigModule' in your main application module. Never commit your '.env' file to version control to prevent exposing sensitive data.
This guide provides a comprehensive, step-by-step walkthrough for integrating Infobip's WhatsApp API into a NestJS application using Node.js. We'll cover everything from initial project setup and sending messages to handling incoming webhooks, error management, security, and deployment best practices. By the end, you'll have a robust foundation for production-level WhatsApp communication.
We assume minimal prior knowledge beyond basic NestJS and Node.js concepts, explaining each step and decision clearly. We prioritize practical, real-world implementation details to ensure your integration is reliable and scalable.
Project Overview and Goals
What We're Building:
We will build a NestJS application capable of:
Problem Solved:
This integration enables applications to leverage WhatsApp for various communication needs, such as notifications, customer support interactions, OTP delivery, or marketing messages (respecting WhatsApp policies), automating processes previously handled manually or through less direct channels.
Technologies Used:
@infobip-api/sdk: The official Infobip Node.js SDK, simplifying interactions with the Infobip API.@nestjs/config: For managing environment variables securely.class-validator&class-transformer: For robust request validation using Data Transfer Objects (DTOs).System Architecture:
Prerequisites:
npm install -g @nestjs/cli).ngrokor similar: For testing webhooks locally.curl: For testing API endpoints.1. Setting up the Project
Let's initialize our NestJS project and install necessary dependencies.
Create a New NestJS Project: Open your terminal and run:
nest new infobip-whatsapp-app cd infobip-whatsapp-appChoose your preferred package manager (npm or yarn) when prompted.
Install Dependencies: We need the Infobip SDK and NestJS configuration module.
# Using npm npm install @infobip-api/sdk @nestjs/config dotenv class-validator class-transformer # Using yarn yarn add @infobip-api/sdk @nestjs/config dotenv class-validator class-transformer@infobip-api/sdk: The core library for interacting with Infobip.@nestjs/config: Manages environment variables.dotenv: Loads environment variables from a.envfile for local development.class-validator&class-transformer: Enable validation using DTOs.Configure Environment Variables:
.envfile in the project root:touch .env.env. Never commit this file to version control.#.env # Infobip Credentials INFOBIP_BASE_URL=your_infobip_base_url.api.infobip.com INFOBIP_API_KEY=YourInfobipApiKeyGoesHere INFOBIP_WHATSAPP_SENDER=YourInfobipWhatsAppSenderNumber # e.g., 447860099299 # Application Port (Optional) PORT=3000 # Webhook Secret (Add if implementing signature verification) # INFOBIP_WEBHOOK_SECRET=YourInfobipWebhookSecretINFOBIP_BASE_URL&INFOBIP_API_KEY: Log in to your Infobip Portal. Navigate to the homepage or API Keys management section. Your unique Base URL and API Key will be displayed.INFOBIP_WHATSAPP_SENDER: Navigate to Channels and Numbers -> WhatsApp -> Numbers. Copy the registered sender number you intend to use. Ensure it's approved and active.Load Environment Variables using
ConfigModule: Import and configureConfigModulein your main application module (src/app.module.ts). Making it global simplifies access in other modules.// 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 { WhatsappModule } from './whatsapp/whatsapp.module'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Makes ConfigService available globally envFilePath: '.env', // Specifies the env file path }), WhatsappModule, // Import the feature module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}ConfigModule? It provides a structured and secure way to handle environment-specific configurations, preventing hardcoding sensitive data like API keys directly in the code.isGlobal: trueavoids needing to importConfigModuleinto every feature module that requires access to environment variables.Enable Validation Pipe Globally: Configure the
ValidationPipeinsrc/main.tsto automatically validate incoming request payloads against DTOs.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe import { ConfigService } from '@nestjs/config'; // Import ConfigService async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); // Get ConfigService instance // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties that do not have any decorators forbidNonWhitelisted: true, // Throw error if non-whitelisted values are provided transform: true, // Automatically transform payloads to DTO instances })); const port = configService.get<number>('PORT') || 3000; // Get port from env await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap();Create the WhatsApp Feature Module: Generate a module, controller, and service for our WhatsApp functionality using the NestJS CLI.
nest generate module whatsapp nest generate controller whatsapp --no-spec # Optional: skip test file nest generate service whatsapp --no-spec # Optional: skip test fileThis creates a
src/whatsappdirectory withwhatsapp.module.ts,whatsapp.controller.ts, andwhatsapp.service.ts. The CLI automatically updatessrc/app.module.tsto importWhatsappModule.2. Implementing Core Functionality (Sending Messages)
We'll implement the logic to send a WhatsApp message via the Infobip SDK within our
WhatsappService.Initialize Infobip Client in the Service: Inject
ConfigServiceintoWhatsappServiceand initialize the Infobip client in the constructor.// src/whatsapp/whatsapp.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; // Import Infobip SDK elements @Injectable() export class WhatsappService { private readonly logger = new Logger(WhatsappService.name); private infobipClient: Infobip; private whatsappSender: string; constructor(private configService: ConfigService) { const apiKey = this.configService.get<string>('INFOBIP_API_KEY'); const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL'); this.whatsappSender = this.configService.get<string>('INFOBIP_WHATSAPP_SENDER'); if (!apiKey || !baseUrl || !this.whatsappSender) { this.logger.error('Infobip API Key, Base URL, or Sender Number not configured in environment variables.'); throw new InternalServerErrorException('WhatsApp Service is not configured correctly.'); } this.infobipClient = new Infobip({ baseUrl: baseUrl, apiKey: apiKey, authType: AuthType.ApiKey, // Specify API Key authentication }); this.logger.log('Infobip client initialized successfully.'); } // Method to send a text message will go here }Implement the
sendTextMessageMethod: Add a method to handle sending the message payload.// src/whatsapp/whatsapp.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; @Injectable() export class WhatsappService { private readonly logger = new Logger(WhatsappService.name); private infobipClient: Infobip; private whatsappSender: string; constructor(private configService: ConfigService) { // ... (constructor logic as above) ... } async sendTextMessage(to: string, text: string): Promise<any> { this.logger.log(`Attempting to send message to ${to}`); // Basic validation (more robust validation happens at controller level via DTOs) if (!to || !text) { throw new BadRequestException('Recipient number (to) and message text cannot be empty.'); } // Basic check for phone number format. Infobip usually expects international format without '+'. // WARNING: This regex is very basic. For production, use a robust library like 'libphonenumber-js' // or `class-validator`'s `@IsPhoneNumber(region)` in the DTO for proper validation. if (!/^\d{10,15}$/.test(to)) { this.logger.warn(`Potential invalid phone number format for recipient: ${to}. Expected format like 447123456789 (international without '+').`); // Depending on requirements, you might throw BadRequestException here: // throw new BadRequestException('Invalid recipient phone number format. Use international format without ""+"".'); } const payload = { type: 'text', // Specify message type as text from: this.whatsappSender, to: to, // Recipient number (validated format is crucial) content: { text: text, // The message content }, }; try { const response = await this.infobipClient.channels.whatsapp.send(payload); this.logger.log(`Message sent successfully to ${to}. Message ID: ${response.data.messages[0]?.messageId}`); // Consider logging the full response for detailed tracking if needed // this.logger.debug(`Infobip API Response: ${JSON.stringify(response.data)}`); // Optional: Persist message status to database here // await this.messageRepository.save({ ... }); return response.data; // Return the relevant part of the Infobip response } catch (error) { this.logger.error(`Failed to send message to ${to}: ${error.message}`, error.stack); // Enhance error handling based on Infobip error structure if available // Example: Check error.response?.data for specific API error codes/messages if (error.response?.data) { this.logger.error(`Infobip API Error Details: ${JSON.stringify(error.response.data)}`); } // Re-throw a more specific HTTP exception if possible, or a generic one throw new InternalServerErrorException(`Failed to send WhatsApp message: ${error.message}`); } } // Method to handle incoming messages will go here }try...catchblock handles potential API errors gracefully, logs them, and throws appropriate NestJS HTTP exceptions. Basic input validation is added as a safety measure, though the primary validation occurs via DTOs in the controller. We return theresponse.datawhich contains useful information likemessageId.3. Building the API Layer (Sending Messages)
Now, let's expose an endpoint in
WhatsappControllerto trigger thesendTextMessagemethod.Create a Data Transfer Object (DTO): Define a DTO to specify the expected shape and validation rules for the request body.
// src/whatsapp/dto/send-message.dto.ts import { IsNotEmpty, IsString, Length, IsPhoneNumber /* Recommended */ } from 'class-validator'; export class SendMessageDto { @IsNotEmpty() @IsString() // Basic length validation. For production, strongly consider using: // @IsPhoneNumber(null) // or specify region e.g. @IsPhoneNumber('GB') // Ensure the format matches Infobip expectations (usually international without '+', e.g., 447123456789) @Length(10, 15) // Adjust length constraints as needed based on expected format to: string; @IsNotEmpty() @IsString() @Length(1, 4096) // WhatsApp text message limit is 4,096 characters (Meta WhatsApp Cloud API) text: string; }ValidationPipe, provide automatic request validation, improving security and data integrity. Using@IsPhoneNumberis preferred for robust validation.Implement the Controller Endpoint: Inject
WhatsappServiceand create a POST endpoint.// src/whatsapp/whatsapp.controller.ts import { Controller, Post, Body, Logger, HttpCode, HttpStatus } from '@nestjs/common'; import { WhatsappService } from './whatsapp.service'; import { SendMessageDto } from './dto/send-message.dto'; // Import the DTO @Controller('whatsapp') // Route prefix for this controller export class WhatsappController { private readonly logger = new Logger(WhatsappController.name); constructor(private readonly whatsappService: WhatsappService) {} @Post('send') // Route: POST /whatsapp/send @HttpCode(HttpStatus.ACCEPTED) // Indicate request accepted, processing initiated async sendMessage(@Body() sendMessageDto: SendMessageDto) { this.logger.log(`Received request to send message: ${JSON.stringify(sendMessageDto)}`); try { // The 'to' number format should align with expectations set in the service/DTO comments const result = await this.whatsappService.sendTextMessage( sendMessageDto.to, sendMessageDto.text, ); // Return a minimal success response, including the message ID might be useful return { message: 'Message accepted for delivery.', infobipResponse: { bulkId: result.bulkId, messageId: result.messages[0]?.messageId, status: result.messages[0]?.status // Example status from response } }; } catch (error) { this.logger.error(`Error in sendMessage endpoint: ${error.message}`, error.stack); // Exceptions thrown from the service (like BadRequestException, InternalServerErrorException) // will be automatically handled by NestJS's exception filter. throw error; // Re-throw the error for the default exception handler } } // Webhook endpoint will go here }@Body() sendMessageDto: SendMessageDto? This tells NestJS to parse the request body and validate it against theSendMessageDtousing the globally configuredValidationPipe.HttpStatus.ACCEPTED? Sending a message is often asynchronous. Returning 202 Accepted signifies the request is valid and processing has started, but doesn't guarantee immediate delivery.BadRequestException, 500 forInternalServerErrorException).Testing the Endpoint: Start your application:
npm run start:devUsecurlor Postman to send a POST request:curl -X POST http://localhost:3000/whatsapp/send \ -H 'Content-Type: application/json' \ -d '{ ""to"": ""YOUR_REGISTERED_TEST_NUMBER"", ""text"": ""Hello from NestJS and Infobip!"" }'Replace
YOUR_REGISTERED_TEST_NUMBERwith your registered test number (e.g.,447123456789). You should receive a202 Acceptedresponse and the message on your WhatsApp (if using your registered trial number). Check the application logs and the Infobip portal for status updates.4. Integrating with Third-Party Services (Infobip Webhook)
To receive messages sent to your Infobip WhatsApp number, you need to configure a webhook.
Configure Webhook in Infobip Portal:
ngrokto expose your local port:ngrok http 3000(if your app runs on port 3000).https://<your-ngrok-id>.ngrok.ioURL.https://<your-ngrok-id>.ngrok.io/whatsapp/webhook(we'll create/whatsapp/webhooknext)..envfile (e.g.,INFOBIP_WEBHOOK_SECRET).Create DTO for Incoming Webhook Payload: Define a DTO based on the expected structure of Infobip's incoming WhatsApp message webhook. Note: This structure might vary slightly; always refer to the official Infobip documentation for the most accurate payload format.
// src/whatsapp/dto/incoming-message.dto.ts import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested, IsDateString } from 'class-validator'; // Define nested structures based on Infobip's documentation class MessageContent { @IsString() text: string; // Add other potential content types like 'imageUrl', 'documentUrl', etc. if needed } class MessagePrice { // Define price properties if needed (check Infobip docs) } class MessageStatus { // Define status properties if needed (check Infobip docs) } class MessageContext { // Define context properties if needed (e.g., for replies) @IsString() @IsOptional() from?: string; @IsString() @IsOptional() id?: string; } class MessageResult { @IsString() from: string; @IsString() to: string; @IsString() @IsOptional() integrationType?: string; @IsString() messageId: string; @IsObject() @ValidateNested() @Type(() => MessageContent) content: MessageContent; @IsString() // Consider using @IsDateString() if format is consistently ISO8601 receivedAt: string; @IsOptional() @IsObject() @ValidateNested() @Type(() => MessagePrice) price?: MessagePrice; @IsOptional() @IsObject() @ValidateNested() @Type(() => MessageStatus) status?: MessageStatus; @IsOptional() @IsObject() @ValidateNested() @Type(() => MessageContext) context?: MessageContext; // Add other potential properties like 'callbackData' } export class IncomingWhatsappDto { @IsArray() @ValidateNested({ each: true }) @Type(() => MessageResult) results: MessageResult[]; // These counts are often strings in webhook payloads, verify with Infobip docs @IsOptional() @IsString() messageCount?: string; @IsOptional() @IsString() pendingMessageCount?: string; }@ValidateNestedand@Typeare crucial for validating nested objects and arrays. Using@IsDateString()forreceivedAtis safer if you know the format is ISO8601.Implement the Webhook Handler in Controller: Add a new POST endpoint in
WhatsappControllerto receive webhook events.// src/whatsapp/whatsapp.controller.ts import { Controller, Post, Body, Logger, HttpCode, HttpStatus, Req, ForbiddenException } from '@nestjs/common'; // Added Req, ForbiddenException import { WhatsappService } from './whatsapp.service'; import { SendMessageDto } from './dto/send-message.dto'; import { IncomingWhatsappDto } from './dto/incoming-message.dto'; // Import the DTO import { Request } from 'express'; // Import Request type if using Express @Controller('whatsapp') export class WhatsappController { private readonly logger = new Logger(WhatsappController.name); constructor(private readonly whatsappService: WhatsappService) {} // ... (sendMessage endpoint) ... @Post('webhook') // Route: POST /whatsapp/webhook @HttpCode(HttpStatus.OK) // Acknowledge receipt immediately handleIncomingMessage(@Body() incomingDto: IncomingWhatsappDto, @Req() req: Request) { // Inject Request object this.logger.log(`Webhook received: ${JSON.stringify(incomingDto)}`); // TODO: IMPORTANT SECURITY STEP - Implement signature verification here // Use the secret/token from Infobip portal to verify the request authenticity. // This requires access to the raw request body. Configure NestJS for raw body parsing if needed. // Example conceptual check: // const signature = req.headers['x-infobip-signature'] as string; // Get signature header (actual name may vary) // const rawBody = (req as any).rawBody; // Access raw body (requires middleware setup) // if (!rawBody || !this.whatsappService.verifyWebhookSignature(rawBody, signature)) { // this.logger.warn('Invalid webhook signature attempt.'); // throw new ForbiddenException('Invalid webhook signature'); // } // this.logger.log('Webhook signature verified successfully.'); // Hand off processing to the service to keep the controller thin this.whatsappService.processIncomingMessages(incomingDto.results); // Return 200 OK quickly to Infobip to prevent retries return { message: 'Webhook received successfully.' }; } }HttpStatus.OK? Webhook providers like Infobip expect a quick 2xx response to acknowledge receipt. Long processing should happen asynchronously.bodyParser.raw({ type: 'application/json' })) inmain.ts.Implement Processing Logic in Service: Add a method in
WhatsappServiceto handle the incoming message data. You'll also need to add the verification logic stubbed in the controller if you implement it.// src/whatsapp/whatsapp.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; import { IncomingWhatsappDto } from './dto/incoming-message.dto'; // Import the DTO type import * as crypto from 'crypto'; // Import crypto for verification // Define the type for a single message result based on DTO for clarity type IncomingMessageResult = IncomingWhatsappDto['results'][0]; @Injectable() export class WhatsappService { private readonly logger = new Logger(WhatsappService.name); private infobipClient: Infobip; private whatsappSender: string; constructor(private configService: ConfigService) { // ... (constructor logic) ... } // ... (sendTextMessage method) ... // Conceptual method for webhook signature verification (implement based on Infobip docs) verifyWebhookSignature(rawBody: Buffer, signatureHeader: string): boolean { const secret = this.configService.get<string>('INFOBIP_WEBHOOK_SECRET'); if (!secret || !signatureHeader || !rawBody) { this.logger.warn('Webhook signature verification failed: Missing secret, signature, or body.'); return false; } // Implement actual HMAC SHA comparison based on Infobip's method // Example (pseudo-code, consult Infobip docs for exact algorithm and header format): // Assuming signatureHeader is just the hex digest try { const computedSignature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const receivedSignature = Buffer.from(signatureHeader); const expectedSignature = Buffer.from(computedSignature); if (receivedSignature.length !== expectedSignature.length) { this.logger.warn('Webhook signature length mismatch.'); return false; } // Use timingSafeEqual to prevent timing attacks return crypto.timingSafeEqual(receivedSignature, expectedSignature); } catch (error) { this.logger.error(`Error during webhook signature verification: ${error.message}`, error.stack); return false; } // this.logger.warn('Webhook signature verification logic needs confirmation with Infobip documentation.'); // return true; // Placeholder - REMOVE THIS IN PRODUCTION AFTER VERIFYING LOGIC } async processIncomingMessages(messages: IncomingMessageResult[]): Promise<void> { for (const message of messages) { try { this.logger.log(`Processing incoming message from ${message.from}: ""${message.content?.text}"" (ID: ${message.messageId})`); // Example: Log message details // Optional: Persist incoming message to database // await this.messageRepository.save({ from: message.from, body: message.content?.text, ... }); // Example: Implement basic auto-reply logic if (message.content?.text?.toLowerCase().includes('hello')) { await this.sendTextMessage(message.from, 'Hi there! How can I help?'); } // Add more complex routing or processing logic here based on message content, context, etc. } catch (error) { this.logger.error(`Error processing incoming message ${message.messageId}: ${error.message}`, error.stack); // Implement error tracking or alerting here } } } }INFOBIP_WEBHOOK_SECRETto your.envfile and retrieve it viaConfigServicewhen implementing signature verification.Testing the Webhook:
ngroktunnel are running.5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are crucial for production.
Error Handling Strategy:
ValidationPipe-> 400 Bad Request.BadRequestException,NotFoundException, etc., where applicable within the service.try...catch). Log detailed error information (includingerror.response?.dataif available). ThrowInternalServerErrorExceptionor a more specific custom exception if you can map Infobip errors.ForbiddenException(403) if signature verification fails.Logging:
Logger. Inject it where needed (private readonly logger = new Logger(ClassName.name)).log: General information (request received, action started, webhook received).warn: Potential issues (invalid format detected but handled, retry attempt, signature failure).error: Failures (API call failed, processing error, configuration missing). Include stack traces.debug: Detailed information for troubleshooting (full API responses/payloads) – disable in production unless needed.pinoorwinstonfor structured logging (JSON format), log rotation, and transport to external logging services (e.g., Datadog, ELK Stack). Configure log levels via environment variables.Retry Mechanisms (Conceptual):
forloop with delays (shown below - use with caution).async-retryor integrate with a queue system (e.g., BullMQ) that has built-in retry logic with exponential backoff. Queues are strongly recommended for production reliability.// Example: Conceptual retry in service (simple version - prefer queues for production) async sendTextMessageWithRetry(to: string, text: string, retries = 3): Promise<any> { for (let i = 0; i < retries; i++) { try { // Note: This calls the original sendTextMessage which includes its own try/catch. // You might refactor to have a core sending logic method called by both. return await this.sendTextMessage(to, text); } catch (error) { // WARNING: This is a basic check. Refine based on specific Infobip error codes or types. // Check for specific status codes (e.g., 429, 500, 502, 503, 504) or network error types. const isRetryable = error instanceof InternalServerErrorException || (error.response && [429, 500, 502, 503, 504].includes(error.response.status)); // Example refinement if (isRetryable && i < retries - 1) { const delay = Math.pow(2, i) * 1000; // Exponential backoff (1s, 2s, 4s...) this.logger.warn(`Retrying message send to ${to} (attempt ${i + 1}/${retries}) after ${delay}ms delay. Error: ${error.message}`); await new Promise(resolve => setTimeout(resolve, delay)); // Wait before retrying } else { this.logger.error(`Final attempt failed or error not retryable for message to ${to}. Error: ${error.message}`); throw error; // Re-throw the error after final attempt or if not retryable } } } }