code examples

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

Plivo Inbound SMS & Two-Way Messaging with NestJS: Complete Implementation Guide

Learn how to build production-ready NestJS applications with Plivo webhooks for inbound SMS. Complete tutorial covering webhook setup, two-way messaging, signature validation, and secure deployment.

Plivo Inbound SMS and Two-Way Messaging with NestJS: Complete Guide

Learn how to build a production-ready NestJS application that receives incoming SMS messages via Plivo webhooks and responds automatically with two-way messaging. This comprehensive guide covers project setup, webhook implementation, security best practices, error handling, and deployment strategies.

You'll learn how to configure Plivo to forward incoming messages to your NestJS application, process the message content, and send replies using Plivo's XML format. This forms the foundation for building interactive SMS applications, automated customer service bots, appointment reminder systems, and real-time notification platforms.

Project Overview and Goals

  • What We'll Build: A NestJS application with a dedicated webhook endpoint that listens for incoming SMS messages sent to a Plivo phone number. Upon receiving a message, the application will log the message details and automatically send a reply back to the sender.

  • Problem Solved: Enables applications to react to and engage with users via standard SMS, facilitating interactive communication without requiring users to install a separate app.

  • Technologies Used:

    • NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Its modular architecture and built-in features (like configuration management, logging, and validation pipes) make it ideal for production APIs.
    • Plivo: A cloud communications platform providing SMS and Voice APIs. We'll use their SMS API and Node.js SDK.
    • Node.js: The underlying JavaScript runtime environment.
    • TypeScript: Superset of JavaScript used by NestJS, providing static typing.
    • ngrok (for local development): A tool to expose local development servers to the public internet, necessary for Plivo webhooks to reach your machine during testing.
  • Architecture:

    User's Phone <-- SMS --> Plivo Platform <-- HTTPS POST --> Your Server (via ngrok or Public IP) ^ | | | Process Request | Plivo XML Response <----------------| NestJS Application | | ------------------------- SMS Reply <-------------------------
  • Prerequisites:

    • Node.js (LTS version recommended) and npm/yarn installed.
    • A Plivo account (Free trial available).
    • An SMS-enabled Plivo phone number.
    • Plivo Auth ID and Auth Token (found on your Plivo Console dashboard).
    • ngrok installed globally or available in your PATH for local testing.
    • Basic understanding of TypeScript, Node.js, and REST APIs.
  • Final Outcome: A functional NestJS application deployed (locally via ngrok or on a server) that receives SMS messages sent to your Plivo number and replies automatically.

1. Setting Up Your NestJS Project with Plivo

Let's initialize our NestJS project and install the necessary dependencies for Plivo webhook integration.

  1. Install NestJS CLI: If you don't have it, install it globally:

    bash
    npm install -g @nestjs/cli
  2. Create NestJS Project:

    bash
    nest new plivo-nestjs-inbound
    cd plivo-nestjs-inbound

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

  3. Install Dependencies: We need the Plivo Node.js SDK and NestJS config module for environment variables.

    bash
    npm install plivo @nestjs/config
    # OR
    yarn add plivo @nestjs/config
  4. Environment Variables: Create a .env file in the project root for storing sensitive credentials and configuration. Never commit this file to version control – add .env to your .gitignore file.

    dotenv
    # .env
    PORT=3000
    
    # Plivo Credentials - Get from https://console.plivo.com/dashboard/
    PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
    PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
    PLIVO_PHONE_NUMBER=YOUR_PLIVO_PHONE_NUMBER # The number receiving messages (e.g., +14155551212)
    • PORT: The port your NestJS application will listen on.
    • PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN: Your Plivo API credentials. Crucially important for validating incoming webhook requests.
    • PLIVO_PHONE_NUMBER: Your Plivo number associated with the webhook. Useful for configuration and potentially as the src if sending replies via the API instead of XML response.
  5. Configure Environment Variables: Import and configure ConfigModule in your main application module (src/app.module.ts) to load the .env file.

    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 { PlivoWebhookModule } from './plivo-webhook/plivo-webhook.module'; // We will create this next
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true, // Make ConfigService available globally
          envFilePath: '.env',
        }),
        PlivoWebhookModule, // Import our Plivo module
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
  6. Update Main Entry Point: Modify src/main.ts to use the configured port and enable raw body parsing for signature validation.

    typescript
    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ConfigService } from '@nestjs/config';
    import { Logger } from '@nestjs/common'; // Import Logger
    
    async function bootstrap() {
      // Enable rawBody for webhook signature validation
      const app = await NestFactory.create(AppModule, { rawBody: true });
      const configService = app.get(ConfigService);
      const port = configService.get<number>('PORT', 3000); // Default to 3000 if not set
    
      // Enable shutdown hooks for graceful shutdown
      app.enableShutdownHooks();
    
      // NestJS with rawBody: true handles common body types like JSON and urlencoded
      // while preserving the raw buffer. No need for app.useBodyParser here usually.
    
      await app.listen(port);
      Logger.log(`Application listening on port ${port}`, 'Bootstrap'); // Use NestJS Logger
    }
    bootstrap();

    Note: Passing { rawBody: true } to NestFactory.create is essential for the Plivo signature validation guard to work correctly.

  7. Create Plivo Webhook Module: Generate a new module, controller, and service to handle Plivo logic.

    bash
    nest generate module plivo-webhook
    nest generate controller plivo-webhook --no-spec
    nest generate service plivo-webhook --no-spec

    This creates a src/plivo-webhook directory with the necessary files and updates src/app.module.ts automatically (as seen in step 5).

2. Implementing the Plivo Webhook Handler

Now, let's build the controller and service to handle incoming Plivo webhooks for inbound SMS messages.

  1. Plivo Webhook Service (src/plivo-webhook/plivo-webhook.service.ts): This service will contain the logic to process the incoming message data and generate the Plivo XML response.

    typescript
    // src/plivo-webhook/plivo-webhook.service.ts
    import { Injectable, Logger } from '@nestjs/common';
    import * as plivo from 'plivo';
    
    export interface PlivoSmsPayload {
      From: string;
      To: string;
      Text: string;
      Type: string; // 'sms', 'mms', or 'whatsapp'
      MessageUUID: string;
      Units: number; // Number of message parts (>1 for long messages)
      TotalRate?: string; // Charge per message unit
      TotalAmount?: string; // Total message charge
      MessageIntent?: 'optout' | 'optin' | 'help'; // Platform-detected compliance keywords
      // MMS-specific fields (when Type='mms')
      Body?: string; // Text content for MMS
      MediaCount?: number; // Number of media files
      Media0?: string; // URL for first media file
      Media1?: string; // URL for second media file (if exists)
      // Additional Media URLs follow pattern Media2, Media3, etc.
    }
    
    @Injectable()
    export class PlivoWebhookService {
      private readonly logger = new Logger(PlivoWebhookService.name);
    
      handleIncomingSms(payload: PlivoSmsPayload): string {
        this.logger.log(
          `Received SMS from ${payload.From} to ${payload.To}: ""${payload.Text}"" (UUID: ${payload.MessageUUID})`,
        );
    
        // Basic validation or keyword checking can go here
        // Example: Check if Text is 'HELP' or 'STOP'
    
        // --- Generate Plivo XML Response ---
        const response = new plivo.Response();
    
        const replyText = `Thanks for your message! You said: ""${payload.Text}""`;
    
        // Parameters for the <Message> XML element
        const params = {
          src: payload.To, // The Plivo number that received the message
          dst: payload.From, // The user's number to reply to
        };
    
        response.addMessage(replyText, params);
    
        const xmlResponse = response.toXML();
        this.logger.log(`Generated XML Response: ${xmlResponse}`);
    
        return xmlResponse;
      }
    
      // Add methods here for sending outbound messages via API if needed later
      // e.g., using Plivo client: client.messages.create(...)
    }
    • We define an interface PlivoSmsPayload for type safety based on Plivo's webhook parameters.
    • The handleIncomingSms method takes the parsed payload, logs it, and uses the plivo SDK's Response class to build the XML.
    • response.addMessage(text, params) creates the <Message> element in the XML. Note how src (source of reply) and dst (destination of reply) are set.
    • It returns the generated XML string.
  2. Plivo Webhook Controller (src/plivo-webhook/plivo-webhook.controller.ts): This controller defines the HTTP endpoint that Plivo will call.

    typescript
    // src/plivo-webhook/plivo-webhook.controller.ts
    import {
      Controller,
      Post,
      Body,
      Header,
      HttpCode,
      HttpStatus,
      Logger,
      UseGuards, // Import UseGuards
      Req, // Import Req
      RawBodyRequest, // Import RawBodyRequest
    } from '@nestjs/common';
    import { PlivoWebhookService, PlivoSmsPayload } from './plivo-webhook.service';
    import { PlivoSignatureGuard } from './plivo-signature.guard'; // We will create this next
    import { Request } from 'express'; // Import Request type
    
    @Controller('plivo-webhook') // Route prefix: /plivo-webhook
    export class PlivoWebhookController {
      private readonly logger = new Logger(PlivoWebhookController.name);
    
      constructor(private readonly plivoWebhookService: PlivoWebhookService) {}
    
      @Post('message') // Full route: POST /plivo-webhook/message
      @UseGuards(PlivoSignatureGuard) // Apply the signature validation guard
      @HttpCode(HttpStatus.OK) // Plivo expects a 200 OK for successful handling
      @Header('Content-Type', 'application/xml') // Set the response content type
      handleMessageWebhook(
        @Body() payload: PlivoSmsPayload,
        // @Req() is still needed for the guard to access the request object
        // even if we primarily use @Body for the payload here.
        @Req() request: RawBodyRequest<Request>,
      ): string {
        this.logger.log('Incoming Plivo Message Webhook Request Received');
        // The PlivoSignatureGuard runs first. If it passes, this handler executes.
    
        // NestJS automatically parses the urlencoded body into 'payload'
        // The guard uses the rawBody attached by NestFactory ({ rawBody: true })
        return this.plivoWebhookService.handleIncomingSms(payload);
      }
    }
    • @Controller('plivo-webhook'): Defines the base path for routes in this controller.
    • @Post('message'): Defines a handler for POST requests to /plivo-webhook/message. This will be our message_url in Plivo.
    • @UseGuards(PlivoSignatureGuard): Crucial security step. This applies a guard (created in Section 7) to verify the incoming request genuinely came from Plivo using its signature.
    • @Body() payload: PlivoSmsPayload: Uses NestJS's built-in parsing to extract the request body (expected to be application/x-www-form-urlencoded from Plivo) and map it to our PlivoSmsPayload interface.
    • @Req() request: RawBodyRequest<Request>: Provides access to the full request object, including the raw body buffer needed by the signature guard. RawBodyRequest is used for type safety when { rawBody: true } is enabled.
    • @Header('Content-Type', 'application/xml'): Sets the Content-Type header on the response to tell Plivo we are sending back XML.
    • @HttpCode(HttpStatus.OK): Ensures a 200 OK status code is sent on success. Plivo expects this.
    • It calls the plivoWebhookService.handleIncomingSms method with the payload and returns the resulting XML string directly as the response body.

3. Building an API Layer

In this specific scenario, our "API" is the single webhook endpoint /plivo-webhook/message designed to be consumed by Plivo. Authentication and authorization are handled via Plivo's request signature validation (covered in Section 7). Request validation is implicitly handled by NestJS's body parsing and TypeScript types; more complex validation using class-validator could be added if needed (e.g., ensuring From and To are valid phone numbers).

Testing this endpoint locally requires ngrok and sending an SMS, or using curl/Postman to simulate Plivo's request. Simulating requires generating valid X-Plivo-Signature-V3 and X-Plivo-Signature-V3-Nonce headers, which is complex.

Example Simulated curl Request (Will Fail Signature Validation):

This command demonstrates the structure of Plivo's request but will be blocked by the PlivoSignatureGuard because it lacks a valid signature. Use it only for basic route testing if the guard is temporarily disabled (not recommended).

bash
curl -X POST http://localhost:3000/plivo-webhook/message \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "From=15551234567" \
--data-urlencode "To=14155551212" \
--data-urlencode "Text=Hello from curl" \
--data-urlencode "Type=sms" \
--data-urlencode "MessageUUID=abc-123-def-456"
  • Important Note: Never disable the signature guard in staging or production environments. Doing so creates a severe security vulnerability, allowing anyone to send unverified requests to your webhook endpoint, potentially leading to abuse, data corruption, or unwanted costs. The best way to test the full flow locally is using ngrok and sending a real SMS.

4. Configuring Plivo to Send Webhooks to Your Application

Now, let's configure Plivo to send incoming messages to your local NestJS application using ngrok.

  1. Start Your NestJS App:

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

    Ensure it's running and listening on the correct port (e.g., 3000) and that the rawBody: true option is enabled in main.ts.

  2. Start ngrok: Open a new terminal window and expose your local port to the internet.

    bash
    ngrok http 3000
    # Make sure '3000' matches the PORT in your .env file / main.ts

    ngrok will display forwarding URLs (e.g., https://<unique-id>.ngrok-free.app). Copy the https URL.

  3. Create/Configure Plivo Application:

    • Log in to your Plivo Console.
    • Navigate to Messaging -> Applications -> XML.
    • Click Add New Application.
    • Application Name: Give it a descriptive name (e.g., NestJS Inbound App).
    • Message URL: Paste your ngrok HTTPS URL, appending the controller route: https://<unique-id>.ngrok-free.app/plivo-webhook/message.
    • Method: Select POST.
    • Hangup URL / Answer URL: Leave blank (these are for voice calls).
    • Click Create Application.
  4. Link Plivo Number to Application:

    • Navigate to Phone Numbers -> Your Numbers.
    • Find the SMS-enabled number you want to use. Click on it.
    • Under Application Type, select XML Application.
    • From the Plivo Application dropdown, select the application you just created (NestJS Inbound App).
    • Click Update Number.

Environment Variables Summary:

  • PORT: (e.g., 3000) - Port your NestJS app runs on locally.
  • PLIVO_AUTH_ID: (e.g., MANXXXXXXXXXXXXXXXXX) - Found on Plivo Console Dashboard. Used for signature validation.
  • PLIVO_AUTH_TOKEN: (e.g., abc...xyz) - Found on Plivo Console Dashboard. Used for signature validation.
  • PLIVO_PHONE_NUMBER: (e.g., +14155551212) - Your Plivo number receiving the SMS. Used potentially in logic, good to have in config.

5. Implementing Error Handling and Logging

  • Logging: We are using NestJS's built-in Logger. It provides structured logging with timestamps and context (class names). In production, consider configuring more advanced logging (e.g., sending logs to a centralized service like Datadog or ELK stack) using custom logger implementations or libraries like winston.

  • Basic Error Handling: The plivo.Response() generation is unlikely to throw errors. More complex logic in the service (e.g., database interactions, external API calls) should be wrapped in try...catch blocks.

  • NestJS Exception Filters: For a global error handling strategy, implement NestJS Exception Filters. This allows you to catch unhandled exceptions, log them consistently, and return appropriate responses. For Plivo webhooks, returning a 200 OK with an empty <Response></Response> is often preferred even on error to prevent Plivo from retrying the webhook, while still logging the error internally.

    typescript
    // Example: src/common/all-exceptions.filter.ts (Basic structure)
    import {
      ExceptionFilter,
      Catch,
      ArgumentsHost,
      HttpException,
      HttpStatus,
      Logger,
    } from '@nestjs/common';
    import { Response, Request } from 'express'; // Import Response/Request
    
    @Catch() // Catch all exceptions if no specific type is provided
    export class AllExceptionsFilter implements ExceptionFilter {
      private readonly logger = new Logger(AllExceptionsFilter.name);
    
      catch(exception: unknown, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
    
        const status =
          exception instanceof HttpException
            ? exception.getStatus()
            : HttpStatus.INTERNAL_SERVER_ERROR;
    
        const message =
          exception instanceof HttpException
            ? exception.getResponse()
            : 'Internal server error';
    
        // Log the error with context
        this.logger.error(
          `HTTP Status: ${status} Error Message: ${JSON.stringify(message)} Request: ${request.method} ${request.url}`,
          exception instanceof Error ? exception.stack : '',
          AllExceptionsFilter.name, // Context
        );
    
        // For Plivo webhooks, acknowledge receipt to prevent retries,
        // even if an internal error occurred. Log the error for investigation.
        response.setHeader('Content-Type', 'application/xml');
        response.status(HttpStatus.OK).send('<Response></Response>'); // Empty XML response
      }
    }
    • Apply Globally: Apply this filter in src/main.ts:

      typescript
      // src/main.ts (inside bootstrap function)
      import { AllExceptionsFilter } from './common/all-exceptions.filter'; // Adjust path if needed
      // ...
      app.useGlobalFilters(new AllExceptionsFilter());
      // ...
  • Plivo Retries: If your webhook endpoint fails to respond with a 200 OK within Plivo's timeout period (typically 15 seconds), or returns an error status code (like 5xx), Plivo may retry the request. Returning an empty <Response></Response> with a 200 OK via the exception filter prevents unwanted retries while allowing you to log the internal failure. Check Plivo's logs (Messaging -> Logs) for webhook delivery status and failures.

6. Creating a Database Schema and Data Layer

While this core example doesn't require a database, a real-world application would likely store message history, user state, or related data.

Next Steps (Optional):

  1. Choose ORM/Database: Select a database (e.g., PostgreSQL, MySQL, MongoDB) and an ORM/ODM like TypeORM or Prisma.

  2. Install Dependencies: Example for TypeORM + PostgreSQL: npm install @nestjs/typeorm typeorm pg.

  3. Configure Database Connection: Set up database credentials in .env and configure the TypeOrmModule in app.module.ts.

  4. Define Entities/Models: Create TypeORM entities or Prisma models (e.g., MessageLog) to represent your database tables/collections.

    typescript
    // Example: src/message-log/message-log.entity.ts (TypeORM)
    import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
    
    @Entity('message_logs') // Specify table name
    export class MessageLog {
      @PrimaryGeneratedColumn('uuid')
      id: string;
    
      @Index() // Index for faster lookups
      @Column({ name: 'message_uuid', unique: true }) // Plivo's MessageUUID
      messageUuid: string;
    
      @Column()
      direction: 'inbound' | 'outbound';
    
      @Index()
      @Column({ name: 'from_number' })
      fromNumber: string;
    
      @Index()
      @Column({ name: 'to_number' })
      toNumber: string;
    
      @Column('text')
      text: string;
    
      @CreateDateColumn({ name: 'created_at' })
      createdAt: Date;
    
      // Add columns for status, error messages, media URLs etc. if needed
    }
  5. Create Repository/Service: Inject the TypeORM repository or Prisma client into your PlivoWebhookService (or a dedicated logging service) to save incoming messages and potentially retrieve conversation history.

  6. Migrations: Use TypeORM migrations (typeorm migration:generate, typeorm migration:run) or Prisma Migrate (prisma migrate dev, prisma migrate deploy) to manage schema changes safely.

7. Adding Security Features

  • Webhook Signature Validation (MANDATORY): This is the most critical security measure. Plivo signs its webhook requests using your Auth Token, allowing you to verify the request's authenticity and integrity.

    1. Create a Guard (src/plivo-webhook/plivo-signature.guard.ts):

      typescript
      // src/plivo-webhook/plivo-signature.guard.ts
      import {
        Injectable,
        CanActivate,
        ExecutionContext,
        Logger,
        ForbiddenException,
        RawBodyRequest, // Use RawBodyRequest type
      } from '@nestjs/common';
      import { ConfigService } from '@nestjs/config';
      import * as plivo from 'plivo';
      import { Observable } from 'rxjs';
      import { Request } from 'express'; // Import Request type
      
      @Injectable()
      export class PlivoSignatureGuard implements CanActivate {
        private readonly logger = new Logger(PlivoSignatureGuard.name);
      
        constructor(private configService: ConfigService) {}
      
        canActivate(
          context: ExecutionContext,
        ): boolean | Promise<boolean> | Observable<boolean> {
          const request = context.switchToHttp().getRequest<RawBodyRequest<Request>>(); // Get typed request with rawBody
      
          // Construct the full URL Plivo used to call the webhook
          // Note: Trusting proxy headers might be needed if behind a reverse proxy (e.g., X-Forwarded-Proto)
          // Consult NestJS documentation on `set('trust proxy', true)` if needed.
          const protocol = request.protocol; // http or https
          const host = request.get('host'); // e.g., your-ngrok-id.ngrok-free.app or your domain
          const originalUrl = request.originalUrl; // e.g., /plivo-webhook/message
          const url = `${protocol}://${host}${originalUrl}`;
      
          const signature = request.headers['x-plivo-signature-v3'] as string; // Use V3 signature
          const nonce = request.headers['x-plivo-signature-v3-nonce'] as string;
      
          // --- IMPORTANT: SMS vs Voice Signature Versions ---
          // According to Plivo documentation (as of 2025):
          // - Voice webhooks use: X-Plivo-Signature-V3, X-Plivo-Signature-V3-Nonce
          // - SMS/Messaging webhooks use: X-Plivo-Signature-V2, X-Plivo-Signature-V2-Nonce
          // This code uses V3. For SMS-only applications, you may need to:
          // 1. Use 'x-plivo-signature-v2' and 'x-plivo-signature-v2-nonce' headers instead
          // 2. Call the appropriate SDK validation method (check Plivo Node.js SDK docs)
          // Verify which headers your Plivo webhooks actually send by logging request.headers
      
          // --- CRITICAL: Raw Body Requirement ---
          // Access the raw body buffer attached by NestJS ({ rawBody: true })
          const rawBody = request.rawBody;
      
          if (!signature || !nonce) {
            this.logger.warn(`Missing Plivo signature headers. URL: ${url}`);
            throw new ForbiddenException('Missing Plivo signature headers');
          }
      
          // Ensure rawBody is available (should be if NestFactory was configured correctly)
          if (!rawBody) {
            this.logger.error(
              'Raw request body is not available. Ensure NestJS is configured with { rawBody: true } in NestFactory.create().',
            );
            // Throw ForbiddenException because validation cannot proceed.
            throw new ForbiddenException(
              'Server configuration error: Raw body unavailable for signature validation.',
            );
          }
      
          const authToken = this.configService.get<string>('PLIVO_AUTH_TOKEN');
          if (!authToken) {
            this.logger.error('PLIVO_AUTH_TOKEN is not configured!');
            // Avoid leaking info about missing token in the response
            throw new ForbiddenException('Server configuration error');
          }
      
          try {
            // Use the rawBody buffer directly for validation.
            const isValid = plivo.validateV3Signature(url, nonce, signature, authToken, rawBody);
      
            if (!isValid) {
              this.logger.warn(`Invalid Plivo signature. URL: ${url}, Nonce: ${nonce}, Sig: ${signature}`);
              throw new ForbiddenException('Invalid Plivo signature');
            }
      
            this.logger.log(`Plivo signature validated successfully for URL: ${url}`);
            return true; // Allow request to proceed
      
          } catch (error) {
            this.logger.error('Error validating Plivo signature:', error);
            throw new ForbiddenException('Signature validation failed');
          }
        }
      }
      • Critical Note on Raw Body: This guard relies entirely on request.rawBody. Ensure you have configured NestJS to provide it by passing { rawBody: true } to NestFactory.create in src/main.ts. Without the exact raw body bytes, signature validation will fail.
      • URL Construction: The guard constructs the full URL (protocol://host/originalUrl) that Plivo used to sign the request. Ensure this matches exactly, especially if running behind a reverse proxy (consider trust proxy settings).
      • Error Handling: Throws ForbiddenException on missing headers, missing token, missing raw body, or invalid signature, preventing unauthorized access.
    2. Apply the Guard: We already applied it in the controller using @UseGuards(PlivoSignatureGuard).

    3. Import/Provide: Ensure ConfigService is available (it is, via ConfigModule.forRoot({ isGlobal: true })) and that the guard is provided in the PlivoWebhookModule.

      typescript
      // src/plivo-webhook/plivo-webhook.module.ts
      import { Module } from '@nestjs/common';
      import { PlivoWebhookController } from './plivo-webhook.controller';
      import { PlivoWebhookService } from './plivo-webhook.service';
      import { PlivoSignatureGuard } from './plivo-signature.guard'; // Import the guard
      
      @Module({
        // imports: [ConfigModule], // Not needed if ConfigModule is global
        controllers: [PlivoWebhookController],
        providers: [
          PlivoWebhookService,
          PlivoSignatureGuard, // Provide the guard so NestJS can inject ConfigService
        ],
      })
      export class PlivoWebhookModule {}
  • Input Validation/Sanitization: While signature validation confirms the source, validate the content (payload.Text, payload.From, etc.) if you use it for logic beyond simple replies. Use NestJS ValidationPipe with class-validator DTOs or sanitization libraries (like class-sanitizer or manual checks) to prevent unexpected behavior or injection if storing/processing data.

  • Rate Limiting: Protect your endpoint from potential (though less likely if signature validation is robust) denial-of-service or accidental loops. Use @nestjs/throttler to limit requests per source IP or other criteria.

  • HTTPS: Always use HTTPS for your webhook endpoint in production. ngrok provides this for local testing. Ensure your production deployment server is configured with TLS/SSL certificates.

8. Handling Special Cases

  • Compliance Keywords (STOP/HELP): US/Canada regulations often require handling STOP (opt-out) and HELP (provide info) keywords. Plivo has features to manage opt-outs automatically at the platform level (check your account settings and Messaging Services features). If handling manually in your application:

    typescript
    // Inside PlivoWebhookService.handleIncomingSms
    
    // --- Modern Approach: Use Plivo's MessageIntent field (recommended) ---
    // Plivo automatically detects compliance keywords and sets MessageIntent field
    if (payload.MessageIntent === 'optout') {
      this.logger.log(`Received opt-out via MessageIntent from ${payload.From}.`);
      const response = new plivo.Response();
      // Return empty response if Plivo handles confirmation automatically
      return response.toXML();
    }
    
    if (payload.MessageIntent === 'help') {
      this.logger.log(`Received help request via MessageIntent from ${payload.From}.`);
      const response = new plivo.Response();
      const helpText = "Help: Reply STOP to unsubscribe. Msg&Data rates may apply. More info: your-support-url.com";
      response.addMessage(helpText, { src: payload.To, dst: payload.From });
      return response.toXML();
    }
    
    // --- Alternative: Manual keyword parsing (if MessageIntent not available) ---
    const textUpper = payload.Text.trim().toUpperCase();
    const response = new plivo.Response();
    const params = { // Define params for potential replies
      src: payload.To,
      dst: payload.From,
    };
    
    if (textUpper === 'STOP' || textUpper === 'UNSUBSCRIBE') {
      // Log opt-out, potentially update user record in DB to block future messages
      this.logger.log(`Received STOP keyword from ${payload.From}. Processing opt-out.`);
      // Respond with confirmation (optional, Plivo might handle this via Compliance settings)
      // const stopResponseText = "You have been unsubscribed. No more messages will be sent.";
      // response.addMessage(stopResponseText, params);
      // Return empty response if Plivo handles confirmation, otherwise add message above.
      return response.toXML();
    }
    
    if (textUpper === 'HELP' || textUpper === 'INFO') {
       // Respond with help information
       this.logger.log(`Received HELP keyword from ${payload.From}. Sending info.`);
       const helpText = "Help: Reply STOP to unsubscribe. Msg&Data rates may apply. More info: your-support-url.com";
       response.addMessage(helpText, params);
       return response.toXML();
    }
    
    // --- If not STOP/HELP, proceed with normal message handling ---
    const replyText = `Thanks for your message! You said: ""${payload.Text}""`;
    response.addMessage(replyText, params);
    const xmlResponse = response.toXML();
    this.logger.log(`Generated XML Response: ${xmlResponse}`);
    return xmlResponse;
  • MMS (Multimedia Messaging): If your Plivo number is MMS-enabled and you expect media:

    • Check the payload for Type=mms.
    • Look for Media_Count (number of media files) and Media_URL0, Media_URL1, etc.
    • Download media from the URLs if needed. Note that Plivo media URLs are temporary and may require authentication.
  • Long Messages (Concatenation): Plivo automatically handles concatenation for inbound messages exceeding single SMS limits. If your reply generated via XML might exceed character limits (160 GSM-7 chars / 70 UCS-2 chars), Plivo automatically splits it into multiple segments when sending. Be mindful of billing implications (each segment is billed as one SMS).

  • Encoding: Plivo typically uses GSM-7 encoding for standard characters and UCS-2 (Unicode) if special characters are present. Ensure your service handles text appropriately (Node.js/TypeScript generally handle Unicode well via UTF-8 internally).

9. Implementing Performance Optimizations

For a simple webhook like this, performance is rarely an issue initially. However, if handleIncomingSms involves slow operations (complex database queries, external API calls):

  • Asynchronous Processing: Acknowledge the Plivo webhook immediately with a 200 OK (e.g., return response.toXML() with no <Message> element) and then process the message asynchronously.
    1. Use a message queue system (like Redis with BullMQ, RabbitMQ, Kafka, or Google Pub/Sub / AWS SQS).
    2. The webhook handler (PlivoWebhookController) simply validates the request (signature guard) and adds a job to the queue containing the PlivoSmsPayload.
    3. A separate NestJS worker process (or microservice) listens to the queue, picks up jobs, and performs the slow logic (DB writes, external API calls, sending replies).
    4. Important: If processing is async, any reply cannot be sent via the initial XML response. You must use the Plivo Send Message API (e.g., plivo.Client().messages.create(...)) in your asynchronous worker process to send the reply as a separate outbound message.
  • Caching: If fetching common data frequently within the handler (e.g., user settings, templates), use caching (NestJS CacheModule, Redis, Memcached) to speed up lookups and reduce database load.

10. Adding Monitoring, Observability, and Analytics

  • Health Checks: Implement a health check endpoint (e.g., /health) using @nestjs/terminus. This endpoint should verify critical dependencies (database connection, Plivo API reachability if sending outbound messages). Monitor this endpoint externally (e.g., using UptimeRobot, Pingdom, or your cloud provider's monitoring).
  • Performance Metrics (APM): Integrate with Application Performance Monitoring (APM) tools (Datadog, New Relic, Dynatrace, OpenTelemetry). These tools automatically instrument your NestJS application to monitor request latency, throughput, error rates, and resource usage (CPU, memory). Track performance of the /plivo-webhook/message endpoint specifically.
  • Error Tracking: Use dedicated error tracking services like Sentry or Bugsnag. Integrate them via NestJS loggers or exception filters to capture, aggregate, alert on, and debug application errors in real-time.
  • Plivo Logs: Regularly check the Plivo Console (Messaging -> Logs) for detailed information on message delivery status (inbound and outbound), webhook delivery attempts, failures, and error codes from Plivo's perspective. This is crucial for debugging delivery issues.
  • Custom Metrics/Analytics: Log key business events (e.g., inbound_message_received, reply_sent, stop_keyword_received, help_keyword_received, processing_error) to your logging system or a dedicated analytics platform. Build dashboards (Grafana, Kibana, Datadog Dashboards) to visualize SMS activity, error trends, and user engagement.

11. Troubleshooting and Common Issues

  • Ngrok Session Expired: Free ngrok sessions expire after a few hours. You'll need to restart ngrok (which gives a new URL) and update the Plivo Application's Message URL in the Plivo Console. Consider a paid ngrok plan or deploying to a stable environment for persistent URLs.
  • Firewall Issues: If deploying to your own server, ensure your firewall allows incoming HTTPS traffic on the configured port (e.g., 3000 or 443 if behind a proxy) from Plivo's IP ranges. While signature validation is the primary security, network connectivity is still required. Plivo publishes its IP ranges.
  • Incorrect Plivo Config: Double-check:
    • The Message URL in the Plivo Application settings is exactly correct (HTTPS, full path: https://.../plivo-webhook/message).
    • The Method is set to POST.
    • The correct Plivo Number is linked to the correct Plivo Application.
  • Signature Validation Failures:
    • Verify PLIVO_AUTH_TOKEN in your .env file exactly matches the one on your Plivo Console dashboard.
    • Ensure the URL constructed within the PlivoSignatureGuard matches the URL Plivo is actually calling (check ngrok logs or server access logs). Pay attention to http vs https.
    • Raw Body Issue: This is the most common cause. Confirm { rawBody: true } is set in NestFactory.create and that request.rawBody is a Buffer containing the exact bytes Plivo sent. Any intermediate parsing or modification will invalidate the signature.
    • Verify X-Plivo-Signature-V3 and X-Plivo-Signature-V3-Nonce headers are being correctly received and extracted.
  • Body Parsing Issues: NestJS with { rawBody: true } typically handles application/x-www-form-urlencoded correctly, parsing it into @Body() while preserving rawBody. If Plivo were configured to send JSON, you'd need NestJS's JSON body parser, but signature validation still needs the raw JSON buffer.
  • STOP Keyword Blocking: If a user texts STOP, Plivo's platform-level compliance features may automatically block your Plivo number from sending further messages to that user's number. This is expected behavior for compliance. Your application might still receive the STOP message via webhook depending on settings.
  • Trial Account Limitations: Plivo trial accounts have restrictions:
    • Typically can only send messages to phone numbers verified in the Plivo Console (Sandbox Numbers).
    • Receiving messages from any number usually works, but check trial limitations.
    • May have rate limits or volume caps.
  • XML Errors: If your generated XML in the response is invalid (e.g., malformed tags, incorrect structure), Plivo will fail to process the reply instructions. Check your NestJS logs for the exact XML generated by response.toXML() and validate it using an XML validator if needed. Ensure the Content-Type: application/xml header is correctly set on the response.

12. Deployment and CI/CD

  1. Choose Deployment Target:

    • PaaS (Platform as a Service): Heroku, Render, Google App Engine, AWS Elastic Beanstalk. Often provide simpler deployment workflows for Node.js applications. Ensure the platform supports persistent WebSocket connections if needed for other features, and check how environment variables are managed.
    • Containers: Dockerize your NestJS application. Deploy the container image to orchestrators like Kubernetes (EKS, GKE, AKS), AWS ECS, Google Cloud Run, or managed container platforms. This offers more control and portability.
    • Serverless Functions: AWS Lambda, Google Cloud Functions, Azure Functions. Requires using an adapter like @nestjs/platform-fastify with @fastify/aws-lambda or @vendia/serverless-express to run NestJS in a serverless environment. Can be cost-effective but has cold start implications and different architectural considerations.
    • Virtual Machines (IaaS): AWS EC2, Google Compute Engine, Azure VMs. Provides maximum control but requires managing the OS, Node.js runtime, security patches, and potentially a reverse proxy (like Nginx or Caddy) yourself.
  2. Build for Production: Add a build script to your package.json:

    json
    // package.json
    "scripts": {
      // ... other scripts
      "build": "nest build",
      "start:prod": "node dist/main"
    }

    Run npm run build to transpile TypeScript to JavaScript in the dist folder.

  3. Environment Variables: Securely configure environment variables (PORT, PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_PHONE_NUMBER, database credentials, etc.) in your chosen deployment environment. Do not commit sensitive keys to your repository. Use the platform's secrets management (e.g., Heroku Config Vars, AWS Secrets Manager, GCP Secret Manager).

  4. Dockerfile (Example):

    dockerfile
    # Stage 1: Build the application
    FROM node:18-alpine AS builder
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install --only=production --ignore-scripts --prefer-offline
    COPY . .
    RUN npm run build
    
    # Stage 2: Create the final production image
    FROM node:18-alpine
    WORKDIR /usr/src/app
    # Copy only necessary files from builder stage
    COPY --from=builder /usr/src/app/node_modules ./node_modules
    COPY --from=builder /usr/src/app/dist ./dist
    # Copy package.json for runtime info if needed, but not for installing deps
    COPY package.json .
    
    # Expose the port the app runs on
    # Use an ENV var for flexibility, matching your .env or deployment config
    ARG PORT=3000
    ENV PORT=${PORT}
    EXPOSE ${PORT}
    
    # Command to run the application
    CMD ["node", "dist/main"]
  5. CI/CD Pipeline: Set up a Continuous Integration/Continuous Deployment pipeline (GitHub Actions, GitLab CI, Jenkins, CircleCI, AWS CodePipeline, Google Cloud Build):

    • Trigger: On push/merge to main branch.
    • CI Steps: Install dependencies, lint, run tests (unit, integration, e2e).
    • Build Step: Build the production application (npm run build) or build the Docker image.
    • CD Steps: Push Docker image to a registry (Docker Hub, ECR, GCR), deploy the new version to your chosen platform (e.g., update Heroku app, deploy to Kubernetes, update Lambda function).
  6. Update Plivo Message URL: Once deployed to a stable public URL (not ngrok), update the Message URL in your Plivo Application settings in the Plivo Console to point to your production endpoint (e.g., https://your-app-domain.com/plivo-webhook/message).

Frequently Asked Questions

how to receive sms messages in nestjs

Receive SMS messages in NestJS by setting up a webhook endpoint using Plivo. Configure Plivo to forward incoming messages to this endpoint, which will then process them using the Plivo Node.js SDK. This enables two-way messaging and allows your application to react to incoming SMS.

what is plivo used for in nestjs

Plivo is a cloud communications platform that provides SMS and Voice APIs, used in this NestJS application to handle inbound and outbound SMS messages. The Plivo Node.js SDK helps to build the XML responses required by Plivo and also to send outgoing messages if needed.

why use nestjs for sms applications

NestJS provides a structured, scalable, and efficient framework for building server-side applications, making it well-suited for production SMS APIs. Features like dependency injection, modularity, and TypeScript support enhance code maintainability and reliability.

when should I use ngrok with plivo

ngrok is essential for local development with Plivo webhooks. Since Plivo needs a publicly accessible URL, ngrok creates a tunnel to expose your local server, allowing Plivo to reach your application during testing before deploying to a live server.

how to set up plivo webhook in nestjs

Create a dedicated controller with a POST route (e.g., '/plivo-webhook/message') in your NestJS application. Configure this URL as the 'Message URL' in your Plivo application settings. This endpoint will then receive incoming SMS messages from Plivo.

how to validate plivo signature in nestjs

Use the PlivoSignatureGuard with the '@UseGuards' decorator on your webhook route. This guard utilizes the 'plivo.validateV3Signature' function and the 'PLIVO_AUTH_TOKEN' to verify that requests genuinely originate from Plivo and haven't been tampered with.

what is plivo message uuid

The Plivo MessageUUID is a unique identifier assigned to each incoming message by Plivo. Use this UUID for logging, tracking, and potentially storing messages in a database to avoid duplicates and maintain message history.

how to handle stop keyword in plivo sms

Handle 'STOP' keywords by checking the incoming message text. Implement logic to unsubscribe the user, potentially updating your database. The Plivo platform may also offer automatic opt-out management via Compliance settings.

how to send sms replies with plivo xml

Use the 'plivo.Response' object to construct XML containing '<Message>' elements. Set 'src' to your Plivo number and 'dst' to the sender's number. Return this XML from your webhook handler; Plivo will process it and send the reply.

how to configure environment variables for plivo

Create a '.env' file in your project root to store 'PLIVO_AUTH_ID', 'PLIVO_AUTH_TOKEN', and 'PLIVO_PHONE_NUMBER'. Import and configure the '@nestjs/config' module in your app.module.ts to load these variables securely. Never commit the '.env' file.

what is the purpose of raw body in nestjs plivo integration

The 'rawBody' option in NestFactory.create is crucial for Plivo signature validation. It ensures the guard receives the exact, unmodified request body bytes needed by 'plivo.validateV3Signature', which is essential for security.

how to handle mms messages with plivo webhook

For MMS messages, look for 'Type=mms' in the webhook payload. Access media via the provided 'Media_URL' parameters. Note that these URLs are temporary and may require authentication for download.

how to test plivo webhook locally

Use ngrok to expose your local development server. Configure your Plivo application's Message URL to point to the ngrok HTTPS URL plus your webhook route. Send a real SMS to your Plivo number to trigger the webhook.

why does plivo signature validation fail

Plivo signature validation can fail due to several reasons, including an incorrect 'PLIVO_AUTH_TOKEN', a mismatch between the constructed URL and the actual URL called by Plivo, especially when using a proxy, or most commonly issues with the raw body not containing the original request bytes.