MessageBird NestJS: Track SMS Delivery Status with Webhooks & Callbacks
Build a production-ready NestJS app to track MessageBird SMS delivery status. Learn webhook implementation, status callbacks, database persistence, and security best practices.
Sending an SMS successfully is just the first step. To build reliable communication workflows, debug issues, and provide accurate feedback to users or internal systems, you need to know when and if that message reaches the recipient's handset. This MessageBird NestJS guide shows you how to implement SMS delivery tracking with webhooks and callbacks to receive and process real-time status updates.
You'll build a NestJS application that sends SMS messages via MessageBird and includes a dedicated webhook endpoint to receive status updates like sent, delivered, or failed. This guide covers MessageBird webhook configuration, sending messages with callback parameters, handling incoming status callbacks, storing delivery status updates, and production-ready security best practices.
Project Overview and Goals
Goal: Create a NestJS application that sends SMS messages using the MessageBird API and reliably tracks their delivery status through webhooks.
Problem Solved: Gain visibility into SMS delivery beyond the initial API confirmation. Track whether a message was buffered by MessageBird, successfully delivered to the carrier, accepted by the recipient's handset, or failed along the way.
Technologies:
NestJS: Progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Provides modular architecture, dependency injection, and strong TypeScript support.
MessageBird: Communications platform with APIs for SMS, voice, and more. Offers SMS capabilities and webhook features for status reporting.
Node.js: JavaScript runtime environment.
TypeScript: Provides type safety and improved developer experience.
ngrok (development): Exposes your local server to the internet for webhook testing.
(Optional) TypeORM: Persists message details and status updates.
System Architecture:
graph LR
subgraph Your Application
Client[Client/Trigger] -- Send Request --> A[NestJS API: /send]
A -- Save Initial Status (Pending) --> DB[(Database)]
A -- messagebird.messages.create (with reportUrl & reference) --> MB_API[MessageBird API]
WB[NestJS API: /status-webhook] -- Update Status --> DB
end
subgraph MessageBird Platform
MB_API -- Sends SMS --> Carrier[Carrier Network] --> UserDevice[User Device]
MB_API -- Status Update (POST Request) --> WB
end
style DB fill:#f9f,stroke:#333,stroke-width:2px
Prerequisites:
Node.js (LTS version recommended) and npm or yarn
NestJS CLI: npm install -g @nestjs/cli
MessageBird account with API credentials (Live Access Key)
MessageBird virtual mobile number capable of sending SMS
ngrok or similar tunneling service for local development testing
Basic understanding of NestJS concepts (modules, controllers, services)
(Optional) Docker and Docker Compose for containerized setup
What You'll Build:
A production-ready NestJS application with:
API endpoint to send SMS messages
Webhook endpoint to receive MessageBird delivery status updates
Logic to correlate status updates with original outgoing messages
Secure configuration and comprehensive error handling
(Optional) Database persistence for message status tracking
1. Setting Up the Project
Initialize your NestJS project and install the necessary dependencies.
1. Create NestJS Project:
Open your terminal and run:
Bash
nest new messagebird-status-app --strict --package-manager npm
cd messagebird-status-app
This creates a new NestJS project with strict TypeScript configuration and uses npm.
2. Install Dependencies:
Bash
# MessageBird SDK and configuration management
npm install messagebird @nestjs/config dotenv
# (Optional) Database integration (using PostgreSQL and TypeORM)# Note: @nestjs/typeorm 10.x+ requires TypeORM 0.3.x# TypeORM does not follow semantic versioning - minor updates may include breaking changes
npm install @nestjs/typeorm typeorm pg
# (Optional) UUID for generating unique references
npm install uuid
npm install -D @types/uuid
# (Optional) Input validation
npm install class-validator class-transformer
Package Versions Note: The messagebird package (v4.0.1 as of 2024) is the official Node.js SDK. For TypeORM integration, ensure @nestjs/typeorm version 10.x or later is used with typeorm 0.3.x for compatibility.
3. Environment Variables Setup:
Use environment variables for sensitive information like API keys. Configure @nestjs/config to manage these securely.
Create a .env file in the project root:
Text
#.env
# MessageBird Configuration
MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY
# Your purchased MessageBird number (e.g., +12025550135)
MESSAGEBIRD_ORIGINATOR=YOUR_MESSAGEBIRD_NUMBER
# Base URL for your webhook endpoint (ngrok URL for development)
# Example: https://xxxxx.ngrok.io (NO trailing slash)
CALLBACK_BASE_URL=YOUR_NGROK_OR_PUBLIC_URL
# (Optional) Database Connection Details (Example for PostgreSQL)
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=your_db_password
DB_DATABASE=messagebird_status
Important: Replace YOUR_LIVE_API_KEY, YOUR_MESSAGEBIRD_NUMBER, and YOUR_NGROK_OR_PUBLIC_URL with your actual values. Obtain the API key from your MessageBird Dashboard (Developers -> API Access -> Live Key). Purchase a number under the "Numbers" section if you haven't already. The CALLBACK_BASE_URL will be provided by ngrok later.
Add .env to your .gitignore file to prevent accidentally committing secrets.
4. Configure ConfigModule and TypeOrmModule (Optional):
Import and configure ConfigModule in your main application module (src/app.module.ts). If using a database, configure TypeOrmModule.
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule, ConfigService } from'@nestjs/config'; // Import ConfigServiceimport { TypeOrmModule } from'@nestjs/typeorm'; // Uncomment if using DBimport { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { MessagingModule } from'./messaging/messaging.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Make ConfigService available globallyenvFilePath: '.env',
}),
MessagingModule,
// Uncomment the following TypeOrmModule section if using a database/*
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
// Use autoLoadEntities for simplicity, or configure paths manually.
// If configuring manually, ensure the path points to compiled .js files in production (e.g., 'dist/**/*.entity.js').
autoLoadEntities: true,
// synchronize: true MUST NOT be used in production.
// It can lead to data loss. Use migrations instead for schema changes.
// Recommended ONLY for early development or local testing.
synchronize: configService.get<string>('NODE_ENV') !== 'production', // Example: Enable only if not in production
}),
inject: [ConfigService],
}),
*/
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
5. Project Structure:
The initial structure created by nest new is suitable. We will add a dedicated messaging module to handle all MessageBird interactions.
We'll create a module responsible for sending messages and handling status callbacks.
1. Generate Module, Service, Controller:
Bash
nest g module messaging
nest g service messaging # We will add tests later
nest g controller messaging --no-spec
This creates src/messaging/messaging.module.ts, src/messaging/messaging.service.ts, and src/messaging/messaging.controller.ts.
2. Implement MessagingService:
This service will contain the logic for interacting with the MessageBird SDK.
TypeScript
// src/messaging/messaging.service.tsimport { Injectable, Logger, InternalServerErrorException, OnModuleInit } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import * asMessageBirdfrom'messagebird'; // Use namespace importimport { v4 as uuidv4 } from'uuid';
// Optional DB integration imports// import { InjectRepository } from '@nestjs/typeorm';// import { Repository } from 'typeorm';// import { Message } from './entities/message.entity'; // Corrected import path@Injectable()
exportclassMessagingServiceimplementsOnModuleInit {
privatereadonly logger = newLogger(MessagingService.name);
privatemessagebird: MessageBird.MessageBird; // Use MessageBird typeprivateoriginator: string;
privatecallbackUrl: string;
constructor(privateconfigService: ConfigService,
// Uncomment if using DB// @InjectRepository(Message)// private messageRepository: Repository<Message>,) {}
onModuleInit() {
const apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY');
this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR');
const baseUrl = this.configService.get<string>('CALLBACK_BASE_URL');
if (!apiKey || !this.originator || !baseUrl) {
this.logger.error('MessageBird API Key, Originator, or Callback Base URL not configured.');
thrownewInternalServerErrorException('Messaging service configuration is incomplete.');
}
// Initialize MessageBird SDKthis.messagebird = MessageBird(apiKey);
this.callbackUrl = `${baseUrl}/messaging/status`; // Construct the full callback URLthis.logger.log('MessageBird SDK initialized.');
this.logger.log(`Using Originator: ${this.originator}`);
this.logger.log(`Expecting status callbacks at: ${this.callbackUrl}`);
}
/**
* Sends an SMS message via MessageBird and requests status reports.
* @param to Recipient phone number (E.164 format expected)
* @param body Message content
* @returns The unique reference ID generated for this message
*/asyncsendMessage(to: string, body: string): Promise<string> {
// Generate a unique reference for this message - CRUCIAL for trackingconst reference = uuidv4();
this.logger.log(`Attempting to send SMS to ${to} with reference: ${reference}`);
// --- Optional: Persist initial message details (status: 'pending') ---// const newMessage = this.messageRepository.create({ reference, recipient: to, body, status: 'pending' });// try {// await this.messageRepository.save(newMessage);// this.logger.log(`Saved initial message record with reference ${reference}`);// } catch (dbErr) {// this.logger.error(`Database error saving initial message: ${dbErr.message}`, dbErr.stack);// // Decide how to handle - maybe don't send if DB fails? Or send and log?// }// --- End Optional DB Interaction ---constparams: MessageBird.messages.MessageCreateParameters = {
originator: this.originator,
recipients: [to],
body: body,
reference: reference, // Include the reference in the requestreportUrl: this.callbackUrl, // Tell MessageBird where to POST status updates
};
returnnewPromise((resolve, reject) => {
this.messagebird.messages.create(params, async (err, response) => { // Note: async callback for optional DB updateif (err) {
this.logger.error(`Failed to send SMS via MessageBird: ${err.message}`, err.stack);
// --- Optional: Update status to 'failed_to_send' on API error ---// try {// await this.messageRepository.update({ reference }, { status: 'failed_to_send' });// } catch (dbUpdateErr) { this.logger.error(`Failed to update status to failed_to_send for ${reference}: ${dbUpdateErr.message}`); }// --- End Optional Update ---reject(newInternalServerErrorException(`MessageBird API error: ${err.message}`));
} else {
const messageBirdId = response?.id;
if (response?.recipients?.items?.length > 0) {
this.logger.log(`Message accepted by MessageBird for recipient ${to}. Message ID: ${messageBirdId}, Reference: ${reference}`);
// --- Optional: Update status to 'accepted' on successful API call ---// try {// await this.messageRepository.update({ reference }, { status: 'accepted', messageBirdId: messageBirdId });// this.logger.log(`Message ${reference} accepted. DB status updated.`);// } catch (dbUpdateErr) { this.logger.error(`Failed to update status to accepted for ${reference}: ${dbUpdateErr.message}`); }// --- End Optional Update ---
} else {
this.logger.warn(`MessageBird response did not contain expected recipient details for ${to}, Reference: ${reference}. Response: ${JSON.stringify(response)}`);
// Consider how to handle this - maybe update DB status differently?
}
resolve(reference); // Return the reference ID on successful API call
}
});
});
}
/**
* Processes incoming delivery status updates from MessageBird.
* @param statusData The webhook payload from MessageBird
*/asyncprocessStatusUpdate(statusData: any): Promise<void> { // Make async for potential DB opsconst reference = statusData.reference;
const status = statusData.status;
const statusDatetime = statusData.statusDatetime;
let recipient = statusData.recipient; // Can be number or string, sometimes without '+'const messageId = statusData.id; // MessageBird's internal IDif (!reference) {
this.logger.warn('Received status update without a reference ID. Cannot correlate.', statusData);
return;
}
this.logger.log(`Received status update for reference ${reference}: Status=${status}, Recipient=${recipient}, Time=${statusDatetime}, MsgID=${messageId}`);
// --- Database Update Logic ---// Here, you would typically find the message record in your database// using the 'reference' and update its status and statusUpdatedAt fields.// **Normalization Note:** If storing or looking up by recipient, ensure consistent formatting.// MessageBird might send `recipient` without a leading '+'. Consider normalizing// to E.164 format (e.g., using a library like 'libphonenumber-js') before DB operations.// Example (pseudo-code): const normalizedRecipient = normalizeToE164(recipient);// Example DB update (uncomment and adapt if using TypeORM):/*
try {
const message = await this.messageRepository.findOne({ where: { reference } });
if (message) {
message.status = status;
message.statusUpdatedAt = new Date(statusDatetime); // Ensure correct parsing/type
message.messageBirdId = messageId ?? message.messageBirdId; // Update MB ID if present
message.lastRawStatus = statusData; // Optional: Store raw payload for debugging
// Potentially update the recipient field if needed after normalization
await this.messageRepository.save(message);
this.logger.log(`Updated status for reference ${reference} in DB.`);
} else {
this.logger.warn(`Could not find message with reference ${reference} in DB to update status.`);
}
} catch (dbErr) {
this.logger.error(`Database error updating status for reference ${reference}: ${dbErr.message}`, dbErr.stack);
// IMPORTANT: Still return OK to MessageBird below, but log the DB error for investigation.
// Consider pushing to a dead-letter queue for retry if DB update fails.
}
*/// --- End Database Update Logic ---
}
}
Key Points:
Initialization (onModuleInit): Initializes the SDK using ConfigService.
Unique Reference (uuidv4): Critical for correlating status updates. Generated for each message.
reportUrl Parameter: Tells MessageBird where to POST status updates for this specific message.
Asynchronous Handling: Uses Promises for cleaner async/await usage.
processStatusUpdate: Handles incoming webhook data. Extracts key fields, logs them, and includes placeholder logic for database updates. Normalization of the recipient number is noted as a potential requirement.
3. Implement MessagingController:
Defines API endpoints for sending messages and receiving status updates.
TypeScript
// src/messaging/messaging.controller.tsimport { Controller, Post, Body, HttpCode, HttpStatus, Logger, ValidationPipe, UsePipes } from'@nestjs/common';
import { MessagingService } from'./messaging.service';
import { SendMessageDto } from'./dto/send-message.dto'; // We'll create this DTO next@Controller('messaging')
exportclassMessagingController {
privatereadonly logger = newLogger(MessagingController.name);
constructor(privatereadonlymessagingService: MessagingService) {}
/**
* Endpoint to send an SMS message.
*/@Post('send')
@HttpCode(HttpStatus.ACCEPTED) // 202 Accepted is suitable as delivery is async@UsePipes(newValidationPipe({ transform: true, whitelist: true }))
asyncsendMessage(@Body() sendMessageDto: SendMessageDto): Promise<{ message: string; reference: string }> {
this.logger.log(`Received request to send SMS to ${sendMessageDto.to}`);
const reference = awaitthis.messagingService.sendMessage(
sendMessageDto.to,
sendMessageDto.body,
);
return {
message: 'SMS send request accepted by MessageBird.',
reference: reference, // Return the unique reference
};
}
/**
* Webhook endpoint to receive delivery status updates from MessageBird.
* MessageBird expects a 2xx response quickly. Offload heavy processing if needed.
*/@Post('status')
@HttpCode(HttpStatus.OK) // Respond with 200 OK immediately to acknowledge receiptasynchandleStatusWebhook(@Body() statusData: any): Promise<void> {
// Log the raw incoming data for debugging (optional)// this.logger.debug('Received MessageBird status webhook:', JSON.stringify(statusData, null, 2));// No validation DTO here initially, as MessageBird's payload is fixed,// but you could add one for stricter typing or basic checks if desired.// Asynchronously process the update. Don't await if processing might be slow.// Await is safe here ONLY if processStatusUpdate is guaranteed to be fast (e.g., only logging).// For DB operations, consider running without await and handling errors internally,// or push to a queue.awaitthis.messagingService.processStatusUpdate(statusData);
// IMPORTANT: Respond quickly! This endpoint must return 200 OK fast.// If processStatusUpdate involves slow operations (DB writes, external calls),// do NOT await it here. Instead, trigger it asynchronously:// this.messagingService.processStatusUpdate(statusData).catch(err => {// this.logger.error('Error processing status update asynchronously:', err);// });// OR push statusData to a message queue (e.g., BullMQ, RabbitMQ) for background processing.
}
}
Key Points:
/send Endpoint: Accepts POST requests, validates input using SendMessageDto, calls the service, returns 202 Accepted with the reference.
/status Endpoint: Accepts POST requests from MessageBird. It passes data to the service and must return 200 OK quickly. Slow processing should be handled asynchronously (background job queue recommended).
Validation Pipe: Ensures the /send request body adheres to SendMessageDto.
4. Create DTO (Data Transfer Object):
Defines the expected request body for the /send endpoint.
TypeScript
// src/messaging/dto/send-message.dto.tsimport { IsNotEmpty, IsString, IsPhoneNumber } from'class-validator';
exportclassSendMessageDto {
@IsNotEmpty()
@IsPhoneNumber(null) // Use 'null' for generic E.164 format validation (e.g., +1xxxxxxxxxx)@IsString()
readonlyto: string;
@IsNotEmpty()
@IsString()
readonlybody: string;
}
3. Building a Complete API Layer
Our core API endpoints (/messaging/send and /messaging/status) are defined. Let's refine them.
Authentication/Authorization:
/send Endpoint: This endpoint must be protected (e.g., using API Keys via guards/middleware, or JWT if part of a user session).
/status Endpoint: Needs public accessibility but requires security considerations:
Obscurity: Use a less guessable path (minor benefit).
Shared Secret: Add a secret query parameter to reportUrl and verify it.
IP Whitelisting: Allow only MessageBird's webhook IPs (requires infrastructure setup).
Signed Webhooks:Verify with current MessageBird documentation if they support cryptographically signed delivery status webhooks. This is the most secure method if available. Check their docs carefully as features evolve.
Recommendation: Use HTTPS. Rely on the unique reference for correlation. Rigorously sanitize input. If available and feasible, implement signature verification or IP whitelisting.
Request Validation:
Already implemented for /send using class-validator.
API Documentation:
Consider using @nestjs/swagger to generate OpenAPI documentation for the /send endpoint.
Testing Endpoints:
/send Endpoint:
Bash
# Ensure your NestJS app is running (npm run start:dev)
curl -X POST http://localhost:3000/messaging/send \
-H 'Content-Type: application/json' \
-d '{
"to": "+12025550199",
"body": "Hello from NestJS and MessageBird!"
}'# Expected Response (Example):# {# "message": "SMS send request accepted by MessageBird.",# "reference": "a1b2c3d4-e5f6-7890-1234-567890abcdef"# }
/status Endpoint: Test by sending a real message and observing logs/DB, or simulate a callback:
Bash
# Simulate a 'delivered' status update (Replace <your-ngrok-url> and reference)
curl -X POST https://<your-ngrok-url>/messaging/status \
-H 'Content-Type: application/json' \
-d '{
"id": "mb-message-id-123",
"href": "...",
"reference": "YOUR_MESSAGE_REFERENCE_ID",
"status": "delivered",
"statusDatetime": "2025-04-20T10:30:00Z",
"recipient": 31612345678,
"originator": "YourNumber",
"message": "Hello World"
}'# Expected Response: HTTP/1.1 200 OK (with an empty body)
4. Integrating with MessageBird
Configuration:
API Key: MessageBird Dashboard -> Developers -> API access -> Copy Live Key -> .env (MESSAGEBIRD_API_KEY).
Virtual Number (Originator): MessageBird Dashboard -> Numbers -> Buy Number -> .env (MESSAGEBIRD_ORIGINATOR, E.164 format).
Webhook URL (reportUrl):
Important: MessageBird provides status reports ONLY for SMS messages that have BOTH a reference defined when sending AND a status report URL set via reportUrl (per-message) or configured globally in your account settings.
Development: Run ngrok http 3000. Copy HTTPS URL -> .env (CALLBACK_BASE_URL). Full URL is ${CALLBACK_BASE_URL}/messaging/status.
Production: Use your public server URL (e.g., https://api.yourdomain.com) as CALLBACK_BASE_URL.
Default Status Reports: You can set a global webhook URL in MessageBird settings, but using reportUrl per message (as implemented) offers more control and flexibility.
Environment Variables Summary:
MESSAGEBIRD_API_KEY: Authenticates API requests.
MESSAGEBIRD_ORIGINATOR: Sender ID for outgoing SMS.
CALLBACK_BASE_URL: Public base URL for constructing the reportUrl.
Fallback Mechanisms:
Implement retries (with backoff) around the sendMessage call in case of transient MessageBird API errors.
Log API errors clearly. MessageBird automatically retries webhooks if your /status endpoint fails to return 2xx quickly.
5. Error Handling, Logging, and Retry Mechanisms
Error Handling Strategy:
API Sending Errors: Catch errors in sendMessage. Log details. Return appropriate HTTP errors from /send. Optionally update DB status to indicate sending failure.
Webhook Processing Errors: Use try...catch in processStatusUpdate. Log internal errors (DB issues, etc.) but always return 200 OK to MessageBird. Handle the failure internally (log, dead-letter queue).
Validation Errors: Handled by ValidationPipe for /send (returns 400).
Logging:
Use NestJS Logger.
Log key events: Init, send attempt (with reference), API success/failure, webhook received (with reference, status), DB updates, errors (with stack traces).
Use appropriate levels (log, warn, error).
Consider structured logging (JSON) for easier analysis.
Retry Mechanisms:
Sending: Implement manual retries with backoff if the initial API call fails due to transient errors.
Webhook Receiving: Focus on making /status fast and reliable. MessageBird handles retries if needed. Use a background queue for complex processing.
6. Creating a Database Schema and Data Layer (Optional)
Persistence is needed for tracking. Here's a simplified TypeORM/PostgreSQL example.
Simplified Schema:
For the scope of this guide, a single entity to track the message and its latest status is often sufficient.
erDiagram
MESSAGE {
string id PK ""UUID, generated by DB or code""
string reference UK ""UUID, generated by code, used for correlation""
string messageBirdId NULL ""MessageBird's internal message ID""
string recipient ""E.164 phone number""
string body ""Message content""
string status ""pending, accepted, sent, delivered, failed, expired, etc.""
datetime createdAt ""Timestamp when record created""
datetime statusUpdatedAt NULL ""Timestamp of last status update from MB""
jsonb lastRawStatus NULL ""Store the last raw JSON payload from webhook""
datetime updatedAt ""Timestamp record last updated""
}
2. TypeORM Entity:
TypeScript
// src/messaging/entities/message.entity.tsimport { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from'typeorm';
@Entity('messages') // Ensure table name matches your DBexportclassMessage {
@PrimaryGeneratedColumn('uuid')
id: string;
// Critical index for finding the message based on the webhook reference@Index({ unique: true })
@Column({ type: 'uuid' })
reference: string;
// Indexing MessageBird's ID can be useful for reconciliation@Index()
@Column({ nullable: true })
messageBirdId: string;
@Column()
recipient: string; // Store normalized E.164 format if possible@Column({ type: 'text' })
body: string;
// Index status for efficient querying (e.g., find all 'failed' messages)@Index()
@Column({ default: 'pending' })
status: string; // e.g., pending, accepted, buffered, sent, delivered, expired, delivery_failed@CreateDateColumn()
createdAt: Date;
// Use timestamptz for PostgreSQL to store timezone info (recommended)@Column({ type: 'timestamptz', nullable: true })
statusUpdatedAt: Date;
// Store the last raw status payload (JSONB is efficient in PostgreSQL)@Column({ type: 'jsonb', nullable: true })
lastRawStatus: any;
@UpdateDateColumn() // Automatically updated by TypeORM on save/updateupdatedAt: Date;
}
3. Integrate with Service:
Inject the Message repository (@InjectRepository(Message)) into MessagingService constructor. Uncomment and adapt the database interaction logic within sendMessage and processStatusUpdate as shown in the service code comments (Section 2).
4. Migrations:
Strongly recommended for production. Avoid synchronize: true. Use TypeORM migrations.
Bash
# Add scripts to package.json (adjust path if needed)# ""typeorm"": ""ts-node ./node_modules/typeorm/cli.js"",# ""migration:generate"": ""npm run typeorm -- migration:generate --dataSource ./src/data-source.ts -n"",# ""migration:run"": ""npm run typeorm -- migration:run --dataSource ./src/data-source.ts""# Create a TypeORM DataSource file (e.g., src/data-source.ts) if you don't have one
npm run migration:generate -- InitialMessageSchema
# Review the generated migration file in the migrations folder
npm run migration:run
(Note: Setting up TypeORM CLI and DataSource is beyond this guide's scope, refer to TypeORM docs)
7. Adding Security Features
Input Validation: Done for /send (DTO + ValidationPipe). Sanitize webhook data before use.
HTTPS Required: Always use HTTPS for webhook endpoints in production.
Webhook Signature Verification (Recommended): MessageBird supports HMAC-SHA256 signature verification for webhooks. When configuring your webhook with a signingKey, MessageBird includes MessageBird-Signature and MessageBird-Request-Timestamp headers in webhook requests. Verify these signatures to ensure authenticity:
Status Reason: Additional context for the status (reported as statusReason in webhook payload)
Error Code: Specific error identifier when applicable (reported as statusErrorCode in webhook payload)
Common status values and their meanings:
accepted: Message accepted by MessageBird API
sent: Message sent to carrier network
buffered: Temporarily held (usually due to carrier issues)
delivered: Successfully delivered to recipient's device
expired: Message expired before delivery (check validity period)
delivery_failed: Delivery failed (check statusReason and statusErrorCode for details)
Time Zones: MessageBird usually provides UTC timestamps. Store in DB using timestamptz (Postgres) or equivalent. Handle time zone conversions carefully.
Duplicate Webhooks: Design processStatusUpdate to be idempotent (safe to run multiple times with the same input). Check current status before updating, or use DB constraints.
Missing References: Log and monitor webhooks arriving without a reference. This signals an issue.
9. Implementing Performance Optimizations
Webhook Response Time: Critical! Ensure /status returns 200 OK quickly. Offload slow tasks (DB writes, external calls) to a background queue (BullMQ, RabbitMQ).
Database Indexing: Index reference, status, messageBirdId as shown in the entity.
Async Operations: Use async/await correctly, avoid blocking the event loop.
Load Testing: Test /send and simulated /status endpoints under load (k6, artillery).
Caching: Generally not needed for the webhook itself, but potentially useful elsewhere.
10. Adding Monitoring, Observability, and Analytics
Health Checks: Use @nestjs/terminus for a /health endpoint (check DB connection, etc.).
Logging: Centralize logs (Datadog, ELK, Loki, CloudWatch). Use structured logging.
Incorrect Status Logic: Verify handling against MessageBird docs.
Sender ID Issues: Use purchased numbers for reliability, check country restrictions.
Rate Limits: Respect MessageBird API limits; implement backoff/queuing for high volume.
Status Delays: Delivery reports can be delayed by carriers; design for asynchronicity.
Frequently asked questions
Track MessageBird SMS delivery status using webhooks. Set up a webhook endpoint in your NestJS application that receives real-time status updates from MessageBird, such as 'sent', 'delivered', or 'failed'. This allows you to monitor message delivery beyond just the initial send confirmation.
The reportUrl parameter in the MessageBird API tells MessageBird where to send delivery status updates for a specific message. It should point to your webhook endpoint, which is typically structured as your-base-url/status-endpoint. This directs the updates to the correct location in your application.
A UUID (Universally Unique Identifier) is crucial for correlating status updates back to the original message. It acts as a unique reference ID, allowing you to link incoming webhook data with the corresponding message you sent, ensuring accurate tracking even with asynchronous delivery.
Secure your webhook endpoint by using HTTPS and considering additional measures like IP whitelisting (restricting access to MessageBird's IP addresses) or a shared secret embedded in the reportUrl and verified upon webhook receipt. Always sanitize incoming webhook data.
The originator is the sender ID that recipients see when they receive your SMS message. It can be a phone number or an alphanumeric string (depending on regulations and MessageBird configuration), and is set using the originator parameter when sending messages.
ngrok is useful during development to expose your local server to the internet so MessageBird can reach your webhook endpoint. For production, use your public server's URL, as ngrok is not suitable for long-term production use cases.
Store your MessageBird API Key securely as an environment variable (e.g., MESSAGEBIRD_API_KEY). Use @nestjs/config to access and use this key in your NestJS application, ensuring you do not expose sensitive information directly in your code.
Use the MessageBird Node.js SDK along with NestJS to send SMS messages. Create a service that interacts with the SDK and a controller with a /send endpoint to handle requests. Ensure to include the reportUrl for status updates and a reference (UUID) for tracking.
The 'accepted' status in MessageBird means that your SMS message has been accepted by MessageBird's platform for processing. It does not guarantee delivery to the recipient but indicates that MessageBird has received and will attempt to send the message. Further status updates will follow.
If webhooks aren't firing, check your ngrok setup for HTTPS URLs, ensure your CALLBACK_BASE_URL and reportUrl are correct, verify your application and endpoint are accessible (firewalls, etc.), and confirm your /status endpoint is defined as a POST method and returns a 200 OK quickly.
Ensure you are correctly passing the unique reference (UUID) when sending the SMS via the MessageBird API. This reference is essential for matching incoming webhook data to the correct outgoing message in your application.
MessageBird expects a swift 200 OK from your webhook endpoint to confirm receipt of the status update. If your endpoint takes too long to respond, MessageBird might retry, potentially leading to duplicate processing. Offload any time-consuming operations to a background queue.
A simple schema with a table to track messages and their latest status is often sufficient. Include fields for a unique ID, the MessageBird reference, recipient, body, status, timestamps, and optionally the raw webhook payload for debugging.
Make your status update processing idempotent, meaning it's safe to run multiple times with the same input without causing unintended side effects. Check the current status in the database before updating or use database constraints to prevent duplicates.
Sending an SMS successfully is just the first step. To build reliable communication workflows, debug issues, and provide accurate feedback to users or internal systems, you need to know when and if that message reaches the recipient's handset. This MessageBird NestJS guide shows you how to implement SMS delivery tracking with webhooks and callbacks to receive and process real-time status updates.
You'll build a NestJS application that sends SMS messages via MessageBird and includes a dedicated webhook endpoint to receive status updates like
sent,delivered, orfailed. This guide covers MessageBird webhook configuration, sending messages with callback parameters, handling incoming status callbacks, storing delivery status updates, and production-ready security best practices.Project Overview and Goals
Goal: Create a NestJS application that sends SMS messages using the MessageBird API and reliably tracks their delivery status through webhooks.
Problem Solved: Gain visibility into SMS delivery beyond the initial API confirmation. Track whether a message was buffered by MessageBird, successfully delivered to the carrier, accepted by the recipient's handset, or failed along the way.
Technologies:
@nestjs/config: Manages environment variables securely.ngrok(development): Exposes your local server to the internet for webhook testing.System Architecture:
Prerequisites:
npm install -g @nestjs/clingrokor similar tunneling service for local development testingWhat You'll Build:
A production-ready NestJS application with:
1. Setting Up the Project
Initialize your NestJS project and install the necessary dependencies.
1. Create NestJS Project:
Open your terminal and run:
nest new messagebird-status-app --strict --package-manager npm cd messagebird-status-appThis creates a new NestJS project with strict TypeScript configuration and uses npm.
2. Install Dependencies:
# MessageBird SDK and configuration management npm install messagebird @nestjs/config dotenv # (Optional) Database integration (using PostgreSQL and TypeORM) # Note: @nestjs/typeorm 10.x+ requires TypeORM 0.3.x # TypeORM does not follow semantic versioning - minor updates may include breaking changes npm install @nestjs/typeorm typeorm pg # (Optional) UUID for generating unique references npm install uuid npm install -D @types/uuid # (Optional) Input validation npm install class-validator class-transformerPackage Versions Note: The
messagebirdpackage (v4.0.1 as of 2024) is the official Node.js SDK. For TypeORM integration, ensure@nestjs/typeormversion 10.x or later is used withtypeorm0.3.x for compatibility.3. Environment Variables Setup:
Use environment variables for sensitive information like API keys. Configure
@nestjs/configto manage these securely.Create a
.envfile in the project root:Important: Replace
YOUR_LIVE_API_KEY,YOUR_MESSAGEBIRD_NUMBER, andYOUR_NGROK_OR_PUBLIC_URLwith your actual values. Obtain the API key from your MessageBird Dashboard (Developers -> API Access -> Live Key). Purchase a number under the "Numbers" section if you haven't already. TheCALLBACK_BASE_URLwill be provided byngroklater.Add
.envto your.gitignorefile to prevent accidentally committing secrets.4. Configure ConfigModule and TypeOrmModule (Optional):
Import and configure
ConfigModulein your main application module (src/app.module.ts). If using a database, configureTypeOrmModule.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; // Import ConfigService import { TypeOrmModule } from '@nestjs/typeorm'; // Uncomment if using DB import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MessagingModule } from './messaging/messaging.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make ConfigService available globally envFilePath: '.env', }), MessagingModule, // Uncomment the following TypeOrmModule section if using a database /* TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ type: 'postgres', host: configService.get<string>('DB_HOST'), port: configService.get<number>('DB_PORT'), username: configService.get<string>('DB_USERNAME'), password: configService.get<string>('DB_PASSWORD'), database: configService.get<string>('DB_DATABASE'), // Use autoLoadEntities for simplicity, or configure paths manually. // If configuring manually, ensure the path points to compiled .js files in production (e.g., 'dist/**/*.entity.js'). autoLoadEntities: true, // synchronize: true MUST NOT be used in production. // It can lead to data loss. Use migrations instead for schema changes. // Recommended ONLY for early development or local testing. synchronize: configService.get<string>('NODE_ENV') !== 'production', // Example: Enable only if not in production }), inject: [ConfigService], }), */ ], controllers: [AppController], providers: [AppService], }) export class AppModule {}5. Project Structure:
The initial structure created by
nest newis suitable. We will add a dedicatedmessagingmodule to handle all MessageBird interactions.2. Implementing Core Functionality (Messaging Module)
We'll create a module responsible for sending messages and handling status callbacks.
1. Generate Module, Service, Controller:
nest g module messaging nest g service messaging # We will add tests later nest g controller messaging --no-specThis creates
src/messaging/messaging.module.ts,src/messaging/messaging.service.ts, andsrc/messaging/messaging.controller.ts.2. Implement
MessagingService:This service will contain the logic for interacting with the MessageBird SDK.
// src/messaging/messaging.service.ts import { Injectable, Logger, InternalServerErrorException, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as MessageBird from 'messagebird'; // Use namespace import import { v4 as uuidv4 } from 'uuid'; // Optional DB integration imports // import { InjectRepository } from '@nestjs/typeorm'; // import { Repository } from 'typeorm'; // import { Message } from './entities/message.entity'; // Corrected import path @Injectable() export class MessagingService implements OnModuleInit { private readonly logger = new Logger(MessagingService.name); private messagebird: MessageBird.MessageBird; // Use MessageBird type private originator: string; private callbackUrl: string; constructor( private configService: ConfigService, // Uncomment if using DB // @InjectRepository(Message) // private messageRepository: Repository<Message>, ) {} onModuleInit() { const apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY'); this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR'); const baseUrl = this.configService.get<string>('CALLBACK_BASE_URL'); if (!apiKey || !this.originator || !baseUrl) { this.logger.error('MessageBird API Key, Originator, or Callback Base URL not configured.'); throw new InternalServerErrorException('Messaging service configuration is incomplete.'); } // Initialize MessageBird SDK this.messagebird = MessageBird(apiKey); this.callbackUrl = `${baseUrl}/messaging/status`; // Construct the full callback URL this.logger.log('MessageBird SDK initialized.'); this.logger.log(`Using Originator: ${this.originator}`); this.logger.log(`Expecting status callbacks at: ${this.callbackUrl}`); } /** * Sends an SMS message via MessageBird and requests status reports. * @param to Recipient phone number (E.164 format expected) * @param body Message content * @returns The unique reference ID generated for this message */ async sendMessage(to: string, body: string): Promise<string> { // Generate a unique reference for this message - CRUCIAL for tracking const reference = uuidv4(); this.logger.log(`Attempting to send SMS to ${to} with reference: ${reference}`); // --- Optional: Persist initial message details (status: 'pending') --- // const newMessage = this.messageRepository.create({ reference, recipient: to, body, status: 'pending' }); // try { // await this.messageRepository.save(newMessage); // this.logger.log(`Saved initial message record with reference ${reference}`); // } catch (dbErr) { // this.logger.error(`Database error saving initial message: ${dbErr.message}`, dbErr.stack); // // Decide how to handle - maybe don't send if DB fails? Or send and log? // } // --- End Optional DB Interaction --- const params: MessageBird.messages.MessageCreateParameters = { originator: this.originator, recipients: [to], body: body, reference: reference, // Include the reference in the request reportUrl: this.callbackUrl, // Tell MessageBird where to POST status updates }; return new Promise((resolve, reject) => { this.messagebird.messages.create(params, async (err, response) => { // Note: async callback for optional DB update if (err) { this.logger.error(`Failed to send SMS via MessageBird: ${err.message}`, err.stack); // --- Optional: Update status to 'failed_to_send' on API error --- // try { // await this.messageRepository.update({ reference }, { status: 'failed_to_send' }); // } catch (dbUpdateErr) { this.logger.error(`Failed to update status to failed_to_send for ${reference}: ${dbUpdateErr.message}`); } // --- End Optional Update --- reject(new InternalServerErrorException(`MessageBird API error: ${err.message}`)); } else { const messageBirdId = response?.id; if (response?.recipients?.items?.length > 0) { this.logger.log(`Message accepted by MessageBird for recipient ${to}. Message ID: ${messageBirdId}, Reference: ${reference}`); // --- Optional: Update status to 'accepted' on successful API call --- // try { // await this.messageRepository.update({ reference }, { status: 'accepted', messageBirdId: messageBirdId }); // this.logger.log(`Message ${reference} accepted. DB status updated.`); // } catch (dbUpdateErr) { this.logger.error(`Failed to update status to accepted for ${reference}: ${dbUpdateErr.message}`); } // --- End Optional Update --- } else { this.logger.warn(`MessageBird response did not contain expected recipient details for ${to}, Reference: ${reference}. Response: ${JSON.stringify(response)}`); // Consider how to handle this - maybe update DB status differently? } resolve(reference); // Return the reference ID on successful API call } }); }); } /** * Processes incoming delivery status updates from MessageBird. * @param statusData The webhook payload from MessageBird */ async processStatusUpdate(statusData: any): Promise<void> { // Make async for potential DB ops const reference = statusData.reference; const status = statusData.status; const statusDatetime = statusData.statusDatetime; let recipient = statusData.recipient; // Can be number or string, sometimes without '+' const messageId = statusData.id; // MessageBird's internal ID if (!reference) { this.logger.warn('Received status update without a reference ID. Cannot correlate.', statusData); return; } this.logger.log(`Received status update for reference ${reference}: Status=${status}, Recipient=${recipient}, Time=${statusDatetime}, MsgID=${messageId}`); // --- Database Update Logic --- // Here, you would typically find the message record in your database // using the 'reference' and update its status and statusUpdatedAt fields. // **Normalization Note:** If storing or looking up by recipient, ensure consistent formatting. // MessageBird might send `recipient` without a leading '+'. Consider normalizing // to E.164 format (e.g., using a library like 'libphonenumber-js') before DB operations. // Example (pseudo-code): const normalizedRecipient = normalizeToE164(recipient); // Example DB update (uncomment and adapt if using TypeORM): /* try { const message = await this.messageRepository.findOne({ where: { reference } }); if (message) { message.status = status; message.statusUpdatedAt = new Date(statusDatetime); // Ensure correct parsing/type message.messageBirdId = messageId ?? message.messageBirdId; // Update MB ID if present message.lastRawStatus = statusData; // Optional: Store raw payload for debugging // Potentially update the recipient field if needed after normalization await this.messageRepository.save(message); this.logger.log(`Updated status for reference ${reference} in DB.`); } else { this.logger.warn(`Could not find message with reference ${reference} in DB to update status.`); } } catch (dbErr) { this.logger.error(`Database error updating status for reference ${reference}: ${dbErr.message}`, dbErr.stack); // IMPORTANT: Still return OK to MessageBird below, but log the DB error for investigation. // Consider pushing to a dead-letter queue for retry if DB update fails. } */ // --- End Database Update Logic --- } }Key Points:
onModuleInit): Initializes the SDK usingConfigService.uuidv4): Critical for correlating status updates. Generated for each message.reportUrlParameter: Tells MessageBird where to POST status updates for this specific message.async/awaitusage.processStatusUpdate: Handles incoming webhook data. Extracts key fields, logs them, and includes placeholder logic for database updates. Normalization of the recipient number is noted as a potential requirement.3. Implement
MessagingController:Defines API endpoints for sending messages and receiving status updates.
// src/messaging/messaging.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, Logger, ValidationPipe, UsePipes } from '@nestjs/common'; import { MessagingService } from './messaging.service'; import { SendMessageDto } from './dto/send-message.dto'; // We'll create this DTO next @Controller('messaging') export class MessagingController { private readonly logger = new Logger(MessagingController.name); constructor(private readonly messagingService: MessagingService) {} /** * Endpoint to send an SMS message. */ @Post('send') @HttpCode(HttpStatus.ACCEPTED) // 202 Accepted is suitable as delivery is async @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) async sendMessage(@Body() sendMessageDto: SendMessageDto): Promise<{ message: string; reference: string }> { this.logger.log(`Received request to send SMS to ${sendMessageDto.to}`); const reference = await this.messagingService.sendMessage( sendMessageDto.to, sendMessageDto.body, ); return { message: 'SMS send request accepted by MessageBird.', reference: reference, // Return the unique reference }; } /** * Webhook endpoint to receive delivery status updates from MessageBird. * MessageBird expects a 2xx response quickly. Offload heavy processing if needed. */ @Post('status') @HttpCode(HttpStatus.OK) // Respond with 200 OK immediately to acknowledge receipt async handleStatusWebhook(@Body() statusData: any): Promise<void> { // Log the raw incoming data for debugging (optional) // this.logger.debug('Received MessageBird status webhook:', JSON.stringify(statusData, null, 2)); // No validation DTO here initially, as MessageBird's payload is fixed, // but you could add one for stricter typing or basic checks if desired. // Asynchronously process the update. Don't await if processing might be slow. // Await is safe here ONLY if processStatusUpdate is guaranteed to be fast (e.g., only logging). // For DB operations, consider running without await and handling errors internally, // or push to a queue. await this.messagingService.processStatusUpdate(statusData); // IMPORTANT: Respond quickly! This endpoint must return 200 OK fast. // If processStatusUpdate involves slow operations (DB writes, external calls), // do NOT await it here. Instead, trigger it asynchronously: // this.messagingService.processStatusUpdate(statusData).catch(err => { // this.logger.error('Error processing status update asynchronously:', err); // }); // OR push statusData to a message queue (e.g., BullMQ, RabbitMQ) for background processing. } }Key Points:
/sendEndpoint: Accepts POST requests, validates input usingSendMessageDto, calls the service, returns202 Acceptedwith thereference./statusEndpoint: Accepts POST requests from MessageBird. It passes data to the service and must return200 OKquickly. Slow processing should be handled asynchronously (background job queue recommended)./sendrequest body adheres toSendMessageDto.4. Create DTO (Data Transfer Object):
Defines the expected request body for the
/sendendpoint.// src/messaging/dto/send-message.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber } from 'class-validator'; export class SendMessageDto { @IsNotEmpty() @IsPhoneNumber(null) // Use 'null' for generic E.164 format validation (e.g., +1xxxxxxxxxx) @IsString() readonly to: string; @IsNotEmpty() @IsString() readonly body: string; }3. Building a Complete API Layer
Our core API endpoints (
/messaging/sendand/messaging/status) are defined. Let's refine them.Authentication/Authorization:
reportUrland verify it.referencefor correlation. Rigorously sanitize input. If available and feasible, implement signature verification or IP whitelisting.Request Validation:
Already implemented for
/sendusingclass-validator.API Documentation:
Consider using
@nestjs/swaggerto generate OpenAPI documentation for the/sendendpoint.Testing Endpoints:
/send Endpoint:
# Ensure your NestJS app is running (npm run start:dev) curl -X POST http://localhost:3000/messaging/send \ -H 'Content-Type: application/json' \ -d '{ "to": "+12025550199", "body": "Hello from NestJS and MessageBird!" }' # Expected Response (Example): # { # "message": "SMS send request accepted by MessageBird.", # "reference": "a1b2c3d4-e5f6-7890-1234-567890abcdef" # }/status Endpoint: Test by sending a real message and observing logs/DB, or simulate a callback:
# Simulate a 'delivered' status update (Replace <your-ngrok-url> and reference) curl -X POST https://<your-ngrok-url>/messaging/status \ -H 'Content-Type: application/json' \ -d '{ "id": "mb-message-id-123", "href": "...", "reference": "YOUR_MESSAGE_REFERENCE_ID", "status": "delivered", "statusDatetime": "2025-04-20T10:30:00Z", "recipient": 31612345678, "originator": "YourNumber", "message": "Hello World" }' # Expected Response: HTTP/1.1 200 OK (with an empty body)4. Integrating with MessageBird
Configuration:
.env(MESSAGEBIRD_API_KEY)..env(MESSAGEBIRD_ORIGINATOR, E.164 format).reportUrl):referencedefined when sending AND a status report URL set viareportUrl(per-message) or configured globally in your account settings.ngrok http 3000. Copy HTTPS URL ->.env(CALLBACK_BASE_URL). Full URL is${CALLBACK_BASE_URL}/messaging/status.https://api.yourdomain.com) asCALLBACK_BASE_URL.reportUrlper message (as implemented) offers more control and flexibility.Environment Variables Summary:
MESSAGEBIRD_API_KEY: Authenticates API requests.MESSAGEBIRD_ORIGINATOR: Sender ID for outgoing SMS.CALLBACK_BASE_URL: Public base URL for constructing thereportUrl.Fallback Mechanisms:
sendMessagecall in case of transient MessageBird API errors./statusendpoint fails to return2xxquickly.5. Error Handling, Logging, and Retry Mechanisms
Error Handling Strategy:
sendMessage. Log details. Return appropriate HTTP errors from/send. Optionally update DB status to indicate sending failure.try...catchinprocessStatusUpdate. Log internal errors (DB issues, etc.) but always return200 OKto MessageBird. Handle the failure internally (log, dead-letter queue).ValidationPipefor/send(returns 400).Logging:
Logger.log,warn,error).Retry Mechanisms:
/statusfast and reliable. MessageBird handles retries if needed. Use a background queue for complex processing.6. Creating a Database Schema and Data Layer (Optional)
Persistence is needed for tracking. Here's a simplified TypeORM/PostgreSQL example.
Simplified Schema:
For the scope of this guide, a single entity to track the message and its latest status is often sufficient.
2. TypeORM Entity:
// src/messaging/entities/message.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'; @Entity('messages') // Ensure table name matches your DB export class Message { @PrimaryGeneratedColumn('uuid') id: string; // Critical index for finding the message based on the webhook reference @Index({ unique: true }) @Column({ type: 'uuid' }) reference: string; // Indexing MessageBird's ID can be useful for reconciliation @Index() @Column({ nullable: true }) messageBirdId: string; @Column() recipient: string; // Store normalized E.164 format if possible @Column({ type: 'text' }) body: string; // Index status for efficient querying (e.g., find all 'failed' messages) @Index() @Column({ default: 'pending' }) status: string; // e.g., pending, accepted, buffered, sent, delivered, expired, delivery_failed @CreateDateColumn() createdAt: Date; // Use timestamptz for PostgreSQL to store timezone info (recommended) @Column({ type: 'timestamptz', nullable: true }) statusUpdatedAt: Date; // Store the last raw status payload (JSONB is efficient in PostgreSQL) @Column({ type: 'jsonb', nullable: true }) lastRawStatus: any; @UpdateDateColumn() // Automatically updated by TypeORM on save/update updatedAt: Date; }3. Integrate with Service:
Inject the
Messagerepository (@InjectRepository(Message)) intoMessagingServiceconstructor. Uncomment and adapt the database interaction logic withinsendMessageandprocessStatusUpdateas shown in the service code comments (Section 2).4. Migrations:
Strongly recommended for production. Avoid
synchronize: true. Use TypeORM migrations.# Add scripts to package.json (adjust path if needed) # ""typeorm"": ""ts-node ./node_modules/typeorm/cli.js"", # ""migration:generate"": ""npm run typeorm -- migration:generate --dataSource ./src/data-source.ts -n"", # ""migration:run"": ""npm run typeorm -- migration:run --dataSource ./src/data-source.ts"" # Create a TypeORM DataSource file (e.g., src/data-source.ts) if you don't have one npm run migration:generate -- InitialMessageSchema # Review the generated migration file in the migrations folder npm run migration:run(Note: Setting up TypeORM CLI and DataSource is beyond this guide's scope, refer to TypeORM docs)
7. Adding Security Features
/send(DTO +ValidationPipe). Sanitize webhook data before use./send(API Key/JWT Guard).HTTPS Required: Always use HTTPS for webhook endpoints in production.
Webhook Signature Verification (Recommended): MessageBird supports HMAC-SHA256 signature verification for webhooks. When configuring your webhook with a
signingKey, MessageBird includesMessageBird-SignatureandMessageBird-Request-Timestampheaders in webhook requests. Verify these signatures to ensure authenticity:// Example signature verification (add to webhook handler) import * as crypto from 'crypto'; function verifyMessageBirdSignature( signature: string, timestamp: string, url: string, body: string, signingKey: string ): boolean { const bodyHash = crypto.createHash('sha256').update(body).digest('hex'); const payload = `${timestamp}\n${url}\n${bodyHash}`; const expectedSignature = crypto .createHmac('sha256', signingKey) .update(payload) .digest('hex'); return signature === expectedSignature; }Shared Secret: As an alternative, add a secret query parameter to
reportUrland verify it.IP Whitelisting: Allow only MessageBird's webhook IPs (requires infrastructure setup).
Reference Validation: Always verify that incoming webhook
referenceIDs exist in your system before processing.8. Handling Special Cases
Status Meanings: MessageBird provides three complementary levels of status information that should be analyzed together:
scheduled,sent,buffered,delivered,expired,delivery_failed)statusReasonin webhook payload)statusErrorCodein webhook payload)Common status values and their meanings:
accepted: Message accepted by MessageBird APIsent: Message sent to carrier networkbuffered: Temporarily held (usually due to carrier issues)delivered: Successfully delivered to recipient's deviceexpired: Message expired before delivery (check validity period)delivery_failed: Delivery failed (checkstatusReasonandstatusErrorCodefor details)Time Zones: MessageBird usually provides UTC timestamps. Store in DB using
timestamptz(Postgres) or equivalent. Handle time zone conversions carefully.Duplicate Webhooks: Design
processStatusUpdateto be idempotent (safe to run multiple times with the same input). Check current status before updating, or use DB constraints.Missing References: Log and monitor webhooks arriving without a
reference. This signals an issue.9. Implementing Performance Optimizations
/statusreturns200 OKquickly. Offload slow tasks (DB writes, external calls) to a background queue (BullMQ, RabbitMQ).reference,status,messageBirdIdas shown in the entity.async/awaitcorrectly, avoid blocking the event loop./sendand simulated/statusendpoints under load (k6, artillery).10. Adding Monitoring, Observability, and Analytics
@nestjs/terminusfor a/healthendpoint (check DB connection, etc.)./send,/status), status distribution, queue lengths (Prometheus, Datadog).@sentry/node) or similar for detailed error reporting.11. Troubleshooting and Caveats
ngrok(HTTPS),CALLBACK_BASE_URL,reportUrlin API call, app accessibility, firewalls,/statusendpoint definition (POST), MessageBird dashboard logs, quick200 OKresponse.referenceis passed correctly in API call. Check raw webhook payload.referenceexists.