Frequently Asked Questions
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.
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.
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.
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.
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.
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.
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.
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.
Use the 'plivo.Response' object to construct XML containing '' 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.
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.
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.
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.
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.
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.
Implementing Plivo Inbound SMS and Two-Way Messaging with NestJS
This guide provides a step-by-step walkthrough for building a production-ready NestJS application capable of receiving incoming SMS messages via Plivo webhooks and responding automatically, enabling two-way messaging. We will cover project setup, core implementation, security considerations, error handling, deployment, and testing.
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, bots, notification systems, and more.
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:
Architecture:
Prerequisites:
ngrok
installed globally or available in your PATH for local testing.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 the Project
Let's initialize our NestJS project and install the necessary dependencies.
Install NestJS CLI: If you don't have it_ install it globally:
Create NestJS Project:
Choose your preferred package manager (npm or yarn) when prompted.
Install Dependencies: We need the Plivo Node.js SDK and NestJS config module for environment variables.
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.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 thesrc
if sending replies via the API instead of XML response.Configure Environment Variables: Import and configure
ConfigModule
in your main application module (src/app.module.ts
) to load the.env
file.Update Main Entry Point: Modify
src/main.ts
to use the configured port and enable raw body parsing for signature validation.Note: Passing
{ rawBody: true }
toNestFactory.create
is essential for the Plivo signature validation guard to work correctly.Create Plivo Webhook Module: Generate a new module, controller, and service to handle Plivo logic.
This creates a
src/plivo-webhook
directory with the necessary files and updatessrc/app.module.ts
automatically (as seen in step 5).2. Implementing Core Functionality (Webhook Handler)
Now, let's build the controller and service to handle incoming Plivo webhooks.
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.PlivoSmsPayload
for type safety based on Plivo's webhook parameters.handleIncomingSms
method takes the parsed payload, logs it, and uses theplivo
SDK'sResponse
class to build the XML.response.addMessage(text, params)
creates the<Message>
element in the XML. Note howsrc
(source of reply) anddst
(destination of reply) are set.Plivo Webhook Controller (
src/plivo-webhook/plivo-webhook.controller.ts
): This controller defines the HTTP endpoint that Plivo will call.@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 ourmessage_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 beapplication/x-www-form-urlencoded
from Plivo) and map it to ourPlivoSmsPayload
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 theContent-Type
header on the response to tell Plivo we are sending back XML.@HttpCode(HttpStatus.OK)
: Ensures a200 OK
status code is sent on success. Plivo expects this.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 usingclass-validator
could be added if needed (e.g., ensuringFrom
andTo
are valid phone numbers).Testing this endpoint locally requires
ngrok
and sending an SMS, or usingcurl
/Postman to simulate Plivo's request. Simulating requires generating validX-Plivo-Signature-V3
andX-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).ngrok
and sending a real SMS.4. Integrating with Plivo (Configuration)
Now, let's configure Plivo to send incoming messages to our local NestJS application using
ngrok
.Start Your NestJS App:
Ensure it's running and listening on the correct port (e.g., 3000) and that the
rawBody: true
option is enabled inmain.ts
.Start ngrok: Open a new terminal window and expose your local port to the internet.
ngrok
will display forwarding URLs (e.g.,https://<unique-id>.ngrok-free.app
). Copy thehttps
URL.Create/Configure Plivo Application:
NestJS Inbound App
).ngrok
HTTPS URL, appending the controller route:https://<unique-id>.ngrok-free.app/plivo-webhook/message
.Link Plivo Number to Application:
NestJS Inbound App
).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 likewinston
.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 intry...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.src/main.ts
: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 a200 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):
npm install @nestjs/typeorm typeorm pg
..env
and configure theTypeOrmModule
inapp.module.ts
.MessageLog
) to represent your database tables/collections.PlivoWebhookService
(or a dedicated logging service) to save incoming messages and potentially retrieve conversation history.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.
Create a Guard (
src/plivo-webhook/plivo-signature.guard.ts
):request.rawBody
. Ensure you have configured NestJS to provide it by passing{ rawBody: true }
toNestFactory.create
insrc/main.ts
. Without the exact raw body bytes, signature validation will fail.protocol://host/originalUrl
) that Plivo used to sign the request. Ensure this matches exactly, especially if running behind a reverse proxy (considertrust proxy
settings).ForbiddenException
on missing headers, missing token, missing raw body, or invalid signature, preventing unauthorized access.Apply the Guard: We already applied it in the controller using
@UseGuards(PlivoSignatureGuard)
.Import/Provide: Ensure
ConfigService
is available (it is, viaConfigModule.forRoot({ isGlobal: true })
) and that the guard is provided in thePlivoWebhookModule
.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 NestJSValidationPipe
withclass-validator
DTOs or sanitization libraries (likeclass-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
STOP
(opt-out) andHELP
(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:payload
forType=mms
.Media_Count
(number of media files) andMedia_URL0
,Media_URL1
, etc.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):200 OK
(e.g., returnresponse.toXML()
with no<Message>
element) and then process the message asynchronously.PlivoWebhookController
) simply validates the request (signature guard) and adds a job to the queue containing thePlivoSmsPayload
.plivo.Client().messages.create(...)
) in your asynchronous worker process to send the reply as a separate outbound message.CacheModule
, Redis, Memcached) to speed up lookups and reduce database load.10. Adding Monitoring, Observability, and Analytics
/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)./plivo-webhook/message
endpoint specifically.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 Caveats
ngrok
sessions expire after a few hours. You'll need to restartngrok
(which gives a new URL) and update the Plivo Application's Message URL in the Plivo Console. Consider a paidngrok
plan or deploying to a stable environment for persistent URLs.https://.../plivo-webhook/message
).POST
.PLIVO_AUTH_TOKEN
in your.env
file exactly matches the one on your Plivo Console dashboard.PlivoSignatureGuard
matches the URL Plivo is actually calling (checkngrok
logs or server access logs). Pay attention tohttp
vshttps
.{ rawBody: true }
is set inNestFactory.create
and thatrequest.rawBody
is a Buffer containing the exact bytes Plivo sent. Any intermediate parsing or modification will invalidate the signature.X-Plivo-Signature-V3
andX-Plivo-Signature-V3-Nonce
headers are being correctly received and extracted.{ rawBody: true }
typically handlesapplication/x-www-form-urlencoded
correctly, parsing it into@Body()
while preservingrawBody
. 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 textsSTOP
, 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.response.toXML()
and validate it using an XML validator if needed. Ensure theContent-Type: application/xml
header is correctly set on the response.12. Deployment and CI/CD
Choose Deployment Target:
@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.Build for Production: Add a build script to your
package.json
:Run
npm run build
to transpile TypeScript to JavaScript in thedist
folder.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).Dockerfile (Example):
CI/CD Pipeline: Set up a Continuous Integration/Continuous Deployment pipeline (GitHub Actions, GitLab CI, Jenkins, CircleCI, AWS CodePipeline, Google Cloud Build):
npm run build
) or build the Docker image.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
).