code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / MessageBird

Build a Bulk SMS System with MessageBird and NestJS (2025 Guide)

Learn how to build a production-ready bulk SMS broadcasting system using MessageBird API and NestJS. Complete TypeScript guide with rate limiting, retry logic, and Prisma database integration.

Build a Bulk SMS Broadcasting System with MessageBird and NestJS

Bulk SMS broadcasting sends promotional campaigns, alerts, or notifications to thousands of recipients efficiently. This guide shows you how to build a production-ready bulk SMS system using MessageBird's SMS API and NestJS.

Common use cases include:

  • Marketing campaigns: Product launches, seasonal promotions, flash sales (10K–500K recipients)
  • Emergency alerts: Service outages, security notifications, critical updates (response time: <5 minutes)
  • Appointment reminders: Healthcare, salons, service businesses (scheduled delivery)
  • Event notifications: Ticket confirmations, schedule changes, venue updates

Scale considerations: This architecture handles 1,000–50,000 messages/hour out of the box. For campaigns exceeding 100K recipients or requiring <30 second delivery windows, you'll need horizontal scaling (multiple workers), Redis queue distribution, and potentially MessageBird's enterprise batch endpoints.

Important Note: MessageBird's standard /messages API endpoint accepts a maximum of 50 recipients per request according to their official documentation. For larger volumes, batch requests on the application side or verify with MessageBird support about enterprise batch endpoints. Always consult the latest MessageBird API documentation for current limits and features.

What You'll Build with MessageBird and NestJS

By the end of this tutorial, you'll have a NestJS application that:

  • Accepts bulk SMS requests through a REST API
  • Validates phone numbers in E.164 format
  • Processes messages in batches through MessageBird's SMS API (max 50 recipients per request)
  • Handles rate limiting with exponential backoff retry logic
  • Tracks delivery status for each message
  • Stores campaign history in a PostgreSQL database via Prisma

Prerequisites for MessageBird NestJS Integration

Skill level: Intermediate (comfortable with TypeScript, REST APIs, and basic database concepts)

Time estimate: 60–90 minutes for initial setup; additional 30–60 minutes for testing and deployment

Before you begin, ensure you have:

  • Node.js (v18 LTS or later recommended) and npm/pnpm/yarn installed
  • A MessageBird account with an active API key. Sign up at MessageBird if you don't have one
  • PostgreSQL installed locally or access to a hosted instance (e.g., Supabase, Neon, or Railway)
  • Basic familiarity with NestJS, TypeScript, and REST APIs

Step 1: Set Up Your NestJS Project for MessageBird Integration

Create a new NestJS project:

bash
nest new nestjs-messagebird-bulk
cd nestjs-messagebird-bulk

Install the required dependencies:

bash
npm install @nestjs/config @nestjs/axios @nestjs/throttler @prisma/client axios
npm install -D prisma

Dependency breakdown:

  • @nestjs/config – Manages environment variables
  • @nestjs/axios – HTTP client module for MessageBird API calls
  • @nestjs/throttler – Rate limiting to prevent API abuse
  • @prisma/client and prisma – Database ORM for PostgreSQL
  • axios – HTTP library (peer dependency for @nestjs/axios)

Step 2: Configure MessageBird API Environment Variables

Security best practices:

  • Never commit API keys: Add .env to .gitignore immediately
  • Use environment-specific keys: Separate test and live keys; never use live keys in development
  • Rotate keys regularly: MessageBird supports multiple API keys; rotate every 90 days per security policy
  • Restrict key permissions: In MessageBird Dashboard, limit API key scopes to only required operations (e.g., messages:write)
  • Use secrets management: For production, use AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault instead of plain .env files
  • Monitor key usage: Enable API key usage alerts in MessageBird Dashboard to detect unauthorized access

Create a .env file in your project root:

bash
# MessageBird API Configuration
MESSAGEBIRD_API_KEY=YOUR_API_TOKEN_HERE
MESSAGEBIRD_API_BASE_URL=https://rest.messagebird.com

# Database Configuration
DATABASE_URL="postgresql://user:password@localhost:5432/sms_db?schema=public"

# Application Settings
PORT=3000
NODE_ENV=development

Replace YOUR_API_TOKEN_HERE with your actual MessageBird API key from the MessageBird Dashboard.

Update src/app.module.ts to load environment variables:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ThrottlerModule } from '@nestjs/throttler';
import { PrismaModule } from './prisma/prisma.module';
import { SmsModule } from './sms/sms.module';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ThrottlerModule.forRoot([{
      ttl: 60000, // 60 seconds
      limit: 10,  // 10 requests per minute
    }]),
    PrismaModule,
    SmsModule,
  ],
})
export class AppModule {}

Step 3: Set Up Prisma Database Schema for SMS Campaign Tracking

Initialize Prisma:

bash
npx prisma init

This command creates a prisma directory with a schema.prisma file. Update it to define your SMS campaign and message models:

Performance indexes: The schema below includes strategic indexes to optimize common query patterns. Index campaignId on SmsMessage for fast campaign lookups, status for filtering by delivery state, and createdAt for time-based queries. Composite index on (campaignId, status) accelerates dashboard queries showing campaign status breakdowns. For high-volume systems (>1M messages), consider partitioning the SmsMessage table by createdAt month.

Update prisma/schema.prisma:

prisma
model SmsCampaign {
  id              Int      @id @default(autoincrement())
  name            String
  totalMessages   Int      @default(0)
  successCount    Int      @default(0)
  failureCount    Int      @default(0)
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  messages        SmsMessage[]

  @@index([createdAt])
}

model SmsMessage {
  id                    Int      @id @default(autoincrement())
  campaignId            Int
  campaign              SmsCampaign @relation(fields: [campaignId], references: [id])
  recipient             String
  originator            String
  body                  String
  status                String
  messageBirdResponse   Json?
  errorMessage          String?
  createdAt             DateTime @default(now())
  updatedAt             DateTime @updatedAt

  @@index([campaignId])
  @@index([status])
  @@index([createdAt])
  @@index([campaignId, status])
}

Schema explanation:

  • SmsCampaign – Stores campaign metadata, total message count, and success/failure tallies
  • SmsMessage – Tracks individual messages with recipient, status, and MessageBird response data
  • Relations connect campaigns to their messages via campaignId

Generate the Prisma client and run migrations:

bash
npx prisma generate
npx prisma migrate dev --name init

Step 4: Create DTOs for MessageBird SMS Request Validation

NestJS uses Data Transfer Objects (DTOs) with class-validator decorators to validate incoming requests.

Create src/sms/dto/create-bulk-sms.dto.ts:

typescript
import { IsString, IsNotEmpty, IsArray, ArrayNotEmpty, ArrayMaxSize, ValidateNested, Matches, MaxLength } from 'class-validator';
import { Type } from 'class-transformer';

class MessageDto {
  @IsArray()
  @ArrayNotEmpty({ message: 'Recipients array cannot be empty.' })
  @ArrayMaxSize(50, { message: 'Cannot process more than 50 messages per batch request due to MessageBird API limits.' })
  @IsString({ each: true })
  @Matches(/^\+[1-9]\d{1,14}$/, { each: true, message: 'Each recipient must be in E.164 format (e.g., +14155552671).' })
  recipients: string[];

  @IsString()
  @IsNotEmpty({ message: 'Originator (sender ID) is required.' })
  @MaxLength(11, { message: 'Originator cannot exceed 11 characters.' })
  originator: string;

  @IsString()
  @IsNotEmpty({ message: 'Message body is required.' })
  @MaxLength(1600, { message: 'Message body cannot exceed 1600 characters (10 concatenated SMS segments).' })
  body: string;
}

export class CreateBulkSmsDto {
  @IsString()
  @IsNotEmpty({ message: 'Campaign name is required.' })
  campaignName: string;

  @IsArray()
  @ArrayNotEmpty({ message: 'Messages array cannot be empty.' })
  @ValidateNested({ each: true })
  @Type(() => MessageDto)
  messages: MessageDto[];
}

Key validations:

  • E.164 format – Ensures phone numbers include country code (e.g., +14155552671)
  • 50 recipients max – Matches MessageBird's standard API limit per request
  • Originator length – Alphanumeric sender IDs max 11 characters; numeric max 16 digits
  • Message length – Allows up to 10 concatenated SMS segments (160 chars × 10 = 1600 chars)

Step 5: Build the MessageBird SMS Service with Retry Logic

Error handling strategies:

The service implements a three-tier error handling approach:

  1. Retriable errors (429 rate limits, 5xx server errors): Automatic retry with exponential backoff (1s → 2s → 4s). These typically resolve within seconds as MessageBird's infrastructure recovers or rate limit windows reset.

  2. Client errors (400 Bad Request, 401 Unauthorized, 403 Forbidden): No retry. Log the error with full context (request payload, response body) and mark message as failed. These indicate configuration issues (invalid API key, malformed phone numbers) that won't resolve with retries.

  3. Network errors (DNS failures, connection timeouts): Retry once after 2 seconds. If still failing, escalate to monitoring alerts as this indicates infrastructure problems.

When to use circuit breakers: For campaigns exceeding 10,000 messages, implement a circuit breaker pattern (using opossum or similar) to prevent cascading failures. Trip the circuit after 50 consecutive failures and enter half-open state after 30 seconds.

Create src/sms/sms.service.ts to handle MessageBird API interactions, retry logic, and database persistence:

typescript
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { PrismaService } from '../prisma/prisma.service';
import { CreateBulkSmsDto } from './dto/create-bulk-sms.dto';
import { firstValueFrom } from 'rxjs';
import { AxiosError } from 'axios';

interface MessageBirdResponseItem {
  recipients: string[];
  originator: string;
  body: string;
  messageId?: string;
  status?: string;
  error?: string;
}

@Injectable()
export class SmsService {
  private readonly logger = new Logger(SmsService.name);
  private readonly apiKey: string;
  private readonly baseUrl: string;
  private readonly messageApiEndpoint = '/messages';
  private readonly maxRetries = 3;
  private readonly retryDelayMs = 1000;

  constructor(
    private readonly configService: ConfigService,
    private readonly httpService: HttpService,
    private readonly prisma: PrismaService,
  ) {
    this.apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY');
    this.baseUrl = this.configService.get<string>('MESSAGEBIRD_API_BASE_URL', 'https://rest.messagebird.com');

    if (!this.apiKey) {
      throw new Error('MESSAGEBIRD_API_KEY is not configured. Check your .env file.');
    }
  }

  async sendBulkSms(createBulkSmsDto: CreateBulkSmsDto) {
    const { campaignName, messages } = createBulkSmsDto;

    // Create campaign record
    const campaign = await this.prisma.smsCampaign.create({
      data: {
        name: campaignName,
        totalMessages: messages.reduce((sum, msg) => sum + msg.recipients.length, 0),
      },
    });

    this.logger.log(`Created campaign: ${campaign.name} (ID: ${campaign.id})`);

    // Process messages with retry logic
    const results: MessageBirdResponseItem[] = [];

    for (const message of messages) {
      const payload = {
        recipients: message.recipients,
        originator: message.originator,
        body: message.body,
      };

      try {
        const response = await this.sendWithRetry(payload);
        results.push({
          ...message,
          messageId: response.id,
          status: 'sent',
        });

        // Store successful messages in database
        await this.storeMessages(campaign.id, message.recipients, message.originator, message.body, 'sent', response);
      } catch (error) {
        this.logger.error(`Failed to send message after ${this.maxRetries} retries:`, error);
        results.push({
          ...message,
          status: 'failed',
          error: error.message,
        });

        // Store failed messages
        await this.storeMessages(campaign.id, message.recipients, message.originator, message.body, 'failed', null, error.message);
      }
    }

    // Update campaign statistics
    const successCount = results.filter((r) => r.status === 'sent').length;
    const failureCount = results.filter((r) => r.status === 'failed').length;

    await this.prisma.smsCampaign.update({
      where: { id: campaign.id },
      data: {
        successCount,
        failureCount,
      },
    });

    return {
      campaignId: campaign.id,
      totalMessages: messages.length,
      successCount,
      failureCount,
      results,
    };
  }

  private async sendWithRetry(payload: any, attempt = 1): Promise<any> {
    try {
      const response = await firstValueFrom(
        this.httpService.post(`${this.baseUrl}${this.messageApiEndpoint}`, payload, {
          headers: {
            'Authorization': `AccessKey ${this.apiKey}`,
            'Content-Type': 'application/json',
          },
        }),
      );

      return response.data;
    } catch (error) {
      const axiosError = error as AxiosError;
      const statusCode = axiosError.response?.status;

      // Retry on rate limit (429) or server errors (5xx)
      let shouldRetry = false;
      if (!statusCode || statusCode === 429 || (statusCode >= 500 && statusCode <= 599)) {
        shouldRetry = true;
      }

      if (shouldRetry && attempt < this.maxRetries) {
        const delay = this.retryDelayMs * Math.pow(2, attempt - 1); // Exponential backoff
        this.logger.warn(`Retry attempt ${attempt}/${this.maxRetries} after ${delay}ms due to status ${statusCode || 'network error'}`);
        await this.sleep(delay);
        return this.sendWithRetry(payload, attempt + 1);
      }

      // Exhausted retries or non-retriable error
      this.logger.error(`MessageBird API error: ${axiosError.message}`, axiosError.response?.data);
      throw new HttpException(
        axiosError.response?.data || 'Failed to send SMS via MessageBird',
        statusCode || HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }

  private async storeMessages(
    campaignId: number,
    recipients: string[],
    originator: string,
    body: string,
    status: string,
    response?: any,
    error?: string,
  ) {
    const messageData = recipients.map((recipient) => ({
      campaignId,
      recipient,
      originator,
      body,
      status,
      messageBirdResponse: response ? JSON.stringify(response) : null,
      errorMessage: error || null,
    }));

    await this.prisma.smsMessage.createMany({ data: messageData });
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async getCampaignStatus(campaignId: number) {
    const campaign = await this.prisma.smsCampaign.findUnique({
      where: { id: campaignId },
      include: { messages: true },
    });

    if (!campaign) {
      throw new HttpException('Campaign not found', HttpStatus.NOT_FOUND);
    }

    return campaign;
  }
}

Key features:

  • Exponential backoff – Retries failed requests with increasing delays (1s, 2s, 4s)
  • Rate limit handling – Detects 429 status codes and retries automatically (MessageBird rate limit: 500 POST requests/second per official documentation)
  • Comprehensive logging – Tracks each API call for debugging
  • Database persistence – Stores all messages with status and error details

Step 6: Create the NestJS SMS Controller with Rate Limiting

Create src/sms/sms.controller.ts to expose REST endpoints:

typescript
import { Controller, Post, Get, Body, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
import { SmsService } from './sms.service';
import { CreateBulkSmsDto } from './dto/create-bulk-sms.dto';

@Controller('sms')
@UseGuards(ThrottlerGuard) // Rate limiting: 10 requests per minute by default
export class SmsController {
  constructor(private readonly smsService: SmsService) {}

  @Post('bulk')
  async sendBulkSms(@Body() createBulkSmsDto: CreateBulkSmsDto) {
    return this.smsService.sendBulkSms(createBulkSmsDto);
  }

  @Get('campaign/:id')
  async getCampaignStatus(@Param('id', ParseIntPipe) id: number) {
    return this.smsService.getCampaignStatus(id);
  }
}

Step 7: Create the Prisma Service Module

Create src/prisma/prisma.service.ts:

typescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Create src/prisma/prisma.module.ts:

typescript
import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Step 8: Wire Up the SMS Module

Create src/sms/sms.module.ts:

typescript
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { SmsService } from './sms.service';
import { SmsController } from './sms.controller';

@Module({
  imports: [HttpModule],
  controllers: [SmsController],
  providers: [SmsService],
})
export class SmsModule {}

Update src/app.module.ts:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ThrottlerModule } from '@nestjs/throttler';
import { PrismaModule } from './prisma/prisma.module';
import { SmsModule } from './sms/sms.module';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ThrottlerModule.forRoot([{
      ttl: 60000, // 60 seconds
      limit: 10,  // 10 requests per minute
    }]),
    PrismaModule,
    SmsModule,
  ],
})
export class AppModule {}

Step 9: Test Your MessageBird Bulk SMS Integration

Testing best practices:

  • Use test numbers: MessageBird provides test credentials that return success without sending real SMS. Check MessageBird Testing Documentation for test phone numbers
  • Start small: Test with 2–3 recipients before scaling to larger batches
  • Verify E.164 format: Use a tool like libphonenumber to validate phone numbers before testing
  • Check logs: Monitor NestJS logs for MessageBird API responses and error details
  • Inspect database: Query the SmsCampaign and SmsMessage tables to verify persistence

Debugging common issues:

  • 401 Unauthorized: Verify MESSAGEBIRD_API_KEY is correct and not expired
  • 422 Unprocessable Entity: Check phone number format (must be E.164: +14155552671)
  • Rate limit errors: Reduce request frequency or implement queue system
  • Database connection errors: Verify DATABASE_URL and PostgreSQL is running

Start your NestJS application:

bash
npm run start:dev

Send a test bulk SMS request:

bash
curl -X POST http://localhost:3000/sms/bulk \
  -H "Content-Type: application/json" \
  -d '{
    "campaignName": "Black Friday Sale",
    "messages": [
      {
        "recipients": ["+14155552671", "+14155552672"],
        "originator": "YourBrand",
        "body": "Get 50% off everything! Use code BF2025. Shop now: https://yourbrand.com"
      },
      {
        "recipients": ["+442071234567"],
        "originator": "YourBrand",
        "body": "Exclusive UK offer: Free shipping on all orders today!"
      }
    ]
  }'

Expected response:

json
{
  "campaignId": 1,
  "totalMessages": 2,
  "successCount": 2,
  "failureCount": 0,
  "results": [
    {
      "recipients": ["+14155552671", "+14155552672"],
      "originator": "YourBrand",
      "body": "Get 50% off everything!...",
      "messageId": "abc123def456",
      "status": "sent"
    },
    {
      "recipients": ["+442071234567"],
      "originator": "YourBrand",
      "body": "Exclusive UK offer...",
      "messageId": "xyz789ghi012",
      "status": "sent"
    }
  ]
}

Check campaign status:

bash
curl http://localhost:3000/sms/campaign/1

Production Considerations for MessageBird SMS at Scale

Scalability Architecture Patterns

Horizontal scaling strategies:

For campaigns exceeding 50,000 messages/hour, implement these patterns:

  1. Worker-based architecture: Deploy multiple NestJS instances behind a load balancer (NGINX, AWS ALB). Use a shared Redis queue (BullMQ) to distribute messages across workers. Each worker processes batches independently, achieving linear scaling (e.g., 10 workers = 500K messages/hour capacity).

  2. Database connection pooling: Configure Prisma with connection pooling to prevent database bottlenecks:

typescript
// In schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Add connection pooling
  // postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10
}
  1. Async processing pattern: Decouple API request acceptance from message sending. Accept requests immediately (return 202 Accepted), queue them in BullMQ/SQS, and process in background workers. This prevents client timeouts and enables retry logic without blocking API responses.

  2. Geographic distribution: For global campaigns, deploy workers in multiple regions (US-East, EU-West, APAC) to reduce latency to MessageBird's regional endpoints. Use GeoDNS or AWS Global Accelerator for routing.

Throughput benchmarks:

  • Single NestJS instance: 1,000–5,000 messages/hour (limited by sequential processing)
  • With BullMQ queue + 5 workers: 25,000–50,000 messages/hour
  • With BullMQ queue + 20 workers + database read replicas: 100,000–200,000 messages/hour

1. Environment Variables Security

Store sensitive credentials securely:

  • Use environment variable management services (AWS Secrets Manager, HashiCorp Vault)
  • Never commit .env files to version control
  • Rotate API keys regularly

2. Rate Limiting Strategy

MessageBird enforces API rate limits (500 POST requests/second per official documentation). Implement application-level rate limiting:

typescript
ThrottlerModule.forRoot([{
  ttl: 60000,  // 1 minute window
  limit: 100,  // 100 requests per minute per IP
}])

For high-volume campaigns, use a queue system (BullMQ, AWS SQS) to throttle requests.

3. Message Queuing for Large Campaigns

Queue system comparison:

FeatureBullMQ (Redis)RabbitMQAWS SQS
Setup complexityLow (npm install)Medium (server setup)Low (AWS account)
HostingSelf-hosted or Redis CloudSelf-hosted or CloudAMQPFully managed
Max throughput10K+ jobs/sec20K+ msgs/sec3K msgs/sec (standard), unlimited (FIFO limited to 300/sec)
PersistenceRedis AOF/RDBDurable queuesDurable by default
Best forNode.js ecosystems, job schedulingMicroservices, complex routingAWS-native apps, serverless
CostRedis hosting (~$10–50/mo)Server costs (~$20–100/mo)Pay-per-request ($0.40/million)
Retry/DLQBuilt-in (exponential backoff)Manual configNative support

Recommendation: Use BullMQ for Node.js projects with <100K messages/hour. Use RabbitMQ for polyglot systems needing advanced routing. Use AWS SQS for serverless or AWS-native architectures.

For campaigns exceeding 10,000 messages, integrate a job queue:

bash
npm install @nestjs/bull bull

Update sms.service.ts to queue messages:

typescript
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';

@Injectable()
export class SmsService {
  constructor(@InjectQueue('sms') private smsQueue: Queue) {}

  async sendBulkSms(dto: CreateBulkSmsDto) {
    for (const message of dto.messages) {
      await this.smsQueue.add('send-sms', message, {
        attempts: 3,
        backoff: { type: 'exponential', delay: 2000 },
      });
    }
  }
}

4. Monitoring and Logging

Critical metrics to monitor:

MetricAlerting ThresholdAction
SMS send failure rate>5% over 10 minutesCheck MessageBird API status, verify credentials
API response time (P95)>2 secondsScale workers, check database performance
Queue depth>10,000 pending jobsAdd workers, investigate bottlenecks
Database connection pool saturation>80% utilizationIncrease pool size, add read replicas
MessageBird 429 rate limit errors>10/minuteImplement circuit breaker, reduce send rate
Campaign completion time>2× expected durationCheck worker health, database indexes

Monitoring stack setup:

Integrate application performance monitoring (APM):

  • Sentry – Track errors and failed API calls
  • DataDog – Monitor request rates and latency
  • Prometheus + Grafana – Visualize campaign metrics

Add structured logging:

typescript
this.logger.log({
  event: 'bulk_sms_sent',
  campaignId: campaign.id,
  successCount,
  failureCount,
  timestamp: new Date().toISOString(),
});

Implementation details for consent tracking:

GDPR, TCPA, and CASL require verifiable opt-in consent before sending marketing SMS. Implement a consent management system:

Database schema additions:

prisma
model Subscriber {
  id                Int       @id @default(autoincrement())
  phoneNumber       String    @unique
  consentedAt       DateTime
  consentSource     String    // e.g., "website_signup", "in_store", "api"
  consentIpAddress  String?   // Required for TCPA compliance
  consentUserAgent  String?   // Browser/app that captured consent
  optedOutAt        DateTime?
  optOutReason      String?
  createdAt         DateTime  @default(now())
  updatedAt         DateTime  @updatedAt

  @@index([phoneNumber])
  @@index([consentedAt])
}

model ConsentAuditLog {
  id              Int      @id @default(autoincrement())
  subscriberId    Int
  action          String   // "opted_in", "opted_out", "consent_renewed"
  timestamp       DateTime @default(now())
  ipAddress       String
  metadata        Json?    // Additional context
}

Compliance requirements by region:

  • GDPR (EU): Explicit consent required; must provide clear opt-out mechanism; retain consent records for 3 years minimum; allow data export/deletion on request (Article 17 "Right to be forgotten")
  • TCPA (USA): Prior express written consent for marketing; must include "by clicking you agree to receive SMS" language; maintain opt-in timestamp and IP address; honor opt-outs within 10 business days; prohibited calling hours: before 8 AM or after 9 PM local time
  • CASL (Canada): Express or implied consent required; include sender identification and unsubscribe mechanism in every message; consent expires after 24 months unless renewed

Verification before sending:

typescript
async sendBulkSms(dto: CreateBulkSmsDto) {
  // Verify all recipients have active consent
  const recipients = dto.messages.flatMap(m => m.recipients);
  const consented = await this.prisma.subscriber.findMany({
    where: {
      phoneNumber: { in: recipients },
      consentedAt: { not: null },
      optedOutAt: null,
    },
  });

  const consentedNumbers = new Set(consented.map(s => s.phoneNumber));
  const unauthorized = recipients.filter(r => !consentedNumbers.has(r));

  if (unauthorized.length > 0) {
    throw new HttpException(
      `Cannot send to ${unauthorized.length} recipients without consent: ${unauthorized.join(', ')}`,
      HttpStatus.FORBIDDEN
    );
  }

  // Proceed with sending...
}

Ensure legal compliance:

  • GDPR (EU) – Obtain explicit consent before sending promotional messages
  • TCPA (USA) – Maintain opt-in records for marketing messages
  • CASL (Canada) – Include unsubscribe mechanisms in all commercial messages

Store consent timestamps in your database:

prisma
model Subscriber {
  id            Int      @id @default(autoincrement())
  phoneNumber   String   @unique
  consentedAt   DateTime
  optedOutAt    DateTime?
}

6. Cost Optimization

Practical cost calculations:

MessageBird charges per message segment. Here's how pricing works:

Example 1: Standard promotional campaign

  • Message: "Flash sale! 40% off all items. Shop now: example.com/sale" (68 chars)
  • Encoding: GSM-7 (no special characters)
  • Segments: 1 (≤160 chars)
  • Recipients: 10,000
  • Cost: 10,000 × $0.0175 (US rate) = $175

Example 2: Unicode message with emoji

  • Message: "🎉 Sale alert! 50% off today 🛍️" (35 chars including emoji)
  • Encoding: UCS-2 (contains emoji)
  • Segments: 1 (≤70 chars for Unicode)
  • Recipients: 10,000
  • Cost: 10,000 × $0.0175 = $175 (same cost but uses 50% of character budget)

Example 3: Long concatenated message

  • Message: "Dear valued customer, your exclusive VIP discount code for this month is SAVE25OFF. Use it on any purchase above $50. Valid until end of month. Visit store.example.com for details. Terms and conditions apply." (220 chars)
  • Encoding: GSM-7
  • Segments: 2 (splits at 153 chars due to concatenation overhead: 160 - 7 header bytes)
  • Recipients: 10,000
  • Cost: 10,000 × 2 × $0.0175 = $350

Cost optimization strategies:

  1. Message length optimization: Keep messages under 160 chars (GSM-7) or 70 chars (Unicode) to avoid concatenation costs
  2. Avoid Unicode when possible: Replace emoji/special chars with GSM-7 equivalents (e.g., "Sale!" instead of "🎉 Sale!")
  3. Use URL shorteners: Reduce long URLs (e.g., bit.ly/promo instead of full URLs)
  4. Batch timing: Send campaigns during off-peak hours for potential volume discounts (contact MessageBird sales)
  5. Segment audience: Target high-value recipients instead of entire database to reduce waste

MessageBird charges per message segment:

  • GSM-7 encoding – 160 characters per segment
  • UCS-2 (Unicode) – 70 characters per segment

Optimize message length:

typescript
function calculateSegments(body: string): number {
  const isUnicode = /[^\x00-\x7F]/.test(body);
  const maxLength = isUnicode ? 70 : 160;
  return Math.ceil(body.length / maxLength);
}

Display segment count in your API response to help users optimize costs.

Troubleshooting MessageBird NestJS Integration Issues

Diagnostic steps and resolution guide:

1. "Invalid phone number format" Error

Cause: Phone numbers must be in E.164 format (+[country code][number]).

Diagnostic steps:

  1. Log the exact phone number value being sent: console.log('Phone number:', phoneNumber)
  2. Check for common issues: leading zeros without +, spaces, parentheses, dashes
  3. Verify country code is included (e.g., +1 for US, +44 for UK)

Fix: Validate numbers before sending:

typescript
import { parsePhoneNumber } from 'libphonenumber-js';

function validateE164(phone: string): boolean {
  try {
    const parsed = parsePhoneNumber(phone);
    return parsed.isValid() && parsed.format('E.164') === phone;
  } catch {
    return false;
  }
}

Prevention: Add validation to your DTO or use a pre-processing step to normalize phone numbers on input.

2. Rate Limit 429 Errors

Cause: Exceeding MessageBird's rate limits (500 POST requests/second per official documentation).

Diagnostic steps:

  1. Check MessageBird Dashboard → API logs for rate limit violations
  2. Monitor your application's request rate: console.log('Requests/sec:', requestCount / timeWindow)
  3. Identify if rate limits are per-account or per-API key (contact MessageBird support)

Fix: Implement exponential backoff (already included in the service above) or use a queue system to throttle requests.

Advanced solution: Implement token bucket rate limiting in your service:

typescript
// Add to SmsService
private tokenBucket = {
  tokens: 500,
  maxTokens: 500,
  refillRate: 500, // tokens per second
  lastRefill: Date.now(),
};

private async acquireToken(): Promise<void> {
  const now = Date.now();
  const elapsed = (now - this.tokenBucket.lastRefill) / 1000;
  this.tokenBucket.tokens = Math.min(
    this.tokenBucket.maxTokens,
    this.tokenBucket.tokens + elapsed * this.tokenBucket.refillRate
  );
  this.tokenBucket.lastRefill = now;

  if (this.tokenBucket.tokens < 1) {
    const waitTime = (1 - this.tokenBucket.tokens) / this.tokenBucket.refillRate * 1000;
    await this.sleep(waitTime);
    return this.acquireToken();
  }

  this.tokenBucket.tokens -= 1;
}

3. Messages Not Delivering

Possible causes:

  • Recipient opted out or blocked your sender ID
  • Invalid recipient number
  • MessageBird account balance depleted

Debug steps:

  1. Check MessageBird Dashboard for delivery reports
  2. Verify sender ID registration (some countries require pre-registration)
  3. Test with a known working number
  4. Check account balance and payment status
  5. Review country-specific restrictions (some countries block promotional SMS)
  6. Verify time zone compliance (TCPA prohibits calls before 8 AM or after 9 PM local time)

Resolution:

  • For sender ID issues: Register alphanumeric sender IDs in MessageBird Dashboard → Settings → Originators
  • For balance issues: Add payment method in Dashboard → Billing
  • For country restrictions: Check MessageBird SMS Country Guide

4. Database Connection Issues

Cause: Incorrect DATABASE_URL in .env file.

Diagnostic steps:

  1. Verify PostgreSQL is running: pg_isready -h localhost -p 5432
  2. Test connection string manually: psql "postgresql://username:password@localhost:5432/database_name"
  3. Check for firewall blocking port 5432
  4. Verify database user permissions: GRANT ALL ON DATABASE sms_db TO username;

Fix: Verify PostgreSQL connection string format:

bash
DATABASE_URL="postgresql://username:password@localhost:5432/database_name?schema=public"

Test connection:

bash
npx prisma db push

Common issues:

  • SSL required for hosted databases: Add ?sslmode=require to connection string
  • Connection pool exhaustion: Increase pool size in Prisma schema
  • Network timeouts: Increase connect_timeout parameter in connection string

Frequently Asked Questions (FAQ)

How many recipients can I send to in a single MessageBird API request?

MessageBird's standard /messages endpoint accepts a maximum of 50 recipients per request according to their official documentation. For campaigns exceeding 50 recipients, batch requests on the application side (as demonstrated in this tutorial) or contact MessageBird support about enterprise batch endpoints. The code example above automatically handles batching by looping through messages.

What is MessageBird's rate limit for SMS API requests?

MessageBird enforces a rate limit of 500 POST requests per second for SMS messaging according to their official documentation. The NestJS service in this tutorial includes exponential backoff retry logic to handle 429 (rate limit) errors automatically. For high-volume campaigns, implement a queue system like BullMQ to throttle requests and stay within limits.

How do I handle MessageBird API failures and retry logic in NestJS?

The tutorial's SmsService includes a sendWithRetry() method with exponential backoff. It automatically retries failed requests up to 3 times with increasing delays (1s, 2s, 4s). The service retries on 429 (rate limit) and 5xx (server error) status codes. All failed messages log to the database with error details for monitoring.

What database schema do I need for MessageBird bulk SMS campaigns?

The Prisma schema includes two models: SmsCampaign (stores campaign metadata, success/failure counts) and SmsMessage (tracks individual messages with recipient, status, and MessageBird response data). This schema enables campaign tracking, delivery reports, and historical analytics. Run npx prisma migrate dev to create the tables.

How do I validate phone numbers for MessageBird SMS API?

MessageBird requires phone numbers in E.164 format (e.g., +14155552671). The tutorial uses class-validator with a regex pattern /^\+[1-9]\d{1,14}$/ to validate E.164 format. For production, consider using libphonenumber-js library for more robust validation including country code verification and number type detection.

Can I send Unicode (emoji) messages through MessageBird?

Yes, MessageBird supports Unicode messages using UCS-2 encoding. However, Unicode messages have a reduced character limit of 70 characters per segment (vs. 160 for GSM-7). The tutorial's DTO allows up to 1,600 characters (10 concatenated segments). Calculate costs carefully – Unicode messages consume more segments and cost more.

How do I implement MessageBird delivery status webhooks in NestJS?

Complete webhook implementation:

MessageBird can send delivery status updates (delivered, failed, expired) via webhooks. Here's a production-ready implementation:

1. Add webhook endpoint to your controller:

typescript
// src/sms/sms.controller.ts
import { Controller, Post, Body, Headers, HttpCode, HttpStatus } from '@nestjs/common';

@Controller('sms')
export class SmsController {
  constructor(private readonly smsService: SmsService) {}

  @Post('webhook/status')
  @HttpCode(HttpStatus.OK)
  async handleDeliveryStatus(
    @Body() payload: any,
    @Headers('messagebird-signature') signature: string,
  ) {
    // Verify webhook signature (recommended for production)
    // See MessageBird webhook security documentation

    await this.smsService.updateMessageStatus(payload);
    return { status: 'received' };
  }
}

2. Add status update method to service:

typescript
// src/sms/sms.service.ts
async updateMessageStatus(payload: any) {
  const { id: messageBirdId, status, statusDatetime } = payload;

  // Find message by MessageBird ID and update status
  await this.prisma.smsMessage.updateMany({
    where: {
      messageBirdResponse: {
        path: ['id'],
        equals: messageBirdId
      }
    },
    data: {
      status,
      updatedAt: new Date(statusDatetime)
    },
  });

  this.logger.log(`Updated message ${messageBirdId} to status: ${status}`);
}

3. Configure webhook in MessageBird Dashboard:

Navigate to MessageBird Dashboard → Developers → Webhooks → Add Webhook:

  • URL: https://yourdomain.com/sms/webhook/status
  • Events: Select "SMS Delivery Reports"
  • Signature Key: Generate a secret key for webhook verification

4. Secure webhook endpoint:

typescript
import * as crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const hash = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return hash === signature;
}

For complete details, see the MessageBird NestJS Delivery Status Webhooks guide.

What NestJS modules do I need for MessageBird SMS integration?

The core dependencies are: @nestjs/config (environment variables), @nestjs/axios (HTTP client for MessageBird API), @nestjs/throttler (rate limiting), @prisma/client (database ORM), and axios (HTTP library). Install with npm install @nestjs/config @nestjs/axios @nestjs/throttler @prisma/client axios. Additionally, install prisma as a dev dependency for schema management.

Next Steps: Enhance Your MessageBird NestJS SMS System

Detailed enhancement roadmap:

EnhancementDifficultyTime EstimateBusiness Impact
1. Delivery WebhooksMedium2–3 hoursHigh – Real-time delivery tracking improves customer support and retry logic
2. Scheduled CampaignsMedium3–4 hoursHigh – Automated timing for optimal engagement (e.g., send at 10 AM local time)
3. A/B TestingMedium-High4–6 hoursMedium – Optimize message content and CTR by testing variants
4. Unsubscribe ManagementMedium3–4 hoursCritical – Legal requirement for GDPR/TCPA compliance
5. Template SystemLow-Medium2–3 hoursMedium – Reusable templates reduce errors and speed up campaign creation
6. Multi-Channel SupportHigh8–12 hoursHigh – Reach customers via WhatsApp, RCS, and email with fallback logic

Implementation priorities:

Phase 1 (Week 1): Unsubscribe management + Delivery webhooks – Ensures compliance and visibility Phase 2 (Week 2-3): Scheduled campaigns + Template system – Improves operational efficiency Phase 3 (Month 2): A/B testing + Multi-channel support – Drives engagement and ROI

Now that you have a working bulk SMS system, consider these enhancements:

  1. Delivery Webhooks – Implement MessageBird delivery status webhooks to track real-time message delivery
  2. Scheduled Campaigns – Use BullMQ's delayed jobs to schedule campaigns for future dates
  3. A/B Testing – Split campaigns into variants and track performance metrics
  4. Unsubscribe Management – Build an opt-out system compliant with GDPR/TCPA
  5. Template System – Create reusable message templates with variable placeholders
  6. Multi-Channel Support – Extend your system to support WhatsApp, RCS, and email

Additional Resources


Related Guides: