MessageBird Webhook Tutorial: Track SMS Delivery Status with Node.js Express
Build a production-ready MessageBird webhook system for real-time SMS delivery tracking. Complete Node.js Express tutorial with HMAC signature verification, status code handling, MongoDB integration, and ngrok testing setup.
Build a production-ready webhook system to track SMS delivery status in real time using MessageBird's REST API. This comprehensive tutorial shows you how to send SMS messages and receive instant delivery updates through MessageBird webhooks using Node.js and Express. You'll implement HMAC-SHA256 signature verification, status code handling, retry logic, and production security with complete working code examples.
What You'll Build
Create an SMS tracking system with:
MessageBird SMS API Integration: Send messages programmatically through the official Node.js SDK
Webhook Endpoint: Receive real-time delivery status updates from MessageBird
Signature Verification: Secure your webhooks with HMAC-SHA256 cryptographic validation
Database Tracking: Store message status in MongoDB for audit trails and analytics
Ngrok Tunneling: Expose your local Express server for webhook testing
Production Security: Implement rate limiting, HTTPS enforcement, and replay attack prevention
Why Use Webhooks for SMS Delivery Tracking?
Webhook vs. polling performance comparison:
Metric
Webhooks
API Polling (every 30s)
Average notification delay
<2 seconds
15 seconds (average)
API requests per 1,000 messages
1–5 (webhook deliveries)
2,000+ (polling calls)
Server overhead
Minimal (passive receiver)
High (active polling loop)
Scalability
Linear (no polling load)
Degrades with message volume
Cost efficiency
Free webhook notifications
Consumes API quota/rate limits
Webhooks deliver immediate notification when message status changes, eliminating polling. MessageBird sends instant updates when messages are sent, delivered, failed, or expired.
Benefits of webhook-based delivery tracking:
Real-time status updates: Receive notifications within seconds of status changes
Reduced API calls: Eliminate repeated polling of status endpoints
Cost efficiency: Minimize API requests and bandwidth usage
Better user experience: Update customers immediately about message delivery
Comprehensive tracking: Capture all status transitions including intermediate states
Scalability: Handle high-volume messaging without performance degradation
How to Set Up Your MessageBird Node.js Project
System requirements:
Node.js 18+ (minimum 0.10; 18.18 LTS recommended for production)
npm or yarn package manager
MongoDB 4.0+ for message tracking
Valid MessageBird account with API access
According to MessageBird's official Node.js documentation, the MessageBird Node.js SDK requires Node.js 0.10 at minimum. For production deployments in 2025, use Node.js 18.18 LTS or later for security updates and performance improvements.
Initialize your Node.js project and install dependencies:
Security note: Never commit your .env file to version control. Add it to .gitignore immediately.
How to Send SMS Messages with MessageBird
Create a function to send SMS using the MessageBird SDK. Before receiving webhook delivery updates, send SMS messages:
JavaScript
// sendSMS.jsrequire('dotenv').config();
const messagebird = require('messagebird').initClient(process.env.MESSAGEBIRD_ACCESS_KEY);
asyncfunctionsendSMS(recipient, message, originator = 'YourBrand') {
returnnewPromise((resolve, reject) => {
messagebird.messages.create({
originator: originator, // Sender ID (max 11 alphanumeric chars)recipients: [recipient], // Array of E.164 formatted numbersbody: message // Message content (160 chars per SMS part)
}, (err, response) => {
if (err) {
console.error('MessageBird API Error:', err);
returnreject(err);
}
console.log('✓ Message sent successfully');
console.log('Message ID:', response.id);
console.log('Recipients:', response.recipients);
resolve(response);
});
});
}
// Example usagesendSMS('+1234567890', 'Hello from MessageBird!')
.then(response => {
console.log('Message ID for tracking:', response.id);
})
.catch(error => {
console.error('Failed to send SMS:', error);
});
module.exports = sendSMS;
Key parameters explained:
originator: Sender ID displayed to recipients. Can be alphanumeric (max 11 chars) or E.164 phone number. Note: Alphanumeric sender IDs are not supported in all countries including the United States. Use a purchased phone number for US messaging.
recipients: Array of phone numbers in E.164 format (e.g., +14155552671)
body: Message content. SMS messages are limited to 160 characters. Longer messages split into multiple parts, each billed separately.
Test the SMS function:
Bash
node sendSMS.js
When the message sends successfully, MessageBird returns a message ID. This ID appears in webhook payloads for delivery status tracking. According to MessageBird's SMS tutorial, you can track message delivery through webhooks or by polling the Messages API. For more information on phone number formatting, see the E.164 phone number format guide.
How to Verify Webhook Signatures with HMAC-SHA256
MessageBird signs all webhook requests with HMAC-SHA256 cryptographic signatures to prevent unauthorized access and replay attacks. Always verify webhook signatures before processing the payload to ensure security.
Understanding Webhook Signature Verification
Webhook signature verification ensures incoming requests originate from MessageBird and haven't been tampered with. MessageBird generates a cryptographic signature using your signing key and includes it in the MessageBird-Signature-JWT header.
Why signature verification matters:
Prevents spoofing: Blocks malicious actors from sending fake webhook requests
Data integrity: Confirms the payload hasn't been modified during transmission
Replay attack prevention: Detects and blocks duplicate webhook deliveries
Compliance: Meets security requirements for PCI DSS and GDPR
How to Implement Signature Verification
Create a middleware file to verify all incoming webhooks:
JavaScript
// middleware/webhookVerify.jsconst mbWebhookSignatureJwt = require('messagebird/lib/webhook-signature-jwt');
functioncreateWebhookVerifier() {
const signingKey = process.env.MESSAGEBIRD_SIGNING_KEY;
if (!signingKey) {
thrownewError('MESSAGEBIRD_SIGNING_KEY environment variable is required');
}
const verifyOptions = new mbWebhookSignatureJwt.VerifyOptions();
// Add jti (JWT ID) verification to prevent replay attacks// Note: In-memory Set works for single-server deployments// For production clusters, use Redis or similar distributed cacheconst seenJtis = newSet();
verifyOptions.jwtVerifyJti = (jti) => {
// Check if this JWT ID has been processed beforeif (seenJtis.has(jti)) {
console.warn(`Duplicate webhook detected with JTI: ${jti}`);
returnfalse; // Reject duplicate
}
// Mark this JWT ID as processed
seenJtis.add(jti);
// In production: implement TTL cleanup to prevent memory growth// Webhooks expire after ~5 minutes, so JTIs older than 1 hour can be purgedreturntrue;
};
// Return Express middleware that validates signature automaticallyreturnnew mbWebhookSignatureJwt.ExpressMiddlewareVerify(signingKey, verifyOptions);
}
module.exports = createWebhookVerifier;
Important security considerations:
Signing key location: Find your signing key in the MessageBird Dashboard under Developers → Settings → Webhooks
JWT structure: MessageBird uses JWT format with these claims: url_hash, payload_hash, jti, nbf, exp
Expiration: Webhooks include an exp (expiration) claim to prevent processing stale requests
JTI tracking limitations: The in-memory Set works for single-server deployments but doesn't persist across restarts. For production clusters with multiple servers, store JTIs in Redis with 1-hour TTL. Memory usage: ~50 bytes per JTI; 1 million webhooks ≈ 50 MB memory.
Error handling: Return 401 Unauthorized for invalid signatures to signal MessageBird for retry
Signature verification: Applied via middleware before processing payload
Idempotency:findOneAndUpdate with upsert prevents duplicate processing
Error handling: Returns appropriate HTTP status codes to signal retry behavior
Response timing: Returns 200 OK quickly to prevent MessageBird timeouts (respond within 5 seconds)
Database indexing:messageId field is unique and indexed for fast lookups
Send/Receive integration:/api/send-sms endpoint sends messages and stores initial records; webhook updates delivery status
How to Test Webhooks Locally with Ngrok
Ngrok creates a secure HTTPS tunnel to your local development server, enabling MessageBird to deliver webhook callbacks during testing. Learn more about webhook testing best practices in MessageBird's developer documentation.
Understanding Ngrok for Local Webhook Testing
Ngrok exposes your localhost server to the internet with a public URL. MessageBird requires HTTPS webhooks, which ngrok provides automatically with valid SSL certificates.
Install and start ngrok:
Bash
# Install ngrok globally
npm install -g ngrok
# Start ngrok tunnel on port 3000
ngrok http 3000
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 1000, // Limit each IP to 1,000 requests per windowmessage: 'Too many webhook requests',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/webhooks/delivery-status', webhookLimiter, webhookVerifier, async (req, res) => {
// Webhook handler code
});
3. Timestamp Validation
Integrate timestamp validation into webhook handler to reject stale requests:
JavaScript
functionvalidateTimestamp(timestamp, maxAgeSeconds = 300) {
const now = Math.floor(Date.now() / 1000);
const age = now - timestamp;
return age >= 0 && age <= maxAgeSeconds;
}
// Use in webhook handler
app.post('/webhooks/delivery-status', webhookVerifier, async (req, res) => {
try {
const payload = req.body;
// Extract timestamp from JWT claims (nbf or iat)// MessageBird SDK validates expiration automatically// Additional check for message freshnessif (payload.message?.statusDatetime) {
const statusTimestamp = newDate(payload.message.statusDatetime).getTime() / 1000;
if (!validateTimestamp(statusTimestamp, 3600)) { // 1 hour max ageconsole.warn('Webhook payload too old, rejecting');
return res.status(400).json({ error: 'Stale webhook' });
}
}
// Continue processing...
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
4. IP Allowlisting
Important note: According to MessageBird's official documentation, MessageBird's REST API uses dynamic IP addresses from globally distributed infrastructure, making IP allowlisting impractical. MessageBird recommends using signature verification (covered above) instead of IP filtering.
Alternative: Signature verification is mandatory
MessageBird explicitly states: "It will not be possible to whitelist the IP range for our REST API since our IP addresses are dynamic." Instead, always implement webhook signature verification using the signing key method described in this guide. This provides stronger security than IP allowlisting while accommodating MessageBird's distributed architecture.
If your organization requires network-level security, implement these alternatives:
Use a reverse proxy (nginx, Cloudflare) with rate limiting
Deploy webhook endpoints behind a VPN or private network
Implement request logging and anomaly detection
Monitor webhook activity in MessageBird Dashboard for suspicious patterns
5. JTI Persistence with Redis
For production deployments with multiple servers, store JWT IDs in Redis:
Bash
npm install redis
Implement distributed JTI tracking:
JavaScript
const redis = require('redis');
// Create Redis client with connection handlingconst client = redis.createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
socket: {
reconnectStrategy: (retries) => {
if (retries > 10) {
returnnewError('Redis connection failed');
}
return retries * 100; // Exponential backoff
}
}
});
client.on('error', (err) => {
console.error('Redis client error:', err);
});
client.on('connect', () => {
console.log('✓ Redis connected');
});
// Connect to Redis
client.connect().catch(console.error);
// Check and store JTI with automatic expirationasyncfunctioncheckJti(jti) {
try {
const key = `jti:${jti}`;
const exists = await client.get(key);
if (exists) {
console.warn(`Duplicate JTI detected: ${jti}`);
returnfalse; // Reject duplicate
}
// Store JTI with 1-hour TTL (3,600 seconds)// Webhooks expire after ~5 minutes, 1 hour provides safety marginawait client.setEx(key, 3600, '1');
returntrue; // Accept new JTI
} catch (error) {
console.error('Redis JTI check error:', error);
// Fail open: allow webhook if Redis is down// Alternative: fail closed by returning falsereturntrue;
}
}
// Update webhook verifier to use RedisfunctioncreateWebhookVerifier() {
const signingKey = process.env.MESSAGEBIRD_SIGNING_KEY;
if (!signingKey) {
thrownewError('MESSAGEBIRD_SIGNING_KEY environment variable is required');
}
const verifyOptions = new mbWebhookSignatureJwt.VerifyOptions();
// Use Redis for distributed JTI verification
verifyOptions.jwtVerifyJti = async (jti) => {
returnawaitcheckJti(jti);
};
returnnew mbWebhookSignatureJwt.ExpressMiddlewareVerify(signingKey, verifyOptions);
}
module.exports = { createWebhookVerifier, redisClient: client };
Connection refused: Endpoint unreachable – verify URL and firewall rules
Testing Webhook Endpoints Locally
Important note: Testing webhooks with curl requires generating valid JWT signatures. The MessageBird SDK handles signature generation automatically for real webhooks. For local testing, use these approaches:
Option 1: Bypass signature verification for testing (NOT for production):
JavaScript
// test-webhook.jsconst express = require('express');
const app = express();
app.use(express.json());
// Test endpoint WITHOUT signature verification
app.post('/webhooks/delivery-status-test', async (req, res) => {
console.log('Test webhook received:', JSON.stringify(req.body, null, 2));
res.status(200).json({ received: true });
});
app.listen(3001, () => {
console.log('Test server running on port 3001');
});
Option 2: Send real SMS and monitor webhooks (recommended):
Configure ngrok tunnel to your local server
Add ngrok URL to MessageBird Dashboard webhooks
Send test SMS via /api/send-sms endpoint
Monitor webhook deliveries in real-time with proper signatures
Bash
# Terminal 1: Start server
node server.js
# Terminal 2: Start ngrok
ngrok http 3000
# Terminal 3: Send test SMS
curl -X POST http://localhost:3000/api/send-sms \
-H "Content-Type: application/json" \
-d '{
"recipient": "+1234567890",
"message": "Test message",
"originator": "TestApp"
}'# Monitor webhooks in Terminal 1 server logs
Testing checklist:
Test all status codes: sent, delivered, delivery_failed, expired
Verify signature validation rejects invalid JWTs
Test duplicate webhook handling with same JTI
Simulate network failures with delayed responses
Monitor database updates for each webhook
Verify retry logic for temporary failures
Frequently Asked Questions
How do I verify webhook signatures?
MessageBird signs webhooks with HMAC-SHA256 and sends the signature in the MessageBird-Signature-JWT header. Use the official MessageBird SDK's webhook-signature-jwt module to verify signatures automatically. The SDK validates the JWT structure, expiration, and payload hash. Implement JTI (JWT ID) verification to prevent replay attacks by tracking processed JWT IDs in memory or Redis.
What are MessageBird's webhook retry policies?
MessageBird retries failed webhook deliveries using exponential backoff. If your endpoint returns a non-2xx status code or times out, MessageBird retries up to 10 times over 24 hours. The retry schedule: immediate, 1 minute, 5 minutes, 30 minutes, 1 hour, 3 hours, 6 hours, 12 hours, and 24 hours. Return HTTP 200 OK for successful processing, even if business logic fails internally.
What SMS delivery status codes does MessageBird provide?
MessageBird provides three status levels: status (primary), statusReason (secondary), and statusErrorCode (tertiary). Primary statuses: sent, buffered, delivered, delivery_failed, and expired. Status reasons: absent_subscriber, invalid_destination, unknown_subscriber, rejected_by_carrier, and blacklisted. Error codes: 0 (no error), 5 (permanent failure), 6 (temporary failure), 9 (unknown error).
How do I handle duplicate webhooks?
Implement idempotent webhook processing using the message ID as a unique identifier. Use database operations like findOneAndUpdate with upsert to handle duplicates safely. Track JWT IDs (jti claim) in memory or Redis to detect and reject duplicate deliveries. MessageBird may send the same webhook multiple times during network issues or retries.
Why is my webhook endpoint timing out?
MessageBird requires webhook endpoints to respond within 5 seconds. Return HTTP 200 OK immediately after receiving the webhook, then process heavy operations asynchronously. Use job queues like Bull or BullMQ for background processing:
Optimize database queries with proper indexing. Avoid synchronous external API calls in webhook handlers. Monitor endpoint response times with APM tools.
What security measures should I implement for webhooks?
Implement these security measures: (1) Always verify webhook signatures using the signing key, (2) Enforce HTTPS in production, (3) Apply rate limiting to prevent abuse, (4) Validate timestamp freshness (reject requests older than 5 minutes), (5) Track JWT IDs to prevent replay attacks, (6) Note that IP allowlisting is not recommended by MessageBird due to dynamic IPs, (7) Implement request logging for audit trails.
How do I test webhooks locally?
Use ngrok to create a secure HTTPS tunnel to your local server: ngrok http 3000. Copy the ngrok HTTPS URL and configure it in MessageBird Dashboard under Developers → Webhooks. Start your Express server and send test SMS messages. Monitor webhook deliveries in the ngrok inspector (http://localhost:4040) and your server logs. Update the webhook URL in MessageBird Dashboard whenever ngrok restarts (free tier generates new URLs).
What should I do when webhook delivery fails?
Check the webhook activity logs in MessageBird Dashboard under Developers → Webhooks → Activity. Common failures: (1) 401 Unauthorized – verify signing key matches dashboard, (2) Timeout – optimize endpoint to respond within 5 seconds, (3) Connection refused – verify endpoint URL and server status, (4) 500 errors – check application logs for exceptions. MessageBird automatically retries failed deliveries up to 10 times.
Can I use webhooks without signature verification?
No. Always implement signature verification to prevent unauthorized access and spoofed webhook requests. MessageBird provides the signing key and official SDK for verification. Skipping signature verification exposes your system to malicious actors who could send fake delivery status updates, corrupt your database, or trigger unauthorized actions. Signature verification is a security requirement, not optional.
How do I migrate from API polling to webhooks?
Replace polling logic with webhook handlers: (1) Create webhook endpoint in Express, (2) Implement signature verification middleware, (3) Configure webhook URL in MessageBird Dashboard, (4) Remove polling code that calls messages.read() repeatedly, (5) Update database schema to track webhook deliveries, (6) Test with ngrok for local development, (7) Deploy with HTTPS enabled. Webhooks eliminate API rate limit concerns and provide real-time updates.
Summary: Building Production-Ready SMS Webhooks
You've built a complete MessageBird webhook system with Node.js and Express that tracks SMS delivery status in real time. The system includes cryptographic signature verification using HMAC-SHA256, comprehensive status code handling across three levels (status, statusReason, statusErrorCode), MongoDB persistence for audit trails, and production security with rate limiting and replay attack prevention.
Key implementation requirements:
Always verify webhook signatures using the MessageBird-Signature-JWT header
Return HTTP 200 OK within 5 seconds to prevent MessageBird retries
Implement idempotent processing with database upserts on message ID
Track JWT IDs to detect and reject duplicate webhook deliveries
Handle all status codes appropriately: delivered (success), delivery_failed (retry or mark invalid), expired (investigate)
Production deployment checklist:
Configure HTTPS webhook endpoint with valid SSL certificate
Update MessageBird Dashboard with production webhook URL
Implement rate limiting (1,000 requests per 15 minutes recommended)
Deploy Redis for distributed JTI tracking across multiple servers
Enable monitoring with APM tools (New Relic, Datadog, Application Insights)
Set up alerting for webhook delivery failures
Document signing key rotation procedures
Test failover scenarios and backup webhook endpoints
Review MessageBird activity logs weekly for anomalies
Create .env file with MessageBird API keys from dashboard
Start MongoDB and Redis services locally
Run the Express server: node server.js
Set up ngrok tunnel for testing: ngrok http 3000
Configure webhook URL in MessageBird Dashboard
Send test SMS and monitor webhook deliveries
Deploy to production with HTTPS and monitoring
Performance expectations:
Webhook latency: <500ms per request (database update + response)
Throughput: 1,000+ webhooks per second on single Express instance
Memory usage: ~50 MB base + ~50 bytes per cached JTI
Database load: One upsert query per webhook (use connection pooling)
Scaling: Horizontally scalable with Redis for JTI tracking
Next steps for enhancement:
Implement retry logic for temporary delivery failures (statusErrorCode 6)
Add webhook event logging for compliance and debugging
Build dashboard for real-time delivery analytics
Integrate with notification systems for critical failures
Implement dead letter queue for unprocessable webhooks
Add webhook payload validation with JSON schema
Create automated tests for webhook endpoint
The webhook system provides real-time visibility into SMS delivery, eliminates polling overhead, and scales efficiently for high-volume messaging. Monitor delivery rates, optimize for carrier-specific issues, and continuously review security practices to maintain reliable SMS communications.
Frequently asked questions
Use the MessageBird Node.js SDK and Express.js to create a POST route that handles sending SMS messages. This route should extract recipient and message details from the request body, construct the necessary parameters for the MessageBird API, and use the SDK's messages.create() method to send the SMS. Remember to store the returned message ID for tracking delivery status.
A webhook is a mechanism for receiving real-time updates from MessageBird about the status of your sent messages (like 'delivered', 'sent', or 'failed'). MessageBird sends an HTTP POST request to a URL you specify, containing the status details. In your application, create an Express route to handle these incoming requests.
Dotenv loads environment variables from a .env file. This is essential for securely storing sensitive information, like your MessageBird API key, and preventing it from being exposed in your source code or version control systems like Git. Add your API key to the .env file with MESSAGEBIRD_API_KEY=YOUR_API_KEY.
ngrok is useful during development to expose your local server to the internet, allowing you to receive MessageBird webhooks. Since webhooks require a publicly accessible URL, ngrok creates a secure tunnel. It's a development tool; in production, you would use your deployed server's public URL.
Yes, the MessageBird API supports sending SMS messages to multiple recipients. In the recipients parameter of your messagebird.messages.create request, provide an array of phone numbers in E.164 format. Ensure you handle responses and status updates for each recipient individually if needed.
Implement a webhook handler in your Express app as a POST route (e.g., /messagebird-status). This route will receive status updates from MessageBird. Extract the message ID from the request body to correlate it with the original message you sent. Store status updates in a database for persistent tracking.
The message ID is a unique identifier assigned by MessageBird to each SMS message you send. It's crucial for correlating delivery status updates received via webhooks with the original message. You'll use this ID to look up and update the message status in your database.
Acknowledge the webhook by responding with a 2xx HTTP status (e.g., 200 OK) even if errors occur in your processing logic. This prevents MessageBird from retrying the webhook unnecessarily. Log internal errors thoroughly for investigation and implement retry mechanisms or queueing where applicable.
You should use a persistent database for production applications. The tutorial demonstrates setting up MongoDB (install mongodb driver, connect, insert, update). Other suitable databases include PostgreSQL, MySQL, or cloud-based solutions like MongoDB Atlas or Google Cloud Firestore.
In the MessageBird dashboard, go to Developers > API access or Flow Builder, then configure the URL for your webhook handler. This URL should be publicly accessible, so you may use ngrok for local development. In production, use your server's domain/IP + route path. The webhook receives POST requests from MessageBird.
E.164 is an international standard phone number format (+[country code][number]). It's essential for ensuring compatibility and proper delivery of SMS messages globally. Always format numbers in E.164 before sending them to the MessageBird API.
Use a test API key during development to integrate with MessageBird's platform without incurring real SMS costs. Test keys help verify that API requests are structured correctly and that webhook setups are working as expected. Switch to your live API key once you're ready to send actual messages.
Use npm or yarn to install the required packages. The article outlines these: npm install express messagebird dotenv for the main dependencies and npm install --save-dev nodemon for development tools (nodemon auto-restarts your server during coding).
Nodemon automatically restarts your Node.js server whenever you make changes to your code. It's a development convenience tool that saves you from manually restarting the server every time you edit a file, speeding up development considerably. Install with npm as dev dependency.
Build a production-ready webhook system to track SMS delivery status in real time using MessageBird's REST API. This comprehensive tutorial shows you how to send SMS messages and receive instant delivery updates through MessageBird webhooks using Node.js and Express. You'll implement HMAC-SHA256 signature verification, status code handling, retry logic, and production security with complete working code examples.
What You'll Build
Create an SMS tracking system with:
Why Use Webhooks for SMS Delivery Tracking?
Webhook vs. polling performance comparison:
Webhooks deliver immediate notification when message status changes, eliminating polling. MessageBird sends instant updates when messages are sent, delivered, failed, or expired.
Benefits of webhook-based delivery tracking:
How to Set Up Your MessageBird Node.js Project
System requirements:
According to MessageBird's official Node.js documentation, the MessageBird Node.js SDK requires Node.js 0.10 at minimum. For production deployments in 2025, use Node.js 18.18 LTS or later for security updates and performance improvements.
Initialize your Node.js project and install dependencies:
mkdir messagebird-webhooks cd messagebird-webhooks npm init -y npm install messagebird express body-parser mongoose dotenv ngrokPackage overview:
Where to Obtain MessageBird API Keys
To get your MessageBird Access Key:
live_for production ortest_for testing)To get your MessageBird Signing Key:
sk_)Official MessageBird API documentation provides detailed information on API key management and security best practices.
Create your
.envfile with MessageBird credentials:Security note: Never commit your
.envfile to version control. Add it to.gitignoreimmediately.How to Send SMS Messages with MessageBird
Create a function to send SMS using the MessageBird SDK. Before receiving webhook delivery updates, send SMS messages:
// sendSMS.js require('dotenv').config(); const messagebird = require('messagebird').initClient(process.env.MESSAGEBIRD_ACCESS_KEY); async function sendSMS(recipient, message, originator = 'YourBrand') { return new Promise((resolve, reject) => { messagebird.messages.create({ originator: originator, // Sender ID (max 11 alphanumeric chars) recipients: [recipient], // Array of E.164 formatted numbers body: message // Message content (160 chars per SMS part) }, (err, response) => { if (err) { console.error('MessageBird API Error:', err); return reject(err); } console.log('✓ Message sent successfully'); console.log('Message ID:', response.id); console.log('Recipients:', response.recipients); resolve(response); }); }); } // Example usage sendSMS('+1234567890', 'Hello from MessageBird!') .then(response => { console.log('Message ID for tracking:', response.id); }) .catch(error => { console.error('Failed to send SMS:', error); }); module.exports = sendSMS;Key parameters explained:
+14155552671)Test the SMS function:
When the message sends successfully, MessageBird returns a message ID. This ID appears in webhook payloads for delivery status tracking. According to MessageBird's SMS tutorial, you can track message delivery through webhooks or by polling the Messages API. For more information on phone number formatting, see the E.164 phone number format guide.
How to Verify Webhook Signatures with HMAC-SHA256
MessageBird signs all webhook requests with HMAC-SHA256 cryptographic signatures to prevent unauthorized access and replay attacks. Always verify webhook signatures before processing the payload to ensure security.
Understanding Webhook Signature Verification
Webhook signature verification ensures incoming requests originate from MessageBird and haven't been tampered with. MessageBird generates a cryptographic signature using your signing key and includes it in the
MessageBird-Signature-JWTheader.Why signature verification matters:
How to Implement Signature Verification
Create a middleware file to verify all incoming webhooks:
// middleware/webhookVerify.js const mbWebhookSignatureJwt = require('messagebird/lib/webhook-signature-jwt'); function createWebhookVerifier() { const signingKey = process.env.MESSAGEBIRD_SIGNING_KEY; if (!signingKey) { throw new Error('MESSAGEBIRD_SIGNING_KEY environment variable is required'); } const verifyOptions = new mbWebhookSignatureJwt.VerifyOptions(); // Add jti (JWT ID) verification to prevent replay attacks // Note: In-memory Set works for single-server deployments // For production clusters, use Redis or similar distributed cache const seenJtis = new Set(); verifyOptions.jwtVerifyJti = (jti) => { // Check if this JWT ID has been processed before if (seenJtis.has(jti)) { console.warn(`Duplicate webhook detected with JTI: ${jti}`); return false; // Reject duplicate } // Mark this JWT ID as processed seenJtis.add(jti); // In production: implement TTL cleanup to prevent memory growth // Webhooks expire after ~5 minutes, so JTIs older than 1 hour can be purged return true; }; // Return Express middleware that validates signature automatically return new mbWebhookSignatureJwt.ExpressMiddlewareVerify(signingKey, verifyOptions); } module.exports = createWebhookVerifier;Important security considerations:
url_hash,payload_hash,jti,nbf,expexp(expiration) claim to prevent processing stale requestsSetworks for single-server deployments but doesn't persist across restarts. For production clusters with multiple servers, store JTIs in Redis with 1-hour TTL. Memory usage: ~50 bytes per JTI; 1 million webhooks ≈ 50 MB memory.Where to Find Your MessageBird Signing Key
sk_).envfile asMESSAGEBIRD_SIGNING_KEYSecurity warning: Never expose your signing key in client-side code or public repositories.
Understanding SMS Delivery Status Codes
MessageBird provides three levels of status information to track every stage of SMS delivery:
Primary Status Codes
sentbuffereddelivereddelivery_failedstatusReasonandstatusErrorCodefor root causeexpiredStatus transition timing expectations:
deliveredstatus within 2–10 secondsbufferedstatus)expiredstatusErrorCode: 6) may resolve within minutes to hoursSecondary Status Reasons
The
statusReasonfield provides additional context about delivery failures:Tertiary Error Codes
The
statusErrorCodefield provides carrier-specific error details:Example webhook payload with all status levels:
{ "message": { "id": "e8077d803532c0b5937c639b60216938", "status": "delivery_failed", "statusReason": "absent_subscriber", "statusErrorCode": 6 } }How to Handle SMS Delivery Status Codes
Implement appropriate actions based on status with exponential backoff retry logic:
// statusHandler.js function handleDeliveryStatus(status, statusReason, statusErrorCode, messageId) { switch(status) { case 'delivered': // Success – update database, notify user console.log('✓ Message delivered successfully'); // Mark message as delivered in database // Trigger success webhook to your application break; case 'delivery_failed': if (statusErrorCode === 6) { // Temporary failure – schedule retry with exponential backoff console.log('⚠ Temporary failure, scheduling retry'); scheduleRetry(messageId, statusReason); } else if (statusErrorCode === 5) { // Permanent failure – mark invalid, don't retry console.log('✗ Permanent failure, number invalid'); markNumberInvalid(messageId); // Remove from future send lists } else { // Unknown error (code 9) – log for investigation console.log(`⚠ Unknown error: ${statusReason}`); // Alert monitoring system } break; case 'expired': // TTL exceeded – log for investigation console.log('⏱ Message expired before delivery'); // Check carrier connectivity issues // Review message validity period settings break; case 'buffered': // Message queued – normal operation console.log('📦 Message buffered by carrier'); // No action needed, wait for delivered status break; default: console.log(`Status: ${status}`); } } // Exponential backoff retry scheduler for temporary failures function scheduleRetry(messageId, reason) { const retryAttempts = getRetryCount(messageId); // Get from database if (retryAttempts >= 5) { console.log(`Max retries exceeded for message ${messageId}`); return; } // Exponential backoff: 1min, 5min, 15min, 1hr, 3hr const delays = [60, 300, 900, 3600, 10800]; const delaySeconds = delays[retryAttempts]; console.log(`Scheduling retry #${retryAttempts + 1} in ${delaySeconds}s`); // Use job queue (Bull/BullMQ) for production setTimeout(() => { retryMessage(messageId); }, delaySeconds * 1000); } function markNumberInvalid(messageId) { // Add to blacklist or invalid numbers table console.log(`Marking message ${messageId} recipient as invalid`); } function getRetryCount(messageId) { // Retrieve from database return 0; // Placeholder } function retryMessage(messageId) { // Resend message logic console.log(`Retrying message ${messageId}`); } module.exports = handleDeliveryStatus;How to Build a Webhook Endpoint in Express
Create your Express server with webhook handling and SMS sending integration:
// server.js require('dotenv').config(); const express = require('express'); const bodyParser = require('body-parser'); const mongoose = require('mongoose'); const createWebhookVerifier = require('./middleware/webhookVerify'); const sendSMS = require('./sendSMS'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // Database connection mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }).then(() => { console.log('✓ MongoDB connected'); }).catch(err => { console.error('MongoDB connection error:', err); }); // Message schema const messageSchema = new mongoose.Schema({ messageId: { type: String, required: true, unique: true }, recipient: String, originator: String, body: String, status: String, statusReason: String, statusErrorCode: Number, statusDatetime: Date, retryCount: { type: Number, default: 0 }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); const Message = mongoose.model('Message', messageSchema); // API endpoint to send SMS app.post('/api/send-sms', async (req, res) => { try { const { recipient, message, originator } = req.body; if (!recipient || !message) { return res.status(400).json({ error: 'recipient and message are required' }); } // Send SMS via MessageBird const response = await sendSMS(recipient, message, originator); // Store message in database for tracking await Message.create({ messageId: response.id, recipient: recipient, originator: response.originator, body: message, status: 'sent', createdAt: new Date() }); res.status(200).json({ success: true, messageId: response.id, status: 'sent' }); } catch (error) { console.error('SMS sending error:', error); res.status(500).json({ error: 'Failed to send SMS' }); } }); // Webhook endpoint with signature verification const webhookVerifier = createWebhookVerifier(); app.post('/webhooks/delivery-status', webhookVerifier, async (req, res) => { try { const payload = req.body; console.log('Received webhook:', JSON.stringify(payload, null, 2)); const { message } = payload; if (!message || !message.id) { return res.status(400).json({ error: 'Invalid payload' }); } // Update or create message record await Message.findOneAndUpdate( { messageId: message.id }, { messageId: message.id, recipient: message.recipients?.[0]?.recipient, status: message.status, statusReason: message.statusReason, statusErrorCode: message.statusErrorCode, statusDatetime: message.statusDatetime, updatedAt: new Date() }, { upsert: true, new: true } ); console.log(`✓ Updated message ${message.id} with status: ${message.status}`); // Return 200 OK immediately to acknowledge receipt // Process heavy operations asynchronously res.status(200).json({ received: true }); // Handle status asynchronously after responding handleDeliveryStatus( message.status, message.statusReason, message.statusErrorCode, message.id ); } catch (error) { console.error('Webhook processing error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy' }); }); // Query message status app.get('/api/message/:messageId', async (req, res) => { try { const message = await Message.findOne({ messageId: req.params.messageId }); if (!message) { return res.status(404).json({ error: 'Message not found' }); } res.status(200).json(message); } catch (error) { res.status(500).json({ error: 'Database error' }); } }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`Webhook endpoint: http://localhost:${PORT}/webhooks/delivery-status`); });Key implementation details:
findOneAndUpdatewithupsertprevents duplicate processingmessageIdfield is unique and indexed for fast lookups/api/send-smsendpoint sends messages and stores initial records; webhook updates delivery statusHow to Test Webhooks Locally with Ngrok
Ngrok creates a secure HTTPS tunnel to your local development server, enabling MessageBird to deliver webhook callbacks during testing. Learn more about webhook testing best practices in MessageBird's developer documentation.
Understanding Ngrok for Local Webhook Testing
Ngrok exposes your localhost server to the internet with a public URL. MessageBird requires HTTPS webhooks, which ngrok provides automatically with valid SSL certificates.
Install and start ngrok:
# Install ngrok globally npm install -g ngrok # Start ngrok tunnel on port 3000 ngrok http 3000Ngrok output:
Copy the HTTPS forwarding URL (e.g.,
https://abc123.ngrok.io).Configuring Webhooks in MessageBird Dashboard
https://abc123.ngrok.io/webhooks/delivery-statusTesting tips:
http://localhost:4040to debug webhook payloadsCommon ngrok troubleshooting:
npm update -g ngrokProduction Security Best Practices for Webhooks
Implement these security measures before deploying to production:
1. HTTPS Enforcement
Force HTTPS for all webhook endpoints:
app.use((req, res, next) => { if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') { return res.status(403).json({ error: 'HTTPS required' }); } next(); });2. Rate Limiting
Install the rate limiting package:
Prevent abuse with rate limiting:
const rateLimit = require('express-rate-limit'); const webhookLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 1000, // Limit each IP to 1,000 requests per window message: 'Too many webhook requests', standardHeaders: true, legacyHeaders: false, }); app.post('/webhooks/delivery-status', webhookLimiter, webhookVerifier, async (req, res) => { // Webhook handler code });3. Timestamp Validation
Integrate timestamp validation into webhook handler to reject stale requests:
function validateTimestamp(timestamp, maxAgeSeconds = 300) { const now = Math.floor(Date.now() / 1000); const age = now - timestamp; return age >= 0 && age <= maxAgeSeconds; } // Use in webhook handler app.post('/webhooks/delivery-status', webhookVerifier, async (req, res) => { try { const payload = req.body; // Extract timestamp from JWT claims (nbf or iat) // MessageBird SDK validates expiration automatically // Additional check for message freshness if (payload.message?.statusDatetime) { const statusTimestamp = new Date(payload.message.statusDatetime).getTime() / 1000; if (!validateTimestamp(statusTimestamp, 3600)) { // 1 hour max age console.warn('Webhook payload too old, rejecting'); return res.status(400).json({ error: 'Stale webhook' }); } } // Continue processing... } catch (error) { console.error('Webhook processing error:', error); res.status(500).json({ error: 'Internal server error' }); } });4. IP Allowlisting
Important note: According to MessageBird's official documentation, MessageBird's REST API uses dynamic IP addresses from globally distributed infrastructure, making IP allowlisting impractical. MessageBird recommends using signature verification (covered above) instead of IP filtering.
Alternative: Signature verification is mandatory
MessageBird explicitly states: "It will not be possible to whitelist the IP range for our REST API since our IP addresses are dynamic." Instead, always implement webhook signature verification using the signing key method described in this guide. This provides stronger security than IP allowlisting while accommodating MessageBird's distributed architecture.
If your organization requires network-level security, implement these alternatives:
5. JTI Persistence with Redis
For production deployments with multiple servers, store JWT IDs in Redis:
Implement distributed JTI tracking:
const redis = require('redis'); // Create Redis client with connection handling const client = redis.createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379', socket: { reconnectStrategy: (retries) => { if (retries > 10) { return new Error('Redis connection failed'); } return retries * 100; // Exponential backoff } } }); client.on('error', (err) => { console.error('Redis client error:', err); }); client.on('connect', () => { console.log('✓ Redis connected'); }); // Connect to Redis client.connect().catch(console.error); // Check and store JTI with automatic expiration async function checkJti(jti) { try { const key = `jti:${jti}`; const exists = await client.get(key); if (exists) { console.warn(`Duplicate JTI detected: ${jti}`); return false; // Reject duplicate } // Store JTI with 1-hour TTL (3,600 seconds) // Webhooks expire after ~5 minutes, 1 hour provides safety margin await client.setEx(key, 3600, '1'); return true; // Accept new JTI } catch (error) { console.error('Redis JTI check error:', error); // Fail open: allow webhook if Redis is down // Alternative: fail closed by returning false return true; } } // Update webhook verifier to use Redis function createWebhookVerifier() { const signingKey = process.env.MESSAGEBIRD_SIGNING_KEY; if (!signingKey) { throw new Error('MESSAGEBIRD_SIGNING_KEY environment variable is required'); } const verifyOptions = new mbWebhookSignatureJwt.VerifyOptions(); // Use Redis for distributed JTI verification verifyOptions.jwtVerifyJti = async (jti) => { return await checkJti(jti); }; return new mbWebhookSignatureJwt.ExpressMiddlewareVerify(signingKey, verifyOptions); } module.exports = { createWebhookVerifier, redisClient: client };Monitoring and Debugging Webhook Deliveries
Viewing Webhook Delivery Logs in Dashboard
MessageBird Dashboard provides comprehensive webhook activity logs:
Common debugging scenarios:
Testing Webhook Endpoints Locally
Important note: Testing webhooks with curl requires generating valid JWT signatures. The MessageBird SDK handles signature generation automatically for real webhooks. For local testing, use these approaches:
Option 1: Bypass signature verification for testing (NOT for production):
// test-webhook.js const express = require('express'); const app = express(); app.use(express.json()); // Test endpoint WITHOUT signature verification app.post('/webhooks/delivery-status-test', async (req, res) => { console.log('Test webhook received:', JSON.stringify(req.body, null, 2)); res.status(200).json({ received: true }); }); app.listen(3001, () => { console.log('Test server running on port 3001'); });Test with curl:
curl -X POST http://localhost:3001/webhooks/delivery-status-test \ -H "Content-Type: application/json" \ -d '{ "message": { "id": "test123", "status": "delivered", "recipients": [{"recipient": "31612345678"}] } }'Option 2: Send real SMS and monitor webhooks (recommended):
/api/send-smsendpoint# Terminal 1: Start server node server.js # Terminal 2: Start ngrok ngrok http 3000 # Terminal 3: Send test SMS curl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{ "recipient": "+1234567890", "message": "Test message", "originator": "TestApp" }' # Monitor webhooks in Terminal 1 server logsTesting checklist:
sent,delivered,delivery_failed,expiredFrequently Asked Questions
How do I verify webhook signatures?
MessageBird signs webhooks with HMAC-SHA256 and sends the signature in the
MessageBird-Signature-JWTheader. Use the official MessageBird SDK'swebhook-signature-jwtmodule to verify signatures automatically. The SDK validates the JWT structure, expiration, and payload hash. Implement JTI (JWT ID) verification to prevent replay attacks by tracking processed JWT IDs in memory or Redis.What are MessageBird's webhook retry policies?
MessageBird retries failed webhook deliveries using exponential backoff. If your endpoint returns a non-2xx status code or times out, MessageBird retries up to 10 times over 24 hours. The retry schedule: immediate, 1 minute, 5 minutes, 30 minutes, 1 hour, 3 hours, 6 hours, 12 hours, and 24 hours. Return HTTP 200 OK for successful processing, even if business logic fails internally.
What SMS delivery status codes does MessageBird provide?
MessageBird provides three status levels:
status(primary),statusReason(secondary), andstatusErrorCode(tertiary). Primary statuses:sent,buffered,delivered,delivery_failed, andexpired. Status reasons:absent_subscriber,invalid_destination,unknown_subscriber,rejected_by_carrier, andblacklisted. Error codes: 0 (no error), 5 (permanent failure), 6 (temporary failure), 9 (unknown error).How do I handle duplicate webhooks?
Implement idempotent webhook processing using the message ID as a unique identifier. Use database operations like
findOneAndUpdatewithupsertto handle duplicates safely. Track JWT IDs (jti claim) in memory or Redis to detect and reject duplicate deliveries. MessageBird may send the same webhook multiple times during network issues or retries.Why is my webhook endpoint timing out?
MessageBird requires webhook endpoints to respond within 5 seconds. Return HTTP 200 OK immediately after receiving the webhook, then process heavy operations asynchronously. Use job queues like Bull or BullMQ for background processing:
// Install Bull: npm install bull const Queue = require('bull'); const webhookQueue = new Queue('webhooks', process.env.REDIS_URL); // Webhook handler - respond immediately app.post('/webhooks/delivery-status', webhookVerifier, async (req, res) => { // Queue webhook for async processing await webhookQueue.add(req.body); // Respond immediately (< 1 second) res.status(200).json({ received: true }); }); // Process webhooks in background webhookQueue.process(async (job) => { const payload = job.data; // Heavy processing here: database updates, external APIs, etc. await processWebhook(payload); });Optimize database queries with proper indexing. Avoid synchronous external API calls in webhook handlers. Monitor endpoint response times with APM tools.
What security measures should I implement for webhooks?
Implement these security measures: (1) Always verify webhook signatures using the signing key, (2) Enforce HTTPS in production, (3) Apply rate limiting to prevent abuse, (4) Validate timestamp freshness (reject requests older than 5 minutes), (5) Track JWT IDs to prevent replay attacks, (6) Note that IP allowlisting is not recommended by MessageBird due to dynamic IPs, (7) Implement request logging for audit trails.
How do I test webhooks locally?
Use ngrok to create a secure HTTPS tunnel to your local server:
ngrok http 3000. Copy the ngrok HTTPS URL and configure it in MessageBird Dashboard under Developers → Webhooks. Start your Express server and send test SMS messages. Monitor webhook deliveries in the ngrok inspector (http://localhost:4040) and your server logs. Update the webhook URL in MessageBird Dashboard whenever ngrok restarts (free tier generates new URLs).What should I do when webhook delivery fails?
Check the webhook activity logs in MessageBird Dashboard under Developers → Webhooks → Activity. Common failures: (1) 401 Unauthorized – verify signing key matches dashboard, (2) Timeout – optimize endpoint to respond within 5 seconds, (3) Connection refused – verify endpoint URL and server status, (4) 500 errors – check application logs for exceptions. MessageBird automatically retries failed deliveries up to 10 times.
Can I use webhooks without signature verification?
No. Always implement signature verification to prevent unauthorized access and spoofed webhook requests. MessageBird provides the signing key and official SDK for verification. Skipping signature verification exposes your system to malicious actors who could send fake delivery status updates, corrupt your database, or trigger unauthorized actions. Signature verification is a security requirement, not optional.
How do I migrate from API polling to webhooks?
Replace polling logic with webhook handlers: (1) Create webhook endpoint in Express, (2) Implement signature verification middleware, (3) Configure webhook URL in MessageBird Dashboard, (4) Remove polling code that calls
messages.read()repeatedly, (5) Update database schema to track webhook deliveries, (6) Test with ngrok for local development, (7) Deploy with HTTPS enabled. Webhooks eliminate API rate limit concerns and provide real-time updates.Summary: Building Production-Ready SMS Webhooks
You've built a complete MessageBird webhook system with Node.js and Express that tracks SMS delivery status in real time. The system includes cryptographic signature verification using HMAC-SHA256, comprehensive status code handling across three levels (status, statusReason, statusErrorCode), MongoDB persistence for audit trails, and production security with rate limiting and replay attack prevention.
Key implementation requirements:
MessageBird-Signature-JWTheaderProduction deployment checklist:
Next steps to implement this system:
npm install messagebird express body-parser mongoose dotenv ngrok redis.envfile with MessageBird API keys from dashboardnode server.jsngrok http 3000Performance expectations:
Next steps for enhancement:
The webhook system provides real-time visibility into SMS delivery, eliminates polling overhead, and scales efficiently for high-volume messaging. Monitor delivery rates, optimize for carrier-specific issues, and continuously review security practices to maintain reliable SMS communications.