code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

MessageBird WhatsApp API Integration with NestJS: Complete Tutorial (2025)

A guide on setting up a NestJS application to send and receive WhatsApp messages via the MessageBird Conversations API, including project setup, sending messages, and handling webhooks.

MessageBird WhatsApp Integration with Node.js and NestJS: Complete Tutorial

Build a production-ready WhatsApp Business API integration for your Node.js application using NestJS and the MessageBird Conversations API. This complete tutorial covers project setup, webhook handling, sending messages (text and media), implementing secure authentication, and adapting to 2025 WhatsApp Business API changes.

Important Note (2025): MessageBird rebranded to Bird.com in February 2024. However, the legacy APIs, SDKs (including the Node.js messagebird package), and developer documentation still use the MessageBird branding. This guide uses "MessageBird" when referring to the SDK and APIs, but the company now operates as Bird.com.

By completing this NestJS WhatsApp integration tutorial, you'll have a functional application that sends and receives WhatsApp messages programmatically – perfect for building chatbots, automated notification systems, or customer support tools. You'll need basic familiarity with Node.js, TypeScript, and NestJS concepts.

What You'll Build with This WhatsApp API Integration

Create a NestJS backend service that:

  1. Exposes an API endpoint to send outbound WhatsApp messages (text and media)
  2. Receives inbound WhatsApp messages and status updates via MessageBird webhooks
  3. Securely handles API keys and webhook requests
  4. (Optional) Persists message history in a database

Why This Matters:

Leverage WhatsApp's ubiquity to communicate with users in their preferred messaging app. Automate interactions, send timely notifications, and provide support without requiring users to install a separate application.

Technologies:

  • Node.js: JavaScript runtime environment
  • NestJS: Progressive Node.js framework for building efficient, scalable server-side applications with TypeScript. Features modular architecture, dependency injection, and built-in validation and configuration support
  • TypeScript: JavaScript superset adding static types for improved code quality and maintainability
  • MessageBird (now Bird.com): Cloud communications platform with APIs for multiple channels, including WhatsApp. You'll use their official Node.js SDK and Conversations API
  • MessageBird WhatsApp Sandbox: Development environment for testing WhatsApp integration without a fully approved WhatsApp Business Account
  • (Optional) Prisma: Next-generation ORM for Node.js and TypeScript, used for database interaction if you need message persistence
  • (Optional) PostgreSQL/SQLite: Relational database for storing message data

System Architecture:

(A sequence diagram illustrating the flow between Client, NestJS App, MessageBird API, and WhatsApp was intended here.)

Prerequisites:

  • Node.js LTS version (v18.x, v20.x, or v22.x as of 2025) and npm/yarn. Note: NestJS 10 and 11 require Node.js v16 or higher
  • MessageBird Account (sign up at https://messagebird.com/ or https://bird.com/)
  • Access to the MessageBird WhatsApp Sandbox OR a provisioned WhatsApp Business channel
  • API testing tool (Postman or curl)
  • (Optional) Database instance (e.g., PostgreSQL) for persistence
  • (Optional) ngrok or similar tool to expose your local development server for webhook testing

SDK Version Notice: The messagebird npm package (v4.0.1) was last updated approximately 3 years ago (as of 2025). While functional for basic use cases, it may not receive regular updates or support for newer WhatsApp Business API features. Monitor the official Bird.com developer documentation for migration paths or new SDK releases.

1. Setting Up Your NestJS WhatsApp Project

Initialize your NestJS project and install the required dependencies for WhatsApp API integration.

Install NestJS CLI:

If you haven't installed it globally:

bash
npm install -g @nestjs/cli
# or
yarn global add @nestjs/cli

Create Your NestJS Project:

bash
nest new nestjs-whatsapp-messagebird
cd nestjs-whatsapp-messagebird

Choose your preferred package manager (npm or yarn) when prompted.

Install Required Dependencies:

Install the MessageBird SDK and NestJS configuration module:

bash
npm install messagebird @nestjs/config class-validator class-transformer
# or
yarn add messagebird @nestjs/config class-validator class-transformer
  • messagebird: Official Node.js SDK for the MessageBird API
  • @nestjs/config: Securely manages environment variables
  • class-validator & class-transformer: Validate incoming request data (DTOs)

Configure Environment Variables:

Create a .env file in your project root to store sensitive credentials. Never commit this file to version control. Ensure .env is in your .gitignore.

dotenv
# .env

# MessageBird Credentials
MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_OR_TEST_API_KEY
MESSAGEBIRD_WHATSAPP_CHANNEL_ID=YOUR_WHATSAPP_CHANNEL_ID_FROM_MESSAGEBIRD

# Webhook Security
MESSAGEBIRD_WEBHOOK_SIGNING_KEY=YOUR_GENERATED_WEBHOOK_SIGNING_KEY
WEBHOOK_BASE_URL=http://localhost:3000 # Replace with ngrok URL or deployed URL

Obtain Your MessageBird API Credentials:

  1. MESSAGEBIRD_API_KEY:

    • Log in to your MessageBird Dashboard (accessible via https://dashboard.messagebird.com/ or through Bird.com)
    • Navigate to Developers > API access
    • Click "Add access key." Choose "Live" or "Test" mode. Copy the generated key. Note: Test keys work with the Sandbox but won't send real messages
  2. MESSAGEBIRD_WHATSAPP_CHANNEL_ID:

    • Navigate to Channels > WhatsApp
    • For Sandbox: Find the "WhatsApp Sandbox" entry. The Channel ID is listed there
    • For provisioned channel: Find your channel name. The Channel ID is listed there
  3. MESSAGEBIRD_WEBHOOK_SIGNING_KEY:

    • Navigate to Developers > Webhook Signing
    • Click "Generate new signing key." Copy the generated key
  4. WEBHOOK_BASE_URL: Set the public URL where MessageBird sends webhook events. For local development, use ngrok:

    • Install ngrok: https://ngrok.com/download
    • Run: ngrok http 3000 (assuming your NestJS app runs on port 3000)
    • Copy the https forwarding URL from ngrok (e.g., https://abcdef123456.ngrok.io). Use this as your base URL
    • Important: Update the webhook configuration in MessageBird whenever your ngrok URL changes. Ngrok provides temporary URLs for local development and testing only. For staging or production, use a stable, publicly accessible HTTPS endpoint on your server

Load Environment Variables:

Import and configure ConfigModule in your main application module (src/app.module.ts) to make environment variables accessible throughout your application.

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 other modules later (e.g., WhatsappModule, MessageBirdModule, PrismaModule)

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // Make ConfigService available globally
      envFilePath: '.env',
    }),
    // Add other modules here later
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Project Structure:

We'll organize our code into modules for better separation of concerns:

src/ ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── main.ts ├── config/ # Configuration related files (if needed beyond .env) ├── core/ # Core services like MessageBird client wrapper, Prisma │ ├── messagebird/ │ │ ├── messagebird.module.ts │ │ └── messagebird.service.ts │ └── prisma/ # (Optional) If using Prisma │ ├── prisma.module.ts │ └── prisma.service.ts ├── modules/ # Feature modules │ ├── whatsapp/ │ │ ├── dto/ │ │ │ └── send-whatsapp-message.dto.ts │ │ ├── whatsapp.controller.ts │ │ ├── whatsapp.module.ts │ │ └── whatsapp.service.ts │ └── webhooks/ │ ├── dto/ │ │ └── messagebird-webhook.dto.ts # Defines expected payload structure │ ├── webhooks.controller.ts │ └── webhooks.module.ts ├── common/ # Shared utilities, decorators, middleware etc. │ └── middleware/ │ └── messagebird-verify.middleware.ts # For signature verification prisma/ # (Optional) Prisma schema and migrations └── schema.prisma # ... other configuration files (.env, .gitignore, tsconfig.json, etc.)

This structure promotes modularity and makes the application easier to maintain and scale.

2. Configure the MessageBird Client Service

Let's create a dedicated service to initialize and provide the MessageBird client instance.

1. Create the Module and Service:

bash
nest generate module core/messagebird --flat
nest generate service core/messagebird --flat

2. Implement the Service:

This service will inject NestJS's ConfigService to retrieve the API key and initialize the messagebird client.

typescript
// src/core/messagebird/messagebird.service.ts
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as MessageBird from 'messagebird'; // Import the SDK

@Injectable()
export class MessageBirdService implements OnModuleInit {
  private readonly logger = new Logger(MessageBirdService.name);
  private client: MessageBird.MessageBird; // Type for the client instance

  constructor(private configService: ConfigService) {}

  onModuleInit() {
    const apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY');
    if (!apiKey) {
      this.logger.error('MESSAGEBIRD_API_KEY is not defined in environment variables.');
      throw new Error('MessageBird API Key is required.');
    }
    // Initialize the client
    this.client = MessageBird(apiKey);
    this.logger.log('MessageBird client initialized successfully.');
  }

  // Method to get the initialized client
  getClient(): MessageBird.MessageBird {
    if (!this.client) {
      // This should ideally not happen due to OnModuleInit, but defensive check
      this.logger.error('MessageBird client requested before initialization.');
      throw new Error('MessageBird client not initialized.');
    }
    return this.client;
  }
}

3. Create the Module:

Export the MessageBirdService so other modules can use it.

typescript
// src/core/messagebird/messagebird.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; // Ensure ConfigModule is available
import { MessageBirdService } from './messagebird.service';

@Global() // Make MessageBirdService available globally without importing MessageBirdModule everywhere
@Module({
  imports: [ConfigModule], // Import ConfigModule if not already global in AppModule
  providers: [MessageBirdService],
  exports: [MessageBirdService],
})
export class MessageBirdModule {}

4. Import MessageBirdModule in AppModule:

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 { MessageBirdModule } from './core/messagebird/messagebird.module'; // Import
// Import other modules later

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    MessageBirdModule, // Add MessageBirdModule
    // Add other modules here later (WhatsappModule, WebhooksModule, PrismaModule)
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Now, the MessageBirdService can be injected into any other service or controller in our application.

3. How to Send WhatsApp Messages with NestJS

We'll create a dedicated module (WhatsappModule) with a controller and service to handle sending WhatsApp messages.

1. Create Module, Controller, Service, and DTO:

bash
nest generate module modules/whatsapp
nest generate controller modules/whatsapp
nest generate service modules/whatsapp
mkdir -p src/modules/whatsapp/dto
touch src/modules/whatsapp/dto/send-whatsapp-message.dto.ts

2. Define the Request DTO:

Create a Data Transfer Object (DTO) to define the expected structure and validation rules for the request body when sending a message.

typescript
// src/modules/whatsapp/dto/send-whatsapp-message.dto.ts
import { IsNotEmpty, IsString, IsPhoneNumber, IsOptional, IsUrl } from 'class-validator';

export class SendWhatsappMessageDto {
  @IsNotEmpty()
  @IsPhoneNumber(null) // Use null for generic phone number validation, adjust region if needed
  @IsString()
  readonly to: string; // Recipient's WhatsApp number (E.164 format recommended)

  @IsNotEmpty()
  @IsString()
  readonly text: string; // Message content (required even if sending media with caption)

  // Optional fields for media messages
  @IsOptional()
  @IsUrl()
  readonly imageUrl?: string;

  @IsOptional()
  @IsUrl()
  readonly videoUrl?: string;

  @IsOptional()
  @IsUrl()
  readonly audioUrl?: string;

  @IsOptional()
  @IsUrl()
  readonly fileUrl?: string;

  @IsOptional()
  @IsString()
  readonly caption?: string; // Caption for media
}

3. Implement the WhatsApp Service:

Inject MessageBirdService and ConfigService to interact with the API.

Note: The specific types imported from messagebird/types/conversations can change between SDK versions. Ensure these types are compatible with your installed messagebird package version.

typescript
// src/modules/whatsapp/whatsapp.service.ts
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MessageBirdService } from '../../core/messagebird/messagebird.service';
import { SendWhatsappMessageDto } from './dto/send-whatsapp-message.dto';
import {
  StartConversationParameter,
  Content,
  TextContent,
  ImageContent,
  VideoContent,
  AudioContent,
  FileContent,
} from 'messagebird/types/conversations'; // Import necessary types
// Optional: Import PrismaService if using DB
// import { PrismaService } from '../../core/prisma/prisma.service';

@Injectable()
export class WhatsappService {
  private readonly logger = new Logger(WhatsappService.name);
  private readonly channelId: string;

  constructor(
    private readonly messageBirdService: MessageBirdService,
    private readonly configService: ConfigService,
    // Optional: Inject PrismaService if storing messages
    // private readonly prisma: PrismaService,
  ) {
    this.channelId = this.configService.get<string>('MESSAGEBIRD_WHATSAPP_CHANNEL_ID');
    if (!this.channelId) {
      this.logger.error('MESSAGEBIRD_WHATSAPP_CHANNEL_ID is not defined.');
      throw new Error('WhatsApp Channel ID is required.');
    }
  }

  async sendMessage(dto: SendWhatsappMessageDto): Promise<any> {
    const client = this.messageBirdService.getClient();
    let content: Content;

    // Determine content type based on DTO fields
    if (dto.imageUrl) {
      content = { image: { url: dto.imageUrl, caption: dto.caption } } as ImageContent;
    } else if (dto.videoUrl) {
      content = { video: { url: dto.videoUrl, caption: dto.caption } } as VideoContent;
    } else if (dto.audioUrl) {
      content = { audio: { url: dto.audioUrl } } as AudioContent; // Captions may not be supported on all audio types
    } else if (dto.fileUrl) {
      content = { file: { url: dto.fileUrl, caption: dto.caption } } as FileContent;
    } else {
      content = { text: dto.text } as TextContent;
    }

    const params: StartConversationParameter = {
      to: dto.to,
      channelId: this.channelId,
      type: this.getContentType(content), // Dynamically set type based on content
      content: content,
      // reportUrl: Optional: Override global webhook URL for status updates for this specific message
    };

    this.logger.log(`Attempting to send message via channel ${this.channelId} to ${dto.to}. Payload: ${JSON.stringify(params)}`);

    try {
      // Use conversations.start - this works for initiating or replying
      // IMPORTANT: WhatsApp 24-Hour Window Policy
      // - Within 24 hours of a user message: You can send any message type freely
      // - Outside 24-hour window: You MUST use pre-approved Message Templates (HSM)
      // - As of April 1, 2025: Utility templates sent within the 24-hour window are FREE
      // - As of April 1, 2025: Marketing templates to US phone numbers are temporarily paused
      // - As of October 7, 2025: WhatsApp messaging limits are changing - check Meta's documentation
      const result = await client.conversations.start(params);
      this.logger.log(`Message accepted by MessageBird. ConversationID: ${result.id}, MessageID: ${result.messages?.lastMessageId}`);

      // Optional: Store outgoing message in DB
      // await this.storeOutgoingMessage(result, params);

      return result;
    } catch (error) {
      const statusCode = error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR;
      const errorData = error.response?.data || error.message;
      this.logger.error(`MessageBird API Error (${statusCode}) for recipient ${dto.to}: ${JSON.stringify(errorData)}`, error.stack);

      // Map MessageBird errors to NestJS HttpExceptions if desired
      if (statusCode === 400 || statusCode === 422) { // Example: Bad Request or Unprocessable Entity
          throw new HttpException({
             message: 'Failed to send message due to invalid data or number.',
             details: errorData
          }, HttpStatus.BAD_REQUEST);
      }
      // Throw a generic error for others
      throw new HttpException('Failed to send WhatsApp message.', HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  // Helper to determine the content type string
  private getContentType(content: Content): string {
    if ('text' in content) return 'text';
    if ('image' in content) return 'image';
    if ('video' in content) return 'video';
    if ('audio' in content) return 'audio';
    if ('file' in content) return 'file';
    // Add other types like 'location', 'hsm' if needed
    return 'text'; // Default fallback
  }

  // Optional: Method to store outgoing message
  // private async storeOutgoingMessage(result: any, params: StartConversationParameter): Promise<void> {
  //    if (!this.prisma) return;
  //    try {
  //       // ... Prisma upsert/create logic ...
  //       this.logger.log(`Outgoing message ${result.messages.lastMessageId} stored in DB.`);
  //    } catch (dbError) {
  //       this.logger.error(`Database error storing outgoing message ${result.messages.lastMessageId}:`, dbError);
  //    }
  // }

}
  • Why conversations.start? This API call is versatile. It can initiate a new conversation (if one doesn't exist or is archived) or send a message to an existing active conversation. For initiating conversations outside WhatsApp's 24-hour window, you'll need HSM templates. conversations.reply requires an existing conversationId.

4. Implement the WhatsApp Controller:

Expose a POST endpoint to trigger the sendMessage service method. Use ValidationPipe to automatically validate incoming DTOs.

typescript
// src/modules/whatsapp/whatsapp.controller.ts
import { Controller, Post, Body, UsePipes, ValidationPipe, HttpCode, HttpStatus } from '@nestjs/common';
import { WhatsappService } from './whatsapp.service';
import { SendWhatsappMessageDto } from './dto/send-whatsapp-message.dto';

@Controller('whatsapp') // Route prefix: /whatsapp
export class WhatsappController {
  constructor(private readonly whatsappService: WhatsappService) {}

  @Post('send') // Endpoint: POST /whatsapp/send
  @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) // Enable validation
  @HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted as sending is async
  async sendMessage(@Body() sendWhatsappMessageDto: SendWhatsappMessageDto) {
    try {
      const result = await this.whatsappService.sendMessage(sendWhatsappMessageDto);
      // Return a simplified response or the full MessageBird response
      return {
        message: 'Message accepted for delivery.',
        details: {
          conversationId: result.id, // Conversation ID
          messageId: result.messages?.lastMessageId // ID of the newly sent message
        }
      };
    } catch (error) {
      // Error is logged in the service, NestJS will handle the HTTP response
      // based on the error type (e.g., 500 for unexpected, 400 for validation/API error)
       throw error; // Re-throw for NestJS default exception filter or custom filter
    }
  }
}

5. Register WhatsappModule in AppModule:

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 { MessageBirdModule } from './core/messagebird/messagebird.module';
import { WhatsappModule } from './modules/whatsapp/whatsapp.module'; // Import
// Import WebhooksModule and PrismaModule later if used

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    MessageBirdModule,
    WhatsappModule, // Add WhatsappModule
    // Add WebhooksModule later
    // Add PrismaModule later (if using DB)
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

6. Testing the WhatsApp API Endpoint:

Start your NestJS application:

bash
npm run start:dev
# or
yarn start:dev

Use curl or Postman to send a request to POST http://localhost:3000/whatsapp/send (replace port if needed).

Example curl (Text Message):

Remember to use a number registered with your WhatsApp Sandbox for testing initially.

bash
curl -X POST http://localhost:3000/whatsapp/send \
-H "Content-Type: application/json" \
-d '{
  "to": "+12345678900",
  "text": "Hello from NestJS and MessageBird!"
}'

Example curl (Image Message):

bash
curl -X POST http://localhost:3000/whatsapp/send \
-H "Content-Type: application/json" \
-d '{
  "to": "+12345678900",
  "text": "Check out this logo",
  "imageUrl": "https://www.messagebird.com/assets/images/og/messagebird.png",
  "caption": "MessageBird Logo"
}'

Expected Response (Success - 202 Accepted):

json
{
  "message": "Message accepted for delivery.",
  "details": {
    "conversationId": "conv_id_from_messagebird_xxxxxxxx",
    "messageId": "msg_id_from_messagebird_yyyyyyyy"
  }
}

You should receive the WhatsApp message on the target device shortly after. Check your application logs for details from WhatsappService.

4. Receiving WhatsApp Messages via Webhooks

MessageBird uses webhooks to notify your application about incoming messages (message.created) and status updates for outgoing messages (message.updated).

1. Configure Webhook in MessageBird Dashboard:

  • Go to your MessageBird Dashboard > Channels > WhatsApp > Select your Channel (e.g., Sandbox).
  • Find the 'Webhooks' or 'Conversation Webhooks' section.
  • Click 'Add webhook'.
  • URL: Enter your public webhook URL. Using the .env variable: ${WEBHOOK_BASE_URL}/webhooks/messagebird. For local dev with ngrok, this would be like https://abcdef123456.ngrok.io/webhooks/messagebird. Must be HTTPS.
  • Events: Select at least message.created and message.updated. You might also want conversation.created, conversation.updated.
  • Signing Key: Ensure your generated signing key (from step 1.4) is active.
  • Save the webhook.

2. Create Webhooks Module, Controller, and DTO:

bash
nest generate module modules/webhooks
nest generate controller modules/webhooks
mkdir -p src/modules/webhooks/dto
touch src/modules/webhooks/dto/messagebird-webhook.dto.ts

Define Webhook Payload DTO:

Create a DTO to provide basic type safety for the incoming webhook payload.

typescript
// src/modules/webhooks/dto/messagebird-webhook.dto.ts
import { Type } from 'class-transformer';
import { IsString, IsOptional, IsObject, ValidateNested, IsDefined } from 'class-validator';

// Basic nested structures - can be expanded significantly based on MessageBird docs
class MessageContentDto {
  @IsOptional() @IsString() text?: string;
  @IsOptional() @IsObject() image?: { url: string; caption?: string };
  @IsOptional() @IsObject() video?: { url: string; caption?: string };
  @IsOptional() @IsObject() audio?: { url: string };
  @IsOptional() @IsObject() file?: { url: string; caption?: string };
  // Add other content types as needed (location, hsm, etc.)
}

class MessagePayloadDto {
  @IsOptional() @IsString() id?: string; // Message ID
  @IsOptional() @IsString() conversationId?: string;
  @IsOptional() @IsString() channelId?: string;
  @IsOptional() @IsString() from?: string; // Sender phone number for incoming
  @IsOptional() @IsString() to?: string; // Recipient for outgoing status updates
  @IsOptional() @IsString() direction?: 'incoming' | 'outgoing';
  @IsOptional() @IsString() status?: string; // 'pending', 'sent', 'delivered', 'read', 'failed'
  @IsOptional() @IsString() type?: string; // 'text', 'image', 'video', 'audio', 'file', 'hsm', 'location' etc.
  @IsOptional() @ValidateNested() @Type(() => MessageContentDto) content?: MessageContentDto;
  @IsOptional() @IsString() createdDatetime?: string; // ISO 8601 timestamp
  @IsOptional() @IsString() updatedDatetime?: string; // ISO 8601 timestamp
  @IsOptional() @IsObject() error?: { code?: number; description?: string }; // For 'failed' status
}

class ConversationPayloadDto {
  @IsOptional() @IsString() id?: string; // Conversation ID
  @IsOptional() @IsString() contactId?: string;
  @IsOptional() @IsString() status?: 'active' | 'archived';
  @IsOptional() @IsString() createdDatetime?: string; // ISO 8601 timestamp
  @IsOptional() @IsString() updatedDatetime?: string; // ISO 8601 timestamp
  // Add other conversation fields if needed
}

export class MessageBirdWebhookPayloadDto {
  // The top-level structure can vary, ensure validation handles optional fields
  @IsOptional() @ValidateNested() @Type(() => ConversationPayloadDto) conversation?: ConversationPayloadDto;

  @IsOptional() @ValidateNested() @Type(() => MessagePayloadDto) message?: MessagePayloadDto;

  // 'type' is usually the key indicator of the event
  @IsDefined() // Type should always be present
  @IsString()
  type: 'message.created' | 'message.updated' | 'conversation.created' | 'conversation.updated' | string; // Allow other types but specify common ones

  // Add other potential top-level fields if necessary (e.g., contact details)
}

3. Implement Webhook Signature Verification (Security):

We need to verify that incoming requests genuinely originate from MessageBird. We'll create a middleware for this.

bash
mkdir -p src/common/middleware
touch src/common/middleware/messagebird-verify.middleware.ts
typescript
// src/common/middleware/messagebird-verify.middleware.ts
import { Injectable, NestMiddleware, Logger, ForbiddenException, RawBodyRequest } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';

@Injectable()
export class MessageBirdVerifyMiddleware implements NestMiddleware {
  private readonly logger = new Logger(MessageBirdVerifyMiddleware.name);
  private readonly signingKey: string;

  constructor(private configService: ConfigService) {
    this.signingKey = this.configService.get<string>('MESSAGEBIRD_WEBHOOK_SIGNING_KEY');
    if (!this.signingKey) {
      // IMPORTANT: Verification MUST be enabled in production.
      // Consider throwing an error here during startup if the key is missing in a 'production' environment.
      this.logger.error(
          'MESSAGEBIRD_WEBHOOK_SIGNING_KEY is not defined. ' +
          'Webhook verification will be DISABLED. This is INSECURE for production!'
      );
    }
  }

  use(req: RawBodyRequest<Request>, res: Response, next: NextFunction) {
    if (!this.signingKey) {
        // Skip verification if key is missing (development only!)
        this.logger.warn('CRITICAL: Skipping MessageBird webhook verification as signing key is not configured. THIS IS INSECURE AND MUST NOT HAPPEN IN PRODUCTION.');
        return next();
    }

    const signature = req.headers['messagebird-signature'] as string;
    const timestamp = req.headers['messagebird-request-timestamp'] as string;
    // Requires rawBody to be enabled in main.ts
    const requestBody = req.rawBody;

    if (!signature || !timestamp || !requestBody) {
      this.logger.warn('Missing MessageBird signature headers or body for verification.');
      throw new ForbiddenException('MessageBird signature verification failed: Missing headers or body.');
    }

    try {
      const hmac = crypto.createHmac('sha256', this.signingKey);
      // Use utf8 encoding for the body when creating the signature base string
      const signedContent = `${timestamp}.${requestBody.toString('utf8')}`;
      const expectedSignature = hmac.update(signedContent).digest('hex');

      // Use timingSafeEqual for security against timing attacks
      if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
        this.logger.log('MessageBird webhook signature verified successfully.');
        next();
      } else {
        this.logger.warn(`Webhook signature mismatch. Expected: ${expectedSignature}, Received: ${signature}`);
        throw new ForbiddenException('MessageBird signature verification failed: Invalid signature.');
      }
    } catch (error) {
      this.logger.error('Error during webhook signature verification:', error);
      throw new ForbiddenException(`MessageBird signature verification failed: ${error.message}`);
    }
  }
}
  • RawBodyRequest: This middleware relies on accessing the raw request body before it's parsed as JSON. We need to enable this in main.ts.

4. Enable Raw Body Parsing:

Modify src/main.ts to enable rawBody.

typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe
import { NestExpressApplication } from '@nestjs/platform-express'; // Import NestExpressApplication

async function bootstrap() {
  // Specify NestExpressApplication to access underlying Express features
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
     rawBody: true, // <<< Enable raw body parsing for signature verification
  });

  // Global Validation Pipe (applied after rawBody parsing, good for other routes)
  // Note: Webhook validation might need specific pipe settings (see controller)
  app.useGlobalPipes(new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true, // Automatically transform payloads to DTO instances
  }));

  // Enable CORS if your client is on a different origin
  app.enableCors();

  const port = process.env.PORT || 3000;
  await app.listen(port);
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

5. Implement the Webhooks Controller:

Create an endpoint to receive POST requests from MessageBird. Use the DTO and potentially a validation pipe.

typescript
// src/modules/webhooks/webhooks.controller.ts
import { Controller, Post, Body, Headers, Logger, HttpCode, HttpStatus, UsePipes, ValidationPipe, Req } from '@nestjs/common';
import { Request } from 'express';
import { MessageBirdWebhookPayloadDto } from './dto/messagebird-webhook.dto';
// Optional: Import PrismaService if handling DB logic here or in a dedicated service
// import { PrismaService } from '../../core/prisma/prisma.service';

@Controller('webhooks') // Route prefix: /webhooks
export class WebhooksController {
  private readonly logger = new Logger(WebhooksController.name);

  // Optional: Inject PrismaService or a dedicated processing service
  constructor(
    // private readonly prismaService: PrismaService
  ) {}

  @Post('messagebird') // Endpoint: POST /webhooks/messagebird
  @HttpCode(HttpStatus.OK) // Acknowledge receipt successfully
  // Apply validation pipe, skip missing properties as payload structure varies
  @UsePipes(new ValidationPipe({ skipMissingProperties: true, whitelist: true }))
  handleMessageBirdWebhook(
    @Body() payload: MessageBirdWebhookPayloadDto,
    @Headers('messagebird-signature') signature: string, // For logging/debugging if needed
    @Headers('messagebird-request-timestamp') timestamp: string, // For logging/debugging
    @Req() req: Request // Access raw request if needed, though middleware handles verification
  ): void { // Return void or a simple confirmation

    this.logger.log(`Received MessageBird webhook. Type: ${payload.type}, Signature: ${signature ? 'Present' : 'Missing'}, Timestamp: ${timestamp}`);
    this.logger.debug(`Webhook Payload: ${JSON.stringify(payload)}`);

    // Process based on event type
    switch (payload.type) {
      case 'message.created':
        if (payload.message?.direction === 'incoming') {
          this.logger.log(`Incoming message received from ${payload.message.from}. Content: ${JSON.stringify(payload.message.content)}`);
          // TODO: Implement logic to handle incoming message
          // - Parse payload.message.content (text, image, etc.)
          // - Potentially reply using WhatsappService
          // - Store in DB
          // Example: if (payload.message.content?.text === 'hello') { /* send reply */ }
        } else {
          this.logger.log(`Webhook for outgoing message creation (ID: ${payload.message?.id}). Usually handled by message.updated.`);
        }
        break;

      case 'message.updated':
        if (payload.message?.direction === 'outgoing') {
          this.logger.log(`Status update for outgoing message ${payload.message.id} to ${payload.message.to}: ${payload.message.status}`);
          // TODO: Implement logic to handle status update
          // - Update message status in DB
          // - Trigger notifications based on status (e.g., 'delivered', 'read', 'failed')
          if (payload.message.status === 'failed') {
            this.logger.error(`Message ${payload.message.id} failed: ${JSON.stringify(payload.message.error)}`);
            // Handle failure (retry logic, alert admin, etc.)
          }
        }
        break;

      case 'conversation.created':
        this.logger.log(`New conversation started: ${payload.conversation?.id}`);
        // Optional: Handle conversation creation event
        break;

      case 'conversation.updated':
        this.logger.log(`Conversation ${payload.conversation?.id} updated. Status: ${payload.conversation?.status}`);
        // Optional: Handle conversation status changes (e.g., archived)
        break;

      default:
        this.logger.warn(`Received unhandled webhook event type: ${payload.type}`);
    }

    // Acknowledge receipt - NestJS handles sending the 200 OK response implicitly
    // No explicit return needed unless sending back data (not typical for webhooks)
  }
}

6. Apply Middleware and Register Module:

Apply the MessageBirdVerifyMiddleware specifically to the webhook route in the WebhooksModule. Then, register WebhooksModule in AppModule.

typescript
// src/modules/webhooks/webhooks.module.ts
import { Module, MiddlewareConsumer, NestModule, RequestMethod } from '@nestjs/common';
import { WebhooksController } from './webhooks.controller';
import { MessageBirdVerifyMiddleware } from '../../common/middleware/messagebird-verify.middleware';
// Import ConfigModule if not global
// import { ConfigModule } from '@nestjs/config';
// Import PrismaModule if needed for DB operations
// import { PrismaModule } from '../../core/prisma/prisma.module';

@Module({
  imports: [
    // ConfigModule, // Only if not global
    // PrismaModule, // If handling DB logic here or in a service
  ],
  controllers: [WebhooksController],
  providers: [
    // Add any specific services for webhook processing if needed
  ],
})
export class WebhooksModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(MessageBirdVerifyMiddleware)
      .forRoutes({ path: 'webhooks/messagebird', method: RequestMethod.POST });
  }
}
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 { MessageBirdModule } from './core/messagebird/messagebird.module';
import { WhatsappModule } from './modules/whatsapp/whatsapp.module';
import { WebhooksModule } from './modules/webhooks/webhooks.module'; // Import
// Import PrismaModule later if used

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    MessageBirdModule,
    WhatsappModule,
    WebhooksModule, // Add WebhooksModule
    // Add PrismaModule later (if using DB)
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

7. Testing Webhooks:

  1. Ensure your NestJS app is running (npm run start:dev).
  2. Ensure ngrok is running and forwarding to your app's port (ngrok http 3000).
  3. Ensure the https ngrok URL is configured as the webhook URL in MessageBird for your WhatsApp channel, pointing to /webhooks/messagebird.
  4. Test Incoming Message: Send a message from your registered Sandbox phone number to the Sandbox WhatsApp number provided by MessageBird. You should see logs in your NestJS console from WebhooksController indicating a message.created event.
  5. Test Status Update: Send an outgoing message using the /whatsapp/send endpoint (from Step 3). As the message progresses (sent, delivered, read), you should see logs in your NestJS console for message.updated events.

Check the ngrok web interface (http://localhost:4040 by default) to inspect incoming requests and responses, which is helpful for debugging. Ensure you see 200 OK responses from your webhook endpoint.

For more WhatsApp Business API integrations and guides, explore these related tutorials:

Frequently Asked Questions

How to send WhatsApp messages with Node.js and NestJS?

Use the MessageBird Conversations API along with the NestJS framework and Node.js. The provided guide details setting up a NestJS project with the necessary dependencies, creating a service to interact with MessageBird, and setting up a controller to handle sending messages via a dedicated API endpoint. This enables programmatic sending of text messages, images, videos, audio files, and other media through WhatsApp.

What is MessageBird's WhatsApp Sandbox?

MessageBird's WhatsApp Sandbox is a testing environment provided by MessageBird that lets developers experiment with WhatsApp integration without needing a fully approved WhatsApp Business Account. This allows you to test sending and receiving messages within the sandbox without incurring real-world costs or requiring immediate business verification.

Why use NestJS for WhatsApp integration?

NestJS offers several advantages. Its modular architecture, dependency injection, and built-in features like validation and configuration make it ideal for complex integrations like this one. TypeScript support also improves code quality and maintainability of your application.

How to integrate WhatsApp with MessageBird API?

The integration involves setting up a project with the MessageBird Node.js SDK, configuring environment variables for API keys and channel IDs, creating a MessageBird service, and implementing webhook handling for incoming messages and status updates. This setup enables bi-directional communication via WhatsApp using MessageBird as the intermediary platform.

How to receive WhatsApp messages in my NestJS app?

Set up webhooks within your MessageBird dashboard to receive incoming messages and status updates for your outgoing messages. Configure the webhook URL to point to a dedicated endpoint in your NestJS application which processes these updates in real time. Be sure to implement robust security measures, such as signature verification, to validate the webhook's origin and authenticity. It will also be necessary to create Data Transfer Objects (DTOs) and a webhook controller with a POST method to securely handle the incoming data.

How to set up MessageBird webhooks for WhatsApp?

Navigate to your MessageBird Dashboard, then to Channels > WhatsApp and select your channel (Sandbox or live). Add a webhook, specifying your NestJS app's public URL as the target endpoint along with the necessary events for incoming messages and status updates (`message.created`, `message.updated`). Ensure your webhook signing key is active and secure. Always use an HTTPS URL for webhooks to guarantee secure communication between MessageBird and your server, especially in a production environment. A temporary URL from a service such as ngrok can be used for local testing.

What is the MESSAGEBIRD_API_KEY used for?

The `MESSAGEBIRD_API_KEY` authenticates your application with the MessageBird platform. It's a crucial credential that grants your application access to MessageBird's APIs, enabling it to send and receive messages, manage contacts, and access various other features within the MessageBird ecosystem. You'll need to add your key to a `.env` file, which should never be exposed to version control.

Where do I find my MessageBird WhatsApp Channel ID?

The WhatsApp Channel ID is found in your MessageBird Dashboard under Channels > WhatsApp. Select the specific channel you are using, either the "WhatsApp Sandbox" or your provisioned live channel. The Channel ID will be listed within the channel's details.

What is the purpose of a webhook signing key?

The webhook signing key is a security measure to verify the authenticity of incoming webhook requests. It ensures that these requests genuinely originate from MessageBird and haven't been tampered with. Generating and configuring this key is essential for secure operation, and verification should never be bypassed in production environments.

When should I use conversations.start vs conversations.reply with the MessageBird API?

Use `conversations.start` to initiate a new conversation or to reply within an existing, active conversation. For initial messages outside the 24-hour window, HSM templates are required. `conversations.reply` is specifically for replying within an existing conversation and requires the `conversationId`.

Can I send media messages through WhatsApp with MessageBird?

Yes, the integration allows sending various media types including images, videos, audio files, and other documents. Include the media URLs in the request payload when sending a message through your NestJS application. You can also include captions with your media.

How to test MessageBird WhatsApp integration locally?

Use a tool like ngrok to create a secure tunnel to your locally running NestJS application. Configure this public ngrok URL as your webhook URL in the MessageBird dashboard. This allows MessageBird to send webhook events to your local development environment for testing purposes. Send a test message from your registered WhatsApp Sandbox number to the Sandbox number provided by MessageBird.

What are the prerequisites for integrating WhatsApp with Node.js and NestJS?

You'll need Node.js and npm/yarn, a MessageBird account, access to the MessageBird WhatsApp Sandbox or a provisioned WhatsApp Business channel, a tool like Postman for API testing, and optionally a database instance and ngrok for local webhook testing. Familiarity with core web development technologies, including JavaScript and TypeScript is also recommended.