How to Send Bulk SMS with Twilio Notify, NestJS & Node.js (2025 Guide)
Learn how to send bulk SMS to thousands of recipients using Twilio Notify, NestJS, and Node.js. Step-by-step tutorial with production-ready code for broadcast messaging, error handling, validation, and deployment.
Learn how to send bulk SMS messages to thousands of recipients using Twilio Notify, NestJS, and Node.js. This production-ready tutorial shows you how to build a broadcast messaging system that handles mass text messaging without rate limits, complete with error handling, validation, database integration, and deployment strategies.
Twilio Notify solves the challenge of efficiently sending identical messages to large recipient lists without managing complex looping logic or hitting API rate limits. This tutorial implements a NestJS REST API that accepts phone numbers and message content, then dispatches broadcasts through Twilio's optimized notification infrastructure.
What You'll Build
NestJS REST API endpoint for bulk SMS broadcasts
Twilio Notify service integration with proper error handling
Input validation using class-validator and DTOs
Environment-based configuration management
Database schema for recipient lists and message logs (conceptual)
By the end of this guide, you'll have a NestJS application with an API endpoint that accepts phone numbers and message content, then uses Twilio Notify to dispatch messages efficiently. The system includes validation, configuration management, error handling, logging, and guidance on testing and deployment.
Note: For production readiness, implement robust authentication/authorization (Section 8) and status callback webhooks (Section 10).
1. Project Setup and Configuration
Initialize your NestJS project and configure the basic structure.
Install NestJS CLI
Install the NestJS command-line interface globally if you haven't already.
Bash
npm install -g @nestjs/cli
Create New NestJS Project
Generate a new project using your preferred package manager (npm or yarn).
Bash
nest new nestjs-twilio-bulk-sms
cd nestjs-twilio-bulk-sms
Install Dependencies
Install the Twilio Node.js SDK and NestJS configuration module.
class-validator, class-transformer – Validates incoming request data using DTOs
Environment Variable Setup
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 if it's not already there.
.env
# .env
# Twilio Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_NOTIFY_SERVICE_SID=ISxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Optional: If using a specific Messaging Service for Notify
# TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Application Settings
PORT=3000
Configuration values:
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_NOTIFY_SERVICE_SID – Obtained in the next section
PORT – Port your NestJS application runs on
Configure NestJS ConfigModule
Import and configure the ConfigModule in your main application module (src/app.module.ts) to load variables from the .env file.
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule } from'@nestjs/config';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
// Import your messaging module later// import { MessagingModule } from './messaging/messaging.module';@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Make ConfigModule available globallyenvFilePath: '.env', // Specify the env file path
}),
// MessagingModule, // Uncomment once created
],
controllers: [AppController],
providers: [AppService],
})
exportclassAppModule {}
Enable ValidationPipe
Enable the global validation pipe in src/main.ts to automatically validate incoming request bodies based on DTOs.
TypeScript
// src/main.tsimport { NestFactory } from'@nestjs/core';
import { AppModule } from'./app.module';
import { ValidationPipe, HttpException, HttpStatus } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
asyncfunctionbootstrap() {
const app = awaitNestFactory.create(AppModule);
// Enable CORS if needed (adjust origins for production)
app.enableCors();
// Use global validation pipe
app.useGlobalPipes(
newValidationPipe({
whitelist: true, // Strip properties not defined in DTOtransform: true, // Automatically transform payloads to DTO instancesforbidNonWhitelisted: true, // Throw error if extra properties are sent
}),
);
// Get port from ConfigServiceconst configService = app.get(ConfigService);
const port = configService.get<number>('PORT') || 3000;
await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
Your basic project structure and configuration are now set up.
2. Twilio Account and Service Setup
Configure the necessary services within the Twilio console to obtain your credentials.
Goal: Obtain TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_NOTIFY_SERVICE_SID.
Get Account Credentials
Login to Twilio – Go to https://www.twilio.com/login and log in to your account
Locate credentials – On your main account dashboard (https://console.twilio.com/), find your ACCOUNT SID and AUTH TOKEN
Secure your token – Keep your AUTH TOKEN secure and treat it like a password
Update .env – Copy these values into your .env file
.env
# .env
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Paste your Account SID
TWILIO_AUTH_TOKEN=your_auth_token # Paste your Auth Token
Create a Messaging Service (Recommended)
While Notify can use your pool of numbers, use a dedicated Messaging Service for sender ID management, content intelligence, and scalability.
Navigate to Messaging → Services (https://console.twilio.com/us1/service/sms)
Click Create Messaging Service
Enter a friendly name (e.g., Bulk SMS Service)
Select a use case (e.g., Notifications, Marketing)
Click Create Messaging Service
Go to the Sender Pool section on the service's configuration page
Add one or more Twilio phone numbers (or Alphanumeric Sender IDs, Short Codes if configured on your account) to this service – you need at least one sender in the pool
Trial account note: Trial accounts can only send messages to verified phone numbers.
Copy the Messaging Service SID (starts with MG...) and add it to .env if needed
MessagingController – Handles incoming HTTP requests related to messaging (e.g., /messaging/bulk-sms), validates input, and calls the service
MessagingService – Contains the core business logic for interacting with the Twilio API, formats data, and makes API calls
MessagingModule – Bundles the controller and service together
4. Implementing the Core Logic (MessagingService)
Implement the service that interacts with Twilio Notify.
TypeScript
// src/messaging/messaging.service.tsimport { Injectable, Logger, InternalServerErrorException, BadRequestException, HttpException, HttpStatus } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
importTwiliofrom'twilio';
@Injectable()
exportclassMessagingService {
privatereadonly logger = newLogger(MessagingService.name);
privatetwilioClient: Twilio.Twilio;
privatenotifyServiceSid: string;
constructor(privateconfigService: ConfigService) {
const accountSid = this.configService.get<string>('TWILIO_ACCOUNT_SID');
const authToken = this.configService.get<string>('TWILIO_AUTH_TOKEN');
this.notifyServiceSid = this.configService.get<string>('TWILIO_NOTIFY_SERVICE_SID');
if (!accountSid || !authToken || !this.notifyServiceSid) {
this.logger.error('Twilio credentials or Notify Service SID missing in environment variables.');
thrownewInternalServerErrorException('Twilio configuration is incomplete.');
}
this.twilioClient = Twilio(accountSid, authToken);
this.logger.log('Twilio client initialized.');
}
/**
* Sends the same SMS message to multiple recipients using Twilio Notify.
* @paramrecipients - An array of phone numbers in E.164 format (e.g., '+15551234567').
* @parammessageBody - The text content of the SMS.
* @returns The SID of the Notification resource created.
* @throws BadRequestException if the recipients array is empty.
* @throws InternalServerErrorException if the message sending fails for other reasons.
* @throws HttpException for specific errors like rate limiting.
*/asyncsendBulkSms(recipients: string[], messageBody: string): Promise<string> {
if (!recipients || recipients.length === 0) {
this.logger.warn('Attempted to send bulk SMS with no recipients.');
thrownewBadRequestException('No recipients provided.');
}
// Format recipients for Twilio Notify bindings// See: https://www.twilio.com/docs/notify/api/notification-resource#create-a-notification-resourceconst bindings = recipients.map(number => {
returnJSON.stringify({ binding_type: 'sms', address: number });
});
this.logger.log(`Sending bulk SMS via Notify to ${recipients.length} recipients.`);
try {
const notification = awaitthis.twilioClient.notify
.services(this.notifyServiceSid)
.notifications.create({
toBinding: bindings,
body: messageBody,
});
this.logger.log(`Successfully created Notification resource: ${notification.sid}`);
return notification.sid;
} catch (error: any) {
this.logger.error(`Failed to send bulk SMS via Twilio Notify: ${error.message}`, error.stack);
// Handle specific Twilio error codes/statusif (error.code === 20003) {
thrownewInternalServerErrorException('Twilio authentication failed. Check credentials.');
} elseif (error.status === 429) {
this.logger.warn('Twilio rate limit hit. Consider implementing retry logic or queuing.');
thrownewHttpException('Rate limit exceeded', HttpStatus.TOO_MANY_REQUESTS);
} elseif (error.code === 21211 || error.code === 21604) {
thrownewBadRequestException(`Invalid parameter provided to Twilio: ${error.message}`);
}
// Default generic errorthrownewInternalServerErrorException(`Failed to send bulk SMS. Error: ${error.message}`);
}
}
// --- Add other messaging methods here (e.g., sendSingleSms) if needed ---
}
Key Implementation Details
Dependencies:
Injects ConfigService to read environment variables
Uses Logger for application logging
Imports exception classes for proper error handling
Constructor:
Retrieves Twilio credentials (ACCOUNT_SID, AUTH_TOKEN) and NOTIFY_SERVICE_SID from configuration
Validates these values are present, throwing an error if missing
Initializes the Twilio client using the credentials
sendBulkSms Method:
Accepts an array of recipients (phone numbers in E.164 format, e.g., +15551234567) and the messageBody
Input Validation: Checks if the recipients array is empty and throws a BadRequestException if it is
Binding Formatting: Maps recipient phone numbers into the JSON structure required by the toBinding parameter ({ "binding_type": "sms", "address": number })
API Call: Uses the initialized twilioClient to call client.notify.services(SERVICE_SID).notifications.create()
toBinding – Formatted array of recipient bindings
body – Message content
Logging: Logs success and includes the Notification SID returned by Twilio for tracking
Error Handling: Catches errors and throws specific exceptions based on Twilio error codes:
20003 – Authentication error
429 – Rate limit exceeded
21211/21604 – Invalid numbers/parameters
Default – Generic server error
5. Building the API Layer (MessagingController)
Expose the sendBulkSms functionality via a REST API endpoint.
Create Data Transfer Object (DTO)
Define a DTO to specify the expected shape and validation rules for the incoming request body.
TypeScript
// src/messaging/dto/send-bulk-sms.dto.tsimport { IsArray, IsString, IsNotEmpty, ArrayNotEmpty, IsPhoneNumber } from'class-validator';
// Remove swagger imports if not used, or keep if using Swagger// import { ApiProperty } from '@nestjs/swagger';exportclassSendBulkSmsDto {
@IsArray()
@ArrayNotEmpty()
@IsPhoneNumber(null, { each: true, message: 'Each recipient must be a valid phone number in E.164 format (e.g., +15551234567)' })
// @ApiProperty({// description: 'An array of recipient phone numbers in E.164 format.',// example: ['+15551234567', '+447700900123'],// type: [String],// })recipients: string[];
@IsString()
@IsNotEmpty()
// @ApiProperty({// description: 'The text content of the SMS message.',// example: 'Hello from our NestJS app!',// })message: string;
}
Validation Rules
The DTO uses class-validator decorators to enforce rules:
Decorator
Purpose
@IsArray, @ArrayNotEmpty
Ensures recipients is a non-empty array
@IsPhoneNumber(null, { each: true })
Validates each string in the recipients array is a valid phone number using libphonenumber-js. The null parameter accepts any country code when numbers use E.164 format
@IsString, @IsNotEmpty
Ensures message is a non-empty string
Swagger Integration (Optional): Uncomment the @ApiProperty decorators if you integrate Swagger for API documentation.
Implement the Controller
Create the endpoint in MessagingController.
TypeScript
// src/messaging/messaging.controller.tsimport { Controller, Post, Body, UsePipes, ValidationPipe, Logger, HttpCode, HttpStatus } from'@nestjs/common';
import { MessagingService } from'./messaging.service';
import { SendBulkSmsDto } from'./dto/send-bulk-sms.dto';
// Remove swagger imports if not used, or keep if using Swagger// import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';// @ApiTags('Messaging') // Group endpoints in Swagger UI@Controller('messaging')
exportclassMessagingController {
privatereadonly logger = newLogger(MessagingController.name);
constructor(privatereadonlymessagingService: MessagingService) {}
@Post('bulk-sms')
@HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as the process is async// @ApiOperation({ summary: 'Send a bulk SMS message via Twilio Notify' })// @ApiResponse({ status: HttpStatus.ACCEPTED, description: 'Request accepted, notification creation initiated.' })// @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid input data (e.g., missing fields, invalid phone numbers, empty recipients array).' })// @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Failed to initiate bulk SMS due to server or Twilio error.' })@UsePipes(newValidationPipe({ transform: true, whitelist: true }))
asyncsendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto): Promise<{ message: string; notificationSid?: string }> {
this.logger.log(`Received request to send bulk SMS to ${sendBulkSmsDto.recipients.length} recipients.`);
const { recipients, message } = sendBulkSmsDto;
try {
const notificationSid = awaitthis.messagingService.sendBulkSms(recipients, message);
return {
message: 'Bulk SMS request accepted by Twilio Notify.',
notificationSid: notificationSid,
};
} catch (error) {
this.logger.error(`Error processing bulk SMS request: ${error instanceofError ? error.message : String(error)}`, error instanceofError ? error.stack : undefined);
throw error;
}
}
}
Controller Details
Dependencies:
Injects MessagingService
Uses Logger for request logging
Routing:
@Controller('messaging') sets the base path to /messaging
@Post('bulk-sms') defines a POST endpoint at /messaging/bulk-sms
@HttpCode(HttpStatus.ACCEPTED) sets the default success status code to 202 Accepted (appropriate for asynchronous processes)
Request Processing:
@Body() sendBulkSmsDto: SendBulkSmsDto binds the incoming JSON request body to the DTO
ValidationPipe automatically validates the object – validation failures return 400 Bad Request
Logs the incoming request with recipient count
Destructures validated recipients and message from the DTO
Calls messagingService.sendBulkSms
Returns a success response containing a message and the notificationSid
Catches and re-throws errors for NestJS exception filters to handle
Setup Swagger (Optional)
For API documentation, install Swagger:
Bash
npm install @nestjs/swagger swagger-ui-express
# or
yarn add @nestjs/swagger swagger-ui-express
Access interactive API documentation at /api-docs when you run the app. Remember to uncomment the @ApiTags, @ApiOperation, @ApiResponse, and @ApiProperty decorators in the controller and DTO.
6. Database Integration (Conceptual)
This section outlines the conceptual steps for database integration using Prisma. A full implementation is beyond the scope of this core guide but represents a typical next step for production applications.
Real-world applications often need to:
Store Recipient Lists – Manage groups of users
Log Message Status – Track broadcast status (e.g., initiated, completed, failed)
Register PrismaService in the modules where it's needed (e.g., MessagingModule) and ensure PrismaModule is created and imported globally or locally.
Use in Messaging Service
Modify MessagingService to:
Accept a list ID and fetch recipients from the database using PrismaService
Log the broadcast details to BulkMessageLog after initiating the send
TypeScript
// Example modification in MessagingServiceimport { PrismaService } from'../prisma/prisma.service'; // Adjust path as needed// ... inside MessagingService classconstructor(privateconfigService: ConfigService,
privateprisma: PrismaService, // Inject PrismaService) {
// ... Twilio setup
}
// Example method using DB lookup (conceptual)asyncsendBulkSmsToList(listId: string, messageBody: string): Promise<string> {
// 1. Fetch recipients from DB based on listIdconst recipientsData = awaitthis.prisma.recipient.findMany({
// where: { recipientListId: listId }, // Example filter if using listsselect: { phoneNumber: true }
});
const recipients = recipientsData.map(r => r.phoneNumber);
if (recipients.length === 0) {
thrownewBadRequestException(`No recipients found for list ID: ${listId}`);
}
// 2. Call existing logic to send SMSconst notificationSid = awaitthis.sendBulkSms(recipients, messageBody);
// 3. Log the attempttry {
awaitthis.prisma.bulkMessageLog.create({
data: {
twilioNotifySid: notificationSid,
messageBody: messageBody,
recipientCount: recipients.length,
status: 'initiated',
},
});
this.logger.log(`Logged bulk message initiation for SID: ${notificationSid}`);
} catch (dbError) {
this.logger.error(`Failed to log bulk message initiation for SID ${notificationSid}: ${dbError instanceofError ? dbError.message : String(dbError)}`, dbError instanceofError ? dbError.stack : undefined);
// Decide how to handle logging failure: log the error and continue,// or throw an error if logging is critical.
}
return notificationSid;
}
Reminder: This is conceptual. A production system requires building out list management APIs and integrating status updates (likely via webhooks, see Section 10).
7. Error Handling, Logging, and Retry Mechanisms
Production systems need robust error handling and logging.
Error Handling
Error Type
Handling Method
HTTP Status
Validation Errors
ValidationPipe (automatic)
400 Bad Request
Empty Recipient Errors
MessagingService throws BadRequestException
400 Bad Request
Twilio API Errors
try...catch block in MessagingService checks specific error.code and error.status values
NestJS's built-in Logger logs to the console by default.
Production Logging: Integrate a robust logging solution (e.g., Winston, Pino) and configure it to:
Log to files or external services (Datadog, Sentry, ELK stack)
Use JSON format for easier parsing
Include context (request ID, user ID if applicable)
Adjust log levels (debug, info, warn, error)
Key Logging Points:
Location
Event
Log Level
MessagingController
Incoming requests
info
MessagingService constructor
Configuration loading/validation
info/error
MessagingService
Initiation of bulk send with recipient count
info
MessagingService
Successful notification creation with SID
info
MessagingService
Errors during Twilio API calls with details
error
Database operations
Logging successes/failures
info/error
MessagingService
Empty recipient list warnings
warn
Retry Mechanisms
Twilio API calls can fail transiently (network issues, temporary Twilio hiccups, rate limits – 429).
Considerations:
Idempotency: Twilio Notify's create operation is generally not idempotent. Retrying the samecreate call might result in duplicate broadcasts.
Strategy: Instead of retrying the create call directly for transient errors, consider:
Queuing – Implement a background job queue (e.g., BullMQ, RabbitMQ). If the initial API call fails transiently, place the job in the queue to retry later with exponential backoff.
Client-Side Retry – The client calling your API can implement retries if they receive a 5xx or 429 response.
Status Check & Resend – If a call fails, log the failure. A separate process can later check the status of intended recipients (if tracked) and attempt to resend only to those who didn't receive the message (more complex).
Simple Retry (Use with Caution): If you must implement a simple retry within the service (e.g., for immediate network errors), use a library like async-retry or implement carefully with backoff. Avoid retrying for non-transient errors (auth, bad parameters) or potentially duplicate-creating operations like Notify create unless you have specific logic to handle it.
Queue-Based Retry (Recommended)
TypeScript
// Conceptual retry logic using a queue (requires setting up BullMQ or similar)// In MessagingController:// constructor(// private readonly messagingService: MessagingService,// @InjectQueue('message-queue') private messageQueue: Queue, // Inject queue// ) {}// async sendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto) {// this.logger.log(`Queueing request to send bulk SMS to ${sendBulkSmsDto.recipients.length} recipients.`);// await this.messageQueue.add('send-bulk-sms-job', {// recipients: sendBulkSmsDto.recipients,// message: sendBulkSmsDto.message,// }, {// attempts: 3, // Max attempts// backoff: { type: 'exponential', delay: 1000 }, // Exponential backoff// });// return { message: 'Bulk SMS request queued for processing.' };// // HttpStatus remains 202 Accepted// }// Then create a Queue Processor that calls messagingService.sendBulkSms
For production, use a dedicated job queue – it's the most robust approach for handling retries of potentially non-idempotent operations like sending notifications.
Frequently Asked Questions About Bulk SMS with Twilio Notify
What is Twilio Notify and how does it differ from the Twilio SMS API?
Twilio Notify is a specialized service designed for sending identical notifications to large recipient lists across multiple channels (SMS, push notifications, email). Unlike the standard Twilio SMS API that requires individual API calls per recipient, Notify accepts a single request with multiple bindings and handles message distribution efficiently, avoiding rate limits and complex looping logic. Use Notify for bulk broadcasts and the SMS API for individual, personalized messages.
How many recipients can I send to in a single Twilio Notify request?
Twilio Notify supports sending to up to 10,000 recipients in a single API call via the toBinding parameter. For larger audiences, split your recipient list into batches of 10,000 and make multiple Notify requests. Each request returns a unique Notification SID for tracking delivery status.
Do I need a Twilio Messaging Service to use Notify?
Yes, Twilio Notify requires a Messaging Service with at least one phone number in the sender pool. The Messaging Service (SID starting with MG) provides sender ID management, content intelligence, and scalability features. Configure your Notify Service to use the Messaging Service through the Twilio Console under the SMS channel settings.
How do I validate phone numbers in E.164 format with NestJS?
Use the @IsPhoneNumber(null, { each: true }) decorator from class-validator in your DTO. This validates each recipient phone number using libphonenumber-js. The null parameter accepts any country code when numbers use E.164 format (e.g., +15551234567 for US, +447700900123 for UK). The { each: true } option validates every element in the recipients array.
What HTTP status code should my bulk SMS endpoint return?
Return 202 Accepted rather than 200 OK because Twilio Notify processes messages asynchronously. The API call initiates the broadcast but doesn't guarantee immediate delivery to all recipients. A 202 status correctly indicates the server accepted the request for processing without confirming completion.
How do I handle Twilio API rate limits in production?
Implement a job queue (BullMQ, RabbitMQ, or AWS SQS) to handle requests exceeding Twilio's rate limits. When you receive a 429 Too Many Requests error, queue the failed request for retry with exponential backoff. The error handling code in this guide detects error.status === 429 and throws an HttpException that your queue processor can catch and reschedule.
Can I track individual message delivery status with Notify?
Yes, but it requires additional configuration. Implement status callback webhooks by configuring your Messaging Service's callback URL to receive delivery receipts for each message. Create a webhook endpoint in your NestJS application that processes MessageStatus events from Twilio, then update your database with delivery statuses (sent, delivered, failed, undelivered).
Should I store Twilio credentials in environment variables or a secrets manager?
For development, use .env files with the @nestjs/config module. For production, use a secrets manager like AWS Secrets Manager, HashiCorp Vault, or Google Secret Manager. Never commit credentials to version control. The NestJS ConfigService can integrate with secrets managers through custom configuration loaders.
How do I implement authentication for my bulk SMS API endpoint?
Add NestJS Guards for authentication. Implement JWT-based authentication using @nestjs/passport and passport-jwt, or API key authentication with a custom guard. Apply guards to your controller with @UseGuards(JwtAuthGuard) or globally in main.ts. Always validate the authenticated user has permission to send bulk messages before processing requests.
What's the best way to test Twilio Notify integration without sending real SMS?
Use Twilio's test credentials for unit tests or mock the Twilio client in your service tests. For integration testing, purchase a Twilio phone number and send messages to verified numbers on your account (trial accounts can only send to verified numbers). Alternatively, use Twilio's webhook testing tools like twilio-run or ngrok to test callbacks locally without actual SMS delivery.
Next Steps for Your Bulk SMS System
Enhance your bulk SMS broadcast system with these production-ready features:
Implement Authentication & Authorization – Add JWT-based authentication with @nestjs/passport and role-based access control to restrict bulk messaging to authorized users
Set Up Status Webhooks – Configure Twilio callback URLs to receive delivery receipts and update your database with real-time message status
Add Recipient List Management – Build CRUD endpoints for managing recipient groups, importing CSV files, and handling opt-outs with proper consent tracking
Integrate Job Queues – Implement BullMQ or RabbitMQ to handle large broadcast jobs asynchronously with retry logic and progress tracking
Implement Rate Limiting – Add @nestjs/throttler to protect your API from abuse and prevent excessive Twilio charges
Set Up Monitoring & Alerts – Integrate Datadog, Sentry, or New Relic to track API performance, error rates, and Twilio spending
Add Message Scheduling – Allow users to schedule broadcasts for future delivery with timezone support using job queues with delayed execution
Build Analytics Dashboard – Track metrics like delivery rates, failed messages, recipient engagement, and cost per message
Your NestJS bulk SMS system is now ready for production deployment with proper error handling, validation, and scalability considerations.
Frequently asked questions
Use the Twilio Notify service with the Twilio Node.js SDK and a backend framework like NestJS. This setup allows you to send the same SMS message to many recipients without complex client-side looping or API rate limit issues, as the article details. You'll need a Twilio account, Node.js, and basic knowledge of REST APIs and TypeScript.
Twilio Notify is a service specifically designed for sending bulk notifications, including SMS messages, to large groups of recipients. It simplifies the process and handles complexities like API rate limits, making it ideal for broadcast scenarios as described in the article. You'll need a Notify Service SID to use it within your application.
NestJS provides a robust, structured framework for building scalable server-side applications in Node.js. Its modular architecture, dependency injection, and built-in features like validation pipes streamline the development process for the bulk SMS application as shown in the article. It is well-suited for handling API requests and managing complex logic like Twilio integration.
First, log into your Twilio account. Then, retrieve your Account SID and Auth Token from the dashboard. Optionally, create a Messaging Service for better sender ID management, and finally create a Notify Service, ensuring the SMS channel is enabled and linked to your messaging service or a Twilio phone number. These values will be required within your .env file, as covered in the article.
Twilio Messaging Services provide a way to group phone numbers or Alphanumeric Sender IDs and configure additional messaging features. Using a Messaging Service is best practice, especially for bulk messaging with Twilio Notify. The article recommends this for features like sender ID management and content intelligence, but it may not be strictly required depending on your Notify configuration.
Install the required Prisma packages, initialize Prisma using npx prisma init, define your data models in schema.prisma, apply migrations using npx prisma migrate dev, generate the Prisma Client, create a Prisma service, and inject it into your NestJS modules. This allows database operations like storing recipient lists and logging message statuses.
Consider using a database when you need to manage lists of recipients, store message logs, track delivery statuses, or implement other features beyond a simple one-off bulk send. The article suggests using a database like PostgreSQL with Prisma or TypeORM as your application scales and requires persistent data storage.
Class-validator provides decorators for implementing validation rules in DTOs (Data Transfer Objects). In the bulk SMS application, this ensures the incoming request data is in the correct format and non-empty, improving application security and preventing unexpected behavior.
Wrap Twilio API calls in a try...catch block and handle specific error codes and HTTP statuses returned by Twilio. The article recommends checking for common Twilio error codes (e.g. 20003 for auth, 429 for rate limits) or statuses like 429 and 21211 and throwing appropriate exceptions in NestJS (e.g., BadRequestException, HttpException, InternalServerErrorException) based on those codes and statuses.
E.164 is an international standard format for phone numbers (e.g., +15551234567). It ensures consistent formatting across regions and is required by Twilio for accurate message delivery. Using this standard format from the start improves compatibility and reduces issues, as described in the article.
Yes, Docker simplifies deployment by containerizing your application and its dependencies. While optional, the article mentions Docker as a good practice for consistent environments across development, testing, and production. A basic understanding of Docker is beneficial for using this option.
Due to idempotency concerns with Twilio Notify's create operation, it's better to use a message queue (like BullMQ or RabbitMQ). If the initial API call fails transiently, add the task to the queue for retry with exponential backoff, rather than immediately retrying notifications.create. The article covers some considerations regarding idempotency and simple retry strategies for immediate network errors if you must.
Learn how to send bulk SMS messages to thousands of recipients using Twilio Notify, NestJS, and Node.js. This production-ready tutorial shows you how to build a broadcast messaging system that handles mass text messaging without rate limits, complete with error handling, validation, database integration, and deployment strategies.
Twilio Notify solves the challenge of efficiently sending identical messages to large recipient lists without managing complex looping logic or hitting API rate limits. This tutorial implements a NestJS REST API that accepts phone numbers and message content, then dispatches broadcasts through Twilio's optimized notification infrastructure.
What You'll Build
Prerequisites
Technologies Used
System Architecture
Final Outcome
By the end of this guide, you'll have a NestJS application with an API endpoint that accepts phone numbers and message content, then uses Twilio Notify to dispatch messages efficiently. The system includes validation, configuration management, error handling, logging, and guidance on testing and deployment.
Note: For production readiness, implement robust authentication/authorization (Section 8) and status callback webhooks (Section 10).
1. Project Setup and Configuration
Initialize your NestJS project and configure the basic structure.
Install NestJS CLI
Install the NestJS command-line interface globally if you haven't already.
Create New NestJS Project
Generate a new project using your preferred package manager (npm or yarn).
nest new nestjs-twilio-bulk-sms cd nestjs-twilio-bulk-smsInstall Dependencies
Install the Twilio Node.js SDK and NestJS configuration module.
npm install twilio @nestjs/config class-validator class-transformer # or yarn add twilio @nestjs/config class-validator class-transformerDependencies:
twilio– Official Twilio helper library@nestjs/config– Manages environment variables securelyclass-validator,class-transformer– Validates incoming request data using DTOsEnvironment Variable Setup
Create a
.envfile in the project root for storing sensitive credentials and configuration. Never commit this file to version control. Add.envto your.gitignorefile if it's not already there.Configuration values:
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_NOTIFY_SERVICE_SID– Obtained in the next sectionPORT– Port your NestJS application runs onConfigure NestJS ConfigModule
Import and configure the
ConfigModulein your main application module (src/app.module.ts) to load variables from the.envfile.// 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 your messaging module later // import { MessagingModule } from './messaging/messaging.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make ConfigModule available globally envFilePath: '.env', // Specify the env file path }), // MessagingModule, // Uncomment once created ], controllers: [AppController], providers: [AppService], }) export class AppModule {}Enable ValidationPipe
Enable the global validation pipe in
src/main.tsto automatically validate incoming request bodies based on DTOs.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, HttpException, HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Enable CORS if needed (adjust origins for production) app.enableCors(); // Use global validation pipe app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO transform: true, // Automatically transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if extra properties are sent }), ); // Get port from ConfigService const configService = app.get(ConfigService); const port = configService.get<number>('PORT') || 3000; await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap();Your basic project structure and configuration are now set up.
2. Twilio Account and Service Setup
Configure the necessary services within the Twilio console to obtain your credentials.
Goal: Obtain
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN, andTWILIO_NOTIFY_SERVICE_SID.Get Account Credentials
https://www.twilio.com/loginand log in to your accounthttps://console.twilio.com/), find yourACCOUNT SIDandAUTH TOKENAUTH TOKENsecure and treat it like a password.envfileCreate a Messaging Service (Recommended)
While Notify can use your pool of numbers, use a dedicated Messaging Service for sender ID management, content intelligence, and scalability.
https://console.twilio.com/us1/service/sms)Bulk SMS Service)Notifications,Marketing)Trial account note: Trial accounts can only send messages to verified phone numbers.
MG...) and add it to.envif neededCreate a Notify Service
This is the core service for bulk messaging.
https://console.twilio.com/us1/develop/notify/services) – search forNotifyor enable it if neededApp Broadcasts)IS...) displayed on the service's page.envfileYou now have all the necessary Twilio credentials and service SIDs configured in your
.envfile.3. NestJS Messaging Module
Create a dedicated module in NestJS to handle all messaging-related logic.
Generate the Module, Service, and Controller
Use the NestJS CLI to generate the necessary files.
nest generate module messaging nest generate service messaging --no-spec # --no-spec skips test file for brevity nest generate controller messaging --no-specThis creates:
src/messaging/messaging.module.tssrc/messaging/messaging.service.tssrc/messaging/messaging.controller.tsRegister the Module
Import and add
MessagingModuleto theimportsarray insrc/app.module.ts.// 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 { MessagingModule } from './messaging/messaging.module'; // Import @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), MessagingModule, // Add MessagingModule here ], controllers: [AppController], providers: [AppService], }) export class AppModule {}Structure Overview
MessagingController– Handles incoming HTTP requests related to messaging (e.g.,/messaging/bulk-sms), validates input, and calls the serviceMessagingService– Contains the core business logic for interacting with the Twilio API, formats data, and makes API callsMessagingModule– Bundles the controller and service together4. Implementing the Core Logic (MessagingService)
Implement the service that interacts with Twilio Notify.
// src/messaging/messaging.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException, HttpException, HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Twilio from 'twilio'; @Injectable() export class MessagingService { private readonly logger = new Logger(MessagingService.name); private twilioClient: Twilio.Twilio; private notifyServiceSid: string; constructor(private configService: ConfigService) { const accountSid = this.configService.get<string>('TWILIO_ACCOUNT_SID'); const authToken = this.configService.get<string>('TWILIO_AUTH_TOKEN'); this.notifyServiceSid = this.configService.get<string>('TWILIO_NOTIFY_SERVICE_SID'); if (!accountSid || !authToken || !this.notifyServiceSid) { this.logger.error('Twilio credentials or Notify Service SID missing in environment variables.'); throw new InternalServerErrorException('Twilio configuration is incomplete.'); } this.twilioClient = Twilio(accountSid, authToken); this.logger.log('Twilio client initialized.'); } /** * Sends the same SMS message to multiple recipients using Twilio Notify. * @param recipients - An array of phone numbers in E.164 format (e.g., '+15551234567'). * @param messageBody - The text content of the SMS. * @returns The SID of the Notification resource created. * @throws BadRequestException if the recipients array is empty. * @throws InternalServerErrorException if the message sending fails for other reasons. * @throws HttpException for specific errors like rate limiting. */ async sendBulkSms(recipients: string[], messageBody: string): Promise<string> { if (!recipients || recipients.length === 0) { this.logger.warn('Attempted to send bulk SMS with no recipients.'); throw new BadRequestException('No recipients provided.'); } // Format recipients for Twilio Notify bindings // See: https://www.twilio.com/docs/notify/api/notification-resource#create-a-notification-resource const bindings = recipients.map(number => { return JSON.stringify({ binding_type: 'sms', address: number }); }); this.logger.log(`Sending bulk SMS via Notify to ${recipients.length} recipients.`); try { const notification = await this.twilioClient.notify .services(this.notifyServiceSid) .notifications.create({ toBinding: bindings, body: messageBody, }); this.logger.log(`Successfully created Notification resource: ${notification.sid}`); return notification.sid; } catch (error: any) { this.logger.error(`Failed to send bulk SMS via Twilio Notify: ${error.message}`, error.stack); // Handle specific Twilio error codes/status if (error.code === 20003) { throw new InternalServerErrorException('Twilio authentication failed. Check credentials.'); } else if (error.status === 429) { this.logger.warn('Twilio rate limit hit. Consider implementing retry logic or queuing.'); throw new HttpException('Rate limit exceeded', HttpStatus.TOO_MANY_REQUESTS); } else if (error.code === 21211 || error.code === 21604) { throw new BadRequestException(`Invalid parameter provided to Twilio: ${error.message}`); } // Default generic error throw new InternalServerErrorException(`Failed to send bulk SMS. Error: ${error.message}`); } } // --- Add other messaging methods here (e.g., sendSingleSms) if needed --- }Key Implementation Details
Dependencies:
ConfigServiceto read environment variablesLoggerfor application loggingConstructor:
ACCOUNT_SID,AUTH_TOKEN) andNOTIFY_SERVICE_SIDfrom configurationTwilioclient using the credentialssendBulkSmsMethod:recipients(phone numbers in E.164 format, e.g.,+15551234567) and themessageBodyBadRequestExceptionif it istoBindingparameter ({ "binding_type": "sms", "address": number })twilioClientto callclient.notify.services(SERVICE_SID).notifications.create()toBinding– Formatted array of recipient bindingsbody– Message content20003– Authentication error429– Rate limit exceeded21211/21604– Invalid numbers/parameters5. Building the API Layer (MessagingController)
Expose the
sendBulkSmsfunctionality via a REST API endpoint.Create Data Transfer Object (DTO)
Define a DTO to specify the expected shape and validation rules for the incoming request body.
// src/messaging/dto/send-bulk-sms.dto.ts import { IsArray, IsString, IsNotEmpty, ArrayNotEmpty, IsPhoneNumber } from 'class-validator'; // Remove swagger imports if not used, or keep if using Swagger // import { ApiProperty } from '@nestjs/swagger'; export class SendBulkSmsDto { @IsArray() @ArrayNotEmpty() @IsPhoneNumber(null, { each: true, message: 'Each recipient must be a valid phone number in E.164 format (e.g., +15551234567)' }) // @ApiProperty({ // description: 'An array of recipient phone numbers in E.164 format.', // example: ['+15551234567', '+447700900123'], // type: [String], // }) recipients: string[]; @IsString() @IsNotEmpty() // @ApiProperty({ // description: 'The text content of the SMS message.', // example: 'Hello from our NestJS app!', // }) message: string; }Validation Rules
The DTO uses
class-validatordecorators to enforce rules:@IsArray,@ArrayNotEmptyrecipientsis a non-empty array@IsPhoneNumber(null, { each: true })recipientsarray is a valid phone number usinglibphonenumber-js. Thenullparameter accepts any country code when numbers use E.164 format@IsString,@IsNotEmptymessageis a non-empty stringSwagger Integration (Optional): Uncomment the
@ApiPropertydecorators if you integrate Swagger for API documentation.Implement the Controller
Create the endpoint in
MessagingController.// src/messaging/messaging.controller.ts import { Controller, Post, Body, UsePipes, ValidationPipe, Logger, HttpCode, HttpStatus } from '@nestjs/common'; import { MessagingService } from './messaging.service'; import { SendBulkSmsDto } from './dto/send-bulk-sms.dto'; // Remove swagger imports if not used, or keep if using Swagger // import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; // @ApiTags('Messaging') // Group endpoints in Swagger UI @Controller('messaging') export class MessagingController { private readonly logger = new Logger(MessagingController.name); constructor(private readonly messagingService: MessagingService) {} @Post('bulk-sms') @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as the process is async // @ApiOperation({ summary: 'Send a bulk SMS message via Twilio Notify' }) // @ApiResponse({ status: HttpStatus.ACCEPTED, description: 'Request accepted, notification creation initiated.' }) // @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid input data (e.g., missing fields, invalid phone numbers, empty recipients array).' }) // @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Failed to initiate bulk SMS due to server or Twilio error.' }) @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) async sendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto): Promise<{ message: string; notificationSid?: string }> { this.logger.log(`Received request to send bulk SMS to ${sendBulkSmsDto.recipients.length} recipients.`); const { recipients, message } = sendBulkSmsDto; try { const notificationSid = await this.messagingService.sendBulkSms(recipients, message); return { message: 'Bulk SMS request accepted by Twilio Notify.', notificationSid: notificationSid, }; } catch (error) { this.logger.error(`Error processing bulk SMS request: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined); throw error; } } }Controller Details
Dependencies:
MessagingServiceLoggerfor request loggingRouting:
@Controller('messaging')sets the base path to/messaging@Post('bulk-sms')defines a POST endpoint at/messaging/bulk-sms@HttpCode(HttpStatus.ACCEPTED)sets the default success status code to 202 Accepted (appropriate for asynchronous processes)Request Processing:
@Body() sendBulkSmsDto: SendBulkSmsDtobinds the incoming JSON request body to the DTOValidationPipeautomatically validates the object – validation failures return 400 Bad Requestrecipientsandmessagefrom the DTOmessagingService.sendBulkSmsnotificationSidSetup Swagger (Optional)
For API documentation, install Swagger:
npm install @nestjs/swagger swagger-ui-express # or yarn add @nestjs/swagger swagger-ui-expressConfigure it in
src/main.ts:// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, HttpException, HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, }), ); // Swagger Setup const config = new DocumentBuilder() .setTitle('Bulk SMS API') .setDescription('API for sending bulk SMS messages via Twilio Notify') .setVersion('1.0') .addTag('Messaging') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api-docs', app, document); const configService = app.get(ConfigService); const port = configService.get<number>('PORT') || 3000; await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); console.log(`Swagger Docs available at: ${await app.getUrl()}/api-docs`); } bootstrap();Access interactive API documentation at
/api-docswhen you run the app. Remember to uncomment the@ApiTags,@ApiOperation,@ApiResponse, and@ApiPropertydecorators in the controller and DTO.6. Database Integration (Conceptual)
This section outlines the conceptual steps for database integration using Prisma. A full implementation is beyond the scope of this core guide but represents a typical next step for production applications.
Real-world applications often need to:
Install Prisma
npm install prisma @prisma/client --save-dev # or yarn add prisma @prisma/client -DInitialize Prisma
npx prisma init --datasource-provider postgresql # or your chosen DBThis creates a
prismadirectory withschema.prismaand updates.envwithDATABASE_URL. Configure yourDATABASE_URLin.env.Define Schema
Apply Migrations
Generate Prisma Client
Create Prisma Service
Abstract database interactions into a service.
// src/prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(PrismaService.name); async onModuleInit() { await this.$connect(); this.logger.log('Prisma client connected.'); } async onModuleDestroy() { await this.$disconnect(); this.logger.log('Prisma client disconnected.'); } }Register
PrismaServicein the modules where it's needed (e.g.,MessagingModule) and ensurePrismaModuleis created and imported globally or locally.Use in Messaging Service
Modify
MessagingServiceto:PrismaServiceBulkMessageLogafter initiating the send// Example modification in MessagingService import { PrismaService } from '../prisma/prisma.service'; // Adjust path as needed // ... inside MessagingService class constructor( private configService: ConfigService, private prisma: PrismaService, // Inject PrismaService ) { // ... Twilio setup } // Example method using DB lookup (conceptual) async sendBulkSmsToList(listId: string, messageBody: string): Promise<string> { // 1. Fetch recipients from DB based on listId const recipientsData = await this.prisma.recipient.findMany({ // where: { recipientListId: listId }, // Example filter if using lists select: { phoneNumber: true } }); const recipients = recipientsData.map(r => r.phoneNumber); if (recipients.length === 0) { throw new BadRequestException(`No recipients found for list ID: ${listId}`); } // 2. Call existing logic to send SMS const notificationSid = await this.sendBulkSms(recipients, messageBody); // 3. Log the attempt try { await this.prisma.bulkMessageLog.create({ data: { twilioNotifySid: notificationSid, messageBody: messageBody, recipientCount: recipients.length, status: 'initiated', }, }); this.logger.log(`Logged bulk message initiation for SID: ${notificationSid}`); } catch (dbError) { this.logger.error(`Failed to log bulk message initiation for SID ${notificationSid}: ${dbError instanceof Error ? dbError.message : String(dbError)}`, dbError instanceof Error ? dbError.stack : undefined); // Decide how to handle logging failure: log the error and continue, // or throw an error if logging is critical. } return notificationSid; }Reminder: This is conceptual. A production system requires building out list management APIs and integrating status updates (likely via webhooks, see Section 10).
7. Error Handling, Logging, and Retry Mechanisms
Production systems need robust error handling and logging.
Error Handling
ValidationPipe(automatic)MessagingServicethrowsBadRequestExceptiontry...catchblock inMessagingServicechecks specificerror.codeanderror.statusvaluesMessagingServiceon startupSee Twilio Error Codes for a complete list.
Logging
NestJS's built-in
Loggerlogs to the console by default.Production Logging: Integrate a robust logging solution (e.g., Winston, Pino) and configure it to:
debug,info,warn,error)Key Logging Points:
MessagingControllerinfoMessagingServiceconstructorinfo/errorMessagingServiceinfoMessagingServiceinfoMessagingServiceerrorinfo/errorMessagingServicewarnRetry Mechanisms
Twilio API calls can fail transiently (network issues, temporary Twilio hiccups, rate limits –
429).Considerations:
Idempotency: Twilio Notify's
createoperation is generally not idempotent. Retrying the samecreatecall might result in duplicate broadcasts.Strategy: Instead of retrying the
createcall directly for transient errors, consider:5xxor429response.Simple Retry (Use with Caution): If you must implement a simple retry within the service (e.g., for immediate network errors), use a library like
async-retryor implement carefully with backoff. Avoid retrying for non-transient errors (auth, bad parameters) or potentially duplicate-creating operations like Notifycreateunless you have specific logic to handle it.Queue-Based Retry (Recommended)
// Conceptual retry logic using a queue (requires setting up BullMQ or similar) // In MessagingController: // constructor( // private readonly messagingService: MessagingService, // @InjectQueue('message-queue') private messageQueue: Queue, // Inject queue // ) {} // async sendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto) { // this.logger.log(`Queueing request to send bulk SMS to ${sendBulkSmsDto.recipients.length} recipients.`); // await this.messageQueue.add('send-bulk-sms-job', { // recipients: sendBulkSmsDto.recipients, // message: sendBulkSmsDto.message, // }, { // attempts: 3, // Max attempts // backoff: { type: 'exponential', delay: 1000 }, // Exponential backoff // }); // return { message: 'Bulk SMS request queued for processing.' }; // // HttpStatus remains 202 Accepted // } // Then create a Queue Processor that calls messagingService.sendBulkSmsFor production, use a dedicated job queue – it's the most robust approach for handling retries of potentially non-idempotent operations like sending notifications.
Frequently Asked Questions About Bulk SMS with Twilio Notify
What is Twilio Notify and how does it differ from the Twilio SMS API?
Twilio Notify is a specialized service designed for sending identical notifications to large recipient lists across multiple channels (SMS, push notifications, email). Unlike the standard Twilio SMS API that requires individual API calls per recipient, Notify accepts a single request with multiple bindings and handles message distribution efficiently, avoiding rate limits and complex looping logic. Use Notify for bulk broadcasts and the SMS API for individual, personalized messages.
How many recipients can I send to in a single Twilio Notify request?
Twilio Notify supports sending to up to 10,000 recipients in a single API call via the
toBindingparameter. For larger audiences, split your recipient list into batches of 10,000 and make multiple Notify requests. Each request returns a unique Notification SID for tracking delivery status.Do I need a Twilio Messaging Service to use Notify?
Yes, Twilio Notify requires a Messaging Service with at least one phone number in the sender pool. The Messaging Service (SID starting with
MG) provides sender ID management, content intelligence, and scalability features. Configure your Notify Service to use the Messaging Service through the Twilio Console under the SMS channel settings.How do I validate phone numbers in E.164 format with NestJS?
Use the
@IsPhoneNumber(null, { each: true })decorator fromclass-validatorin your DTO. This validates each recipient phone number usinglibphonenumber-js. Thenullparameter accepts any country code when numbers use E.164 format (e.g.,+15551234567for US,+447700900123for UK). The{ each: true }option validates every element in the recipients array.What HTTP status code should my bulk SMS endpoint return?
Return
202 Acceptedrather than200 OKbecause Twilio Notify processes messages asynchronously. The API call initiates the broadcast but doesn't guarantee immediate delivery to all recipients. A 202 status correctly indicates the server accepted the request for processing without confirming completion.How do I handle Twilio API rate limits in production?
Implement a job queue (BullMQ, RabbitMQ, or AWS SQS) to handle requests exceeding Twilio's rate limits. When you receive a
429 Too Many Requestserror, queue the failed request for retry with exponential backoff. The error handling code in this guide detectserror.status === 429and throws anHttpExceptionthat your queue processor can catch and reschedule.Can I track individual message delivery status with Notify?
Yes, but it requires additional configuration. Implement status callback webhooks by configuring your Messaging Service's callback URL to receive delivery receipts for each message. Create a webhook endpoint in your NestJS application that processes
MessageStatusevents from Twilio, then update your database with delivery statuses (sent,delivered,failed,undelivered).Should I store Twilio credentials in environment variables or a secrets manager?
For development, use
.envfiles with the@nestjs/configmodule. For production, use a secrets manager like AWS Secrets Manager, HashiCorp Vault, or Google Secret Manager. Never commit credentials to version control. The NestJSConfigServicecan integrate with secrets managers through custom configuration loaders.How do I implement authentication for my bulk SMS API endpoint?
Add NestJS Guards for authentication. Implement JWT-based authentication using
@nestjs/passportandpassport-jwt, or API key authentication with a custom guard. Apply guards to your controller with@UseGuards(JwtAuthGuard)or globally inmain.ts. Always validate the authenticated user has permission to send bulk messages before processing requests.What's the best way to test Twilio Notify integration without sending real SMS?
Use Twilio's test credentials for unit tests or mock the Twilio client in your service tests. For integration testing, purchase a Twilio phone number and send messages to verified numbers on your account (trial accounts can only send to verified numbers). Alternatively, use Twilio's webhook testing tools like
twilio-runorngrokto test callbacks locally without actual SMS delivery.Next Steps for Your Bulk SMS System
Enhance your bulk SMS broadcast system with these production-ready features:
@nestjs/passportand role-based access control to restrict bulk messaging to authorized users@nestjs/throttlerto protect your API from abuse and prevent excessive Twilio chargesAdditional Resources
Your NestJS bulk SMS system is now ready for production deployment with proper error handling, validation, and scalability considerations.