code examples
code examples
MessageBird SMS Delivery Status Tracking with Node.js, Vite, React & Vue: Complete Webhook Tutorial
Build a production-ready SMS delivery status tracking system with MessageBird webhooks, Node.js backend, and Vite frontend (React/Vue). Step-by-step guide covering delivery callbacks, webhook security, and real-time status updates.
Build a Vonage SMS Sender with Node.js and Express
Tracking SMS delivery status is critical for production applications that rely on SMS for authentication, notifications, or transactional messages. This comprehensive tutorial shows you how to build a production-ready MessageBird SMS delivery tracking system with a Node.js backend and Vite-powered frontend (React or Vue), implementing secure webhook handling for real-time delivery status updates.
This guide provides a step-by-step walkthrough for building a complete SMS delivery tracking system with MessageBird's webhook callbacks. You'll learn how to send SMS messages via MessageBird's REST API, configure and secure delivery status webhooks, and display real-time delivery updates in a modern Vite + React or Vue frontend application.
By the end of this guide, you will have:
- A Node.js Express backend that sends SMS via MessageBird and receives delivery status webhooks
- Secure webhook signature verification to ensure callbacks are authentic
- A database schema to track message delivery states (sent, delivered, failed)
- A Vite-powered React or Vue frontend displaying real-time delivery status
- Production-ready error handling and deployment strategies
Key Technologies:
- Node.js: JavaScript runtime for building the backend API and webhook server
- Express: Lightweight Node.js web framework for REST endpoints and webhook handlers
- MessageBird REST API: Cloud communications platform for sending SMS and receiving delivery status callbacks via webhooks
- Vite: Next-generation frontend build tool offering fast development and optimized production builds
- React or Vue: Modern JavaScript frameworks for building the delivery status dashboard UI
- dotenv: Environment variable management for secure credential storage
- crypto: Node.js native module for webhook signature verification
System Architecture:
The system consists of three main components: a frontend dashboard (Vite + React/Vue), a Node.js backend API, and MessageBird's webhook service. When you send an SMS, MessageBird assigns it a unique message ID and begins delivery. As the delivery progresses through states (sent → buffered → delivered or failed), MessageBird sends HTTP POST webhooks to your server with status updates. Your webhook handler verifies the signature, updates the database, and the frontend polls or uses WebSockets to display real-time status.
[Vite Frontend] <--(REST API)--> [Node.js Backend] <--(MessageBird SDK)--> [MessageBird API]
↓ ↑
[Display Status] [Webhook Endpoint] <--(Delivery Callbacks)-- [MessageBird Webhooks]
↓
[Database: Message Status]Prerequisites:
- Node.js and npm: Version 18.x or later. Download Node.js
- MessageBird API Account: Sign up at MessageBird. You'll receive test credits for development
- MessageBird Access Key: Obtain from your MessageBird Dashboard (Developers → API access)
- MessageBird Signing Key: Required for webhook signature verification (Developers → API access → Signing key)
- MessageBird Virtual Number: Purchase or configure a number capable of sending SMS
- Public Webhook URL: For local development, use ngrok to expose your localhost. For production, use your deployed application URL
- (Optional) Database: PostgreSQL, MySQL, or MongoDB for persisting message delivery status
Understanding MessageBird Delivery Status Webhooks
Before diving into implementation, it's essential to understand how MessageBird delivery status webhooks work and why they're critical for production SMS applications.
What Are SMS Delivery Status Webhooks?
When you send an SMS via MessageBird, the API returns a 201 Created response with a message ID almost instantly. However, this doesn't mean the SMS has been delivered to the recipient's phone – it only confirms MessageBird accepted your request. The actual delivery process involves:
- Queued/Buffered: MessageBird queues your message for delivery
- Sent: The message is handed off to the mobile carrier network
- Delivered: The carrier confirms the recipient's device received the message
- Failed: Delivery failed due to invalid number, network issues, or carrier rejection
Delivery status webhooks are HTTP POST callbacks that MessageBird sends to your server URL whenever a message's status changes. This enables you to:
- Track delivery success rates and identify problematic numbers
- Retry failed messages or trigger fallback communication channels
- Update users in real-time about message delivery status
- Monitor SMS campaign performance and ROI
MessageBird Webhook Payload Structure
MessageBird sends webhooks with a JSON payload containing delivery information. Here's a typical webhook payload for a delivered message:
{
"id": "b65a6c749d7e4758a811ea7c8c7e0000",
"href": "https://rest.messagebird.com/messages/b65a6c749d7e4758a811ea7c8c7e0000",
"direction": "mt",
"type": "sms",
"originator": "YourBrand",
"body": "Your verification code is 123456",
"reference": "customer-order-5678",
"validity": null,
"gateway": 10,
"typeDetails": {},
"datacoding": "plain",
"mclass": 1,
"scheduledDatetime": null,
"createdDatetime": "2025-10-10T14:30:00+00:00",
"recipients": {
"totalCount": 1,
"totalSentCount": 1,
"totalDeliveredCount": 1,
"totalDeliveryFailedCount": 0,
"items": [
{
"recipient": 14155552671,
"status": "delivered",
"statusDatetime": "2025-10-10T14:30:15+00:00",
"messagePartCount": 1
}
]
}
}Key Fields for Delivery Tracking:
id: MessageBird's unique message identifier (use this to correlate with your database)recipients.items[].status: The delivery status (sent,buffered,delivered,delivery_failed,expired)recipients.items[].statusDatetime: Timestamp when the status was updatedrecipients.items[].recipient: The phone number that received (or failed to receive) the messagereference: Your custom reference ID (optional but recommended for linking to orders, users, etc.)
Webhook Security: Signature Verification
Critical: Always verify webhook signatures to prevent malicious actors from sending fake delivery status updates to your system. MessageBird signs each webhook request using HMAC-SHA256 with your signing key.
The signature is sent in the MessageBird-Signature HTTP header. To verify:
- Extract the
MessageBird-Signatureheader value - Retrieve the raw request body as a string (before parsing JSON)
- Compute HMAC-SHA256 hash of the body using your signing key
- Compare your computed signature with the header value
- Reject requests with invalid or missing signatures (return 401 Unauthorized)
Example verification code (we'll implement this in detail later):
const crypto = require('crypto');
function verifyWebhookSignature(signature, body, signingKey) {
const computedSignature = crypto
.createHmac('sha256', signingKey)
.update(body, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computedSignature)
);
}Setting Up Your Development Environment
We'll start by setting up the Node.js backend server that handles SMS sending and webhook callbacks.
1. Initialize Node.js Project
Create a new project directory and initialize npm:
mkdir messagebird-delivery-tracker
cd messagebird-delivery-tracker
npm init -y2. Install Backend Dependencies
Install the required packages for the Node.js backend:
npm install express messagebird dotenv body-parserPackage overview:
express: Web framework for REST API and webhook endpointsmessagebird: Official MessageBird Node.js SDKdotenv: Environment variable managementbody-parser: Middleware to parse webhook JSON payloads (also need raw body for signature verification)
For database integration (optional but recommended), install:
# PostgreSQL
npm install pg
# Or MongoDB
npm install mongodb
# Or Prisma (works with PostgreSQL, MySQL, SQLite)
npm install prisma @prisma/client
npx prisma init3. Configure Environment Variables
Create a .env file in your project root with your MessageBird credentials:
# Server Configuration
PORT=3000
NODE_ENV=development
# MessageBird API Credentials
MESSAGEBIRD_ACCESS_KEY=your_live_access_key_here
MESSAGEBIRD_SIGNING_KEY=your_webhook_signing_key_here
MESSAGEBIRD_ORIGINATOR=YourBrand
# Webhook Configuration
WEBHOOK_URL=https://your-ngrok-url.ngrok.io/webhooks/messagebird/delivery
# Database (example for PostgreSQL)
DATABASE_URL=postgresql://user:password@localhost:5432/messagebird_trackerImportant: Never commit the .env file to version control. Add it to .gitignore:
node_modules/
.env
*.log
.DS_Store4. Obtain MessageBird Credentials
- Access Key: Log into MessageBird Dashboard → Developers → API access → Show Live API Keys
- Signing Key: Same location, scroll to "Signing key" section
- Originator: Your purchased phone number or approved alphanumeric sender ID (max 11 characters)
Implementing the Backend API
Create server.js as your main backend entry point:
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const messagebird = require('messagebird')(process.env.MESSAGEBIRD_ACCESS_KEY);
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// In-memory storage (replace with database in production)
const messages = new Map();
// Helper: Verify MessageBird webhook signature
function verifyWebhookSignature(req, res, next) {
const signature = req.headers['messagebird-signature'];
const signingKey = process.env.MESSAGEBIRD_SIGNING_KEY;
if (!signature) {
console.error('Missing MessageBird-Signature header');
return res.status(401).json({ error: 'Unauthorized: Missing signature' });
}
// Get raw body (important: must be the exact bytes MessageBird signed)
const rawBody = JSON.stringify(req.body);
const computedSignature = crypto
.createHmac('sha256', signingKey)
.update(rawBody, 'utf8')
.digest('base64');
try {
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computedSignature)
);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Unauthorized: Invalid signature' });
}
next();
} catch (error) {
console.error('Signature verification error:', error);
return res.status(401).json({ error: 'Unauthorized: Signature verification failed' });
}
}
// API Endpoint: Send SMS
app.post('/api/sms/send', async (req, res) => {
const { recipient, message, reference } = req.body;
if (!recipient || !message) {
return res.status(400).json({ error: 'Missing required fields: recipient, message' });
}
const params = {
originator: process.env.MESSAGEBIRD_ORIGINATOR,
recipients: [recipient],
body: message,
reference: reference || `msg_${Date.now()}`,
reportUrl: process.env.WEBHOOK_URL
};
messagebird.messages.create(params, (err, response) => {
if (err) {
console.error('MessageBird API Error:', err);
return res.status(500).json({ error: 'Failed to send SMS', details: err });
}
// Store message in database/memory
messages.set(response.id, {
id: response.id,
recipient: response.recipients.items[0].recipient,
body: response.body,
status: response.recipients.items[0].status,
createdAt: response.createdDatetime,
reference: response.reference
});
res.status(201).json({
success: true,
messageId: response.id,
status: response.recipients.items[0].status,
reference: response.reference
});
});
});
// Webhook Endpoint: Receive delivery status updates
app.post('/webhooks/messagebird/delivery', verifyWebhookSignature, (req, res) => {
const webhookData = req.body;
console.log('Received webhook:', JSON.stringify(webhookData, null, 2));
const messageId = webhookData.id;
const recipient = webhookData.recipients.items[0];
// Update message status in database/memory
if (messages.has(messageId)) {
const message = messages.get(messageId);
message.status = recipient.status;
message.statusDatetime = recipient.statusDatetime;
messages.set(messageId, message);
console.log(`Updated message ${messageId} status to: ${recipient.status}`);
} else {
console.warn(`Received webhook for unknown message ID: ${messageId}`);
}
// Always return 200 OK to acknowledge receipt
res.status(200).json({ received: true });
});
// API Endpoint: Get message status
app.get('/api/sms/:messageId', (req, res) => {
const messageId = req.params.messageId;
if (!messages.has(messageId)) {
return res.status(404).json({ error: 'Message not found' });
}
res.json(messages.get(messageId));
});
// API Endpoint: List all messages
app.get('/api/sms', (req, res) => {
res.json(Array.from(messages.values()));
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Webhook endpoint: http://localhost:${PORT}/webhooks/messagebird/delivery`);
});Key Implementation Details:
- Signature Verification Middleware: The
verifyWebhookSignaturefunction runs before the webhook handler, rejecting invalid requests - reportUrl Parameter: When sending SMS via
messagebird.messages.create(), we includereportUrlpointing to our webhook endpoint - In-Memory Storage: For simplicity, this example uses a
Map. In production, replace with PostgreSQL, MongoDB, or Redis - Status Update Logic: When webhooks arrive, we update the stored message status
- 200 OK Response: Always return 200 to MessageBird to acknowledge successful webhook receipt (prevents retries)
Database Schema for Message Tracking
For production applications, implement a proper database schema to persist message delivery status. Here's an example using PostgreSQL with Prisma:
Prisma Schema (schema.prisma)
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Message {
id String @id @default(uuid())
messageBirdId String @unique
recipient String
body String
originator String
status String @default("pending")
statusDatetime DateTime?
reference String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([recipient])
@@index([reference])
}Status Values: pending, sent, buffered, delivered, delivery_failed, expired
Database Integration Code
Replace the in-memory Map with Prisma:
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// In send endpoint:
const dbMessage = await prisma.message.create({
data: {
messageBirdId: response.id,
recipient: response.recipients.items[0].recipient,
body: response.body,
originator: response.originator,
status: response.recipients.items[0].status,
reference: response.reference
}
});
// In webhook handler:
await prisma.message.update({
where: { messageBirdId: messageId },
data: {
status: recipient.status,
statusDatetime: new Date(recipient.statusDatetime)
}
});Building the Vite Frontend
Now let's create the frontend dashboard to display real-time delivery status using Vite with React or Vue.
Initialize Vite Project
# For React
npm create vite@latest frontend -- --template react
# Or for Vue
npm create vite@latest frontend -- --template vue
cd frontend
npm install
npm install axiosReact Component: Message Status Dashboard
Create src/components/MessageDashboard.jsx:
import { useState, useEffect } from 'react';
import axios from 'axios';
const API_BASE_URL = 'http://localhost:3000';
function MessageDashboard() {
const [messages, setMessages] = useState([]);
const [recipient, setRecipient] = useState('');
const [messageText, setMessageText] = useState('');
const [loading, setLoading] = useState(false);
// Fetch messages periodically
useEffect(() => {
fetchMessages();
const interval = setInterval(fetchMessages, 5000);
return () => clearInterval(interval);
}, []);
const fetchMessages = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/api/sms`);
setMessages(response.data);
} catch (error) {
console.error('Failed to fetch messages:', error);
}
};
const sendSMS = async (e) => {
e.preventDefault();
setLoading(true);
try {
await axios.post(`${API_BASE_URL}/api/sms/send`, {
recipient,
message: messageText,
reference: `web_${Date.now()}`
});
setRecipient('');
setMessageText('');
fetchMessages();
} catch (error) {
console.error('Failed to send SMS:', error);
alert('Failed to send SMS: ' + error.message);
} finally {
setLoading(false);
}
};
const getStatusColor = (status) => {
const colors = {
pending: 'gray',
sent: 'blue',
buffered: 'yellow',
delivered: 'green',
delivery_failed: 'red',
expired: 'orange'
};
return colors[status] || 'gray';
};
return (
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
<h1>MessageBird SMS Delivery Tracker</h1>
{/* Send SMS Form */}
<div style={{ marginBottom: '30px', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
<h2>Send New SMS</h2>
<form onSubmit={sendSMS}>
<div style={{ marginBottom: '10px' }}>
<label>
Recipient (E.164 format):
<input
type="tel"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="+14155552671"
required
style={{ marginLeft: '10px', padding: '5px', width: '200px' }}
/>
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
Message:
<textarea
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
placeholder="Your message here..."
required
rows={3}
style={{ marginLeft: '10px', padding: '5px', width: '300px' }}
/>
</label>
</div>
<button type="submit" disabled={loading} style={{ padding: '10px 20px' }}>
{loading ? 'Sending...' : 'Send SMS'}
</button>
</form>
</div>
{/* Messages Table */}
<div>
<h2>Message Delivery Status ({messages.length} messages)</h2>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#f0f0f0' }}>
<th style={{ padding: '10px', border: '1px solid #ddd' }}>Recipient</th>
<th style={{ padding: '10px', border: '1px solid #ddd' }}>Message</th>
<th style={{ padding: '10px', border: '1px solid #ddd' }}>Status</th>
<th style={{ padding: '10px', border: '1px solid #ddd' }}>Created</th>
<th style={{ padding: '10px', border: '1px solid #ddd' }}>Reference</th>
</tr>
</thead>
<tbody>
{messages.map((msg) => (
<tr key={msg.id}>
<td style={{ padding: '10px', border: '1px solid #ddd' }}>{msg.recipient}</td>
<td style={{ padding: '10px', border: '1px solid #ddd' }}>{msg.body.substring(0, 50)}...</td>
<td style={{ padding: '10px', border: '1px solid #ddd' }}>
<span style={{
padding: '5px 10px',
borderRadius: '4px',
backgroundColor: getStatusColor(msg.status),
color: 'white',
fontWeight: 'bold'
}}>
{msg.status.toUpperCase()}
</span>
</td>
<td style={{ padding: '10px', border: '1px solid #ddd' }}>
{new Date(msg.createdAt).toLocaleString()}
</td>
<td style={{ padding: '10px', border: '1px solid #ddd' }}>{msg.reference || 'N/A'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default MessageDashboard;Vue Component Alternative
For Vue users, create src/components/MessageDashboard.vue:
<template>
<div class="dashboard">
<h1>MessageBird SMS Delivery Tracker</h1>
<!-- Send SMS Form -->
<div class="send-form">
<h2>Send New SMS</h2>
<form @submit.prevent="sendSMS">
<div>
<label>
Recipient (E.164):
<input v-model="recipient" type="tel" placeholder="+14155552671" required />
</label>
</div>
<div>
<label>
Message:
<textarea v-model="messageText" placeholder="Your message..." rows="3" required></textarea>
</label>
</div>
<button type="submit" :disabled="loading">
{{ loading ? 'Sending...' : 'Send SMS' }}
</button>
</form>
</div>
<!-- Messages Table -->
<div class="messages-table">
<h2>Message Delivery Status ({{ messages.length }} messages)</h2>
<table>
<thead>
<tr>
<th>Recipient</th>
<th>Message</th>
<th>Status</th>
<th>Created</th>
<th>Reference</th>
</tr>
</thead>
<tbody>
<tr v-for="msg in messages" :key="msg.id">
<td>{{ msg.recipient }}</td>
<td>{{ msg.body.substring(0, 50) }}...</td>
<td>
<span class="status-badge" :style="{ backgroundColor: getStatusColor(msg.status) }">
{{ msg.status.toUpperCase() }}
</span>
</td>
<td>{{ new Date(msg.createdAt).toLocaleString() }}</td>
<td>{{ msg.reference || 'N/A' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
import axios from 'axios';
export default {
name: 'MessageDashboard',
setup() {
const messages = ref([]);
const recipient = ref('');
const messageText = ref('');
const loading = ref(false);
let interval;
const API_BASE_URL = 'http://localhost:3000';
const fetchMessages = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/api/sms`);
messages.value = response.data;
} catch (error) {
console.error('Failed to fetch messages:', error);
}
};
const sendSMS = async () => {
loading.value = true;
try {
await axios.post(`${API_BASE_URL}/api/sms/send`, {
recipient: recipient.value,
message: messageText.value,
reference: `web_${Date.now()}`
});
recipient.value = '';
messageText.value = '';
await fetchMessages();
} catch (error) {
console.error('Failed to send SMS:', error);
alert('Failed to send SMS: ' + error.message);
} finally {
loading.value = false;
}
};
const getStatusColor = (status) => {
const colors = {
pending: 'gray',
sent: 'blue',
buffered: 'yellow',
delivered: 'green',
delivery_failed: 'red',
expired: 'orange'
};
return colors[status] || 'gray';
};
onMounted(() => {
fetchMessages();
interval = setInterval(fetchMessages, 5000);
});
onUnmounted(() => {
clearInterval(interval);
});
return {
messages,
recipient,
messageText,
loading,
sendSMS,
getStatusColor
};
}
};
</script>
<style scoped>
.dashboard {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.send-form {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.status-badge {
padding: 5px 10px;
border-radius: 4px;
color: white;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}
thead tr {
background-color: #f0f0f0;
}
</style>Testing Webhooks Locally with ngrok
MessageBird needs a publicly accessible HTTPS URL to send webhooks. For local development, use ngrok:
1. Install and Run ngrok
# Install ngrok (or download from https://ngrok.com)
npm install -g ngrok
# Start your backend server
node server.js
# In a new terminal, expose port 3000
ngrok http 3000ngrok will provide a public URL like https://abc123.ngrok.io.
2. Update Environment Variables
Update your .env file with the ngrok URL:
WEBHOOK_URL=https://abc123.ngrok.io/webhooks/messagebird/delivery3. Configure MessageBird Dashboard (Optional)
MessageBird also allows configuring default webhook URLs in the dashboard:
- Go to MessageBird Dashboard → Developers → API access
- Scroll to "Webhooks" section
- Add your webhook URL for "SMS Status Reports"
Note: The reportUrl parameter in the send request overrides the dashboard default.
4. Test the Complete Flow
- Start your backend:
node server.js - Start your frontend:
cd frontend && npm run dev - Open the frontend in your browser (typically
http://localhost:5173) - Send a test SMS to your phone number
- Watch the status update in real-time as MessageBird sends webhooks
Check your backend logs for webhook payloads:
Received webhook: {
"id": "b65a6c749d7e4758a811ea7c8c7e0000",
"recipients": {
"items": [
{
"recipient": 14155552671,
"status": "delivered",
"statusDatetime": "2025-10-10T14:30:15+00:00"
}
]
}
}
Updated message b65a6c749d7e4758a811ea7c8c7e0000 status to: delivered
Security Best Practices
1. Webhook Signature Verification
Always verify the MessageBird-Signature header. Never process webhooks without verification:
// Bad: No verification
app.post('/webhooks/messagebird/delivery', (req, res) => {
// ❌ Anyone can send fake webhooks
updateMessageStatus(req.body);
});
// Good: Signature verification
app.post('/webhooks/messagebird/delivery', verifyWebhookSignature, (req, res) => {
// ✅ Only authentic MessageBird webhooks processed
updateMessageStatus(req.body);
});2. Rate Limiting
Protect your endpoints from abuse:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // Limit each IP to 100 requests per minute
message: 'Too many webhook requests'
});
app.post('/webhooks/messagebird/delivery', webhookLimiter, verifyWebhookSignature, handler);3. HTTPS in Production
Always use HTTPS in production to prevent man-in-the-middle attacks:
- Use a reverse proxy (Nginx, Caddy) with SSL certificates
- Deploy to platforms with automatic HTTPS (Heroku, Vercel, Railway)
- Never expose plain HTTP webhook endpoints publicly
4. Environment Variable Security
- Use environment variable management services (AWS Secrets Manager, HashiCorp Vault)
- Never commit
.envfiles to version control - Rotate API keys periodically
- Use separate keys for development, staging, and production
Error Handling and Retry Logic
Handling Webhook Delivery Failures
MessageBird will retry failed webhook deliveries up to 10 times with exponential backoff. Ensure your webhook handler:
- Returns 200 OK quickly: Process webhooks asynchronously if database operations are slow
- Is idempotent: Handle duplicate webhooks gracefully (use message ID as unique key)
- Logs failures: Track webhook processing errors for debugging
app.post('/webhooks/messagebird/delivery', verifyWebhookSignature, async (req, res) => {
// Immediately acknowledge receipt
res.status(200).json({ received: true });
// Process asynchronously
try {
await processWebhook(req.body);
} catch (error) {
console.error('Webhook processing error:', error);
// Log to error tracking service (Sentry, Rollbar, etc.)
}
});Handling SMS Send Failures
Implement retry logic for transient failures:
async function sendSMSWithRetry(params, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await messagebird.messages.create(params);
} catch (error) {
if (attempt === maxRetries || !isRetryableError(error)) {
throw error;
}
await delay(1000 * Math.pow(2, attempt)); // Exponential backoff
}
}
}
function isRetryableError(error) {
// Retry on rate limits and temporary failures
return error.statusCode === 429 || error.statusCode >= 500;
}Deployment
Backend Deployment (Node.js)
Deploy your backend to platforms that support webhooks:
Heroku:
heroku create your-app-name
heroku config:set MESSAGEBIRD_ACCESS_KEY=your_key
heroku config:set MESSAGEBIRD_SIGNING_KEY=your_signing_key
git push heroku mainRailway:
railway login
railway init
railway add
railway upAWS Lambda + API Gateway:
Use the Serverless Framework or AWS SAM to deploy the Express app as Lambda functions.
Frontend Deployment (Vite)
Vercel:
cd frontend
vercel deploy --prodNetlify:
cd frontend
npm run build
netlify deploy --prod --dir=distUpdate your frontend API calls to use the production backend URL.
Monitoring and Analytics
Track Delivery Metrics
Monitor key SMS delivery metrics:
// Calculate delivery rates
const totalMessages = await prisma.message.count();
const deliveredMessages = await prisma.message.count({
where: { status: 'delivered' }
});
const failedMessages = await prisma.message.count({
where: { status: 'delivery_failed' }
});
const deliveryRate = (deliveredMessages / totalMessages) * 100;
const failureRate = (failedMessages / totalMessages) * 100;
console.log(`Delivery rate: ${deliveryRate.toFixed(2)}%`);
console.log(`Failure rate: ${failureRate.toFixed(2)}%`);Integration with Analytics Tools
Send delivery metrics to monitoring platforms:
// Example: Send metrics to Datadog
const StatsD = require('node-dogstatsd').StatsD;
const dogstatsd = new StatsD();
function recordDeliveryStatus(status) {
dogstatsd.increment('sms.delivery', 1, [`status:${status}`]);
}
// In webhook handler
recordDeliveryStatus(recipient.status);Troubleshooting Common Issues
Webhooks Not Being Received
Possible causes:
- Firewall blocking incoming requests: Ensure your server allows incoming HTTP/HTTPS traffic
- Wrong webhook URL: Verify the
reportUrlparameter matches your actual endpoint - ngrok tunnel expired: Free ngrok tunnels expire; restart ngrok and update the URL
- SSL certificate issues: MessageBird requires valid HTTPS certificates (not self-signed)
Solutions:
# Check if webhook endpoint is accessible
curl -X POST https://your-domain.com/webhooks/messagebird/delivery \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Test locally with ngrok
ngrok http 3000
# Update WEBHOOK_URL in .env and restart serverSignature Verification Failures
Possible causes:
- Wrong signing key: Double-check the key from MessageBird dashboard
- Request body modified: Body parsers might transform the body; use raw body
- Encoding issues: Ensure UTF-8 encoding when computing HMAC
Solution: Enable debug logging:
function verifyWebhookSignature(req, res, next) {
const signature = req.headers['messagebird-signature'];
const rawBody = JSON.stringify(req.body);
console.log('Received signature:', signature);
console.log('Raw body:', rawBody);
console.log('Signing key (first 10 chars):', process.env.MESSAGEBIRD_SIGNING_KEY.substring(0, 10));
// ... verification logic
}Messages Stuck in "Sent" Status
Possible causes:
- Invalid recipient number: Wrong format or non-existent number
- Carrier delays: Some carriers take minutes to confirm delivery
- MessageBird not sending webhooks: Check dashboard webhook configuration
Solution: Query MessageBird API directly:
messagebird.messages.read(messageId, (err, response) => {
console.log('Current status:', response.recipients.items[0].status);
});Related Resources
- MessageBird API Documentation - Official API reference
- MessageBird Node.js SDK on GitHub - SDK source code and examples
- Understanding SMS Delivery Reports - In-depth guide to delivery status tracking
- E.164 Phone Number Format - International phone number formatting standard
- ngrok Documentation - Exposing local servers for webhook testing
Frequently Asked Questions
How do I verify MessageBird webhook signatures?
Extract the MessageBird-Signature header, compute HMAC-SHA256 of the raw request body using your signing key, and compare with the header value using crypto.timingSafeEqual() to prevent timing attacks.
What MessageBird delivery statuses should I handle?
Handle these key statuses: sent (handed to carrier), buffered (queued at carrier), delivered (confirmed receipt), delivery_failed (permanent failure), and expired (validity period exceeded).
Can I use Vite with other frameworks besides React and Vue?
Yes, Vite supports Svelte, Preact, Lit, and vanilla JavaScript. Use npm create vite@latest and select your preferred framework template.
How long do MessageBird webhooks take to arrive?
Webhooks typically arrive within 1-30 seconds after status changes. Delivery confirmations depend on carrier speed (usually 5-60 seconds for most carriers).
Do I need a database for production deployments?
Yes, use PostgreSQL, MySQL, or MongoDB to persist message status across server restarts. In-memory storage loses data when the server stops.
How can I test webhooks without deploying to production?
Use ngrok to expose your local development server with a public HTTPS URL. MessageBird will send webhooks to your localhost through the ngrok tunnel.
What happens if my webhook endpoint is down?
MessageBird retries failed webhooks up to 10 times with exponential backoff (up to 24 hours). Ensure your endpoint returns 200 OK quickly to avoid retries.
Can I configure different webhook URLs for different messages?
Yes, use the reportUrl parameter when sending each message. This overrides the default webhook URL configured in your MessageBird dashboard.
How do I handle duplicate webhook deliveries?
Use the message ID as a unique key in your database. When updating status, use UPDATE WHERE messageBirdId = ? which safely handles duplicate webhooks.
What's the difference between MessageBird's signing key and access key?
The access key authenticates API requests you send to MessageBird. The signing key validates webhook requests MessageBird sends to you. Never share either key.
Frequently Asked Questions
How to handle character limits in SMS messages with Vonage?
Vonage automatically segments longer messages exceeding the standard SMS character limit (160 for GSM-7, 70 for UCS-2). Consider message length to manage costs.
How to send SMS with Node.js and Express?
Use the Vonage Messages API with the Express.js framework and the @vonage/server-sdk library. This setup allows your Node.js application to send SMS messages by making HTTP requests to the Vonage API. The article provides a step-by-step guide on setting up this project.
What is the Vonage Messages API?
The Vonage Messages API is a versatile tool that enables sending messages over various channels, including SMS. It offers a unified interface for different message types. This article focuses on its SMS capabilities using the @vonage/messages package in Node.js.
Why use dotenv in a Node.js project?
Dotenv helps manage environment variables by loading them from a .env file into process.env. This enhances security by keeping sensitive credentials like API keys out of your codebase and makes configuration more manageable.
When should I use the Vonage Messages API?
The Vonage Messages API is suitable when your Node.js applications need features like SMS notifications, two-factor authentication, or other communication services. It's a robust solution for sending SMS messages programmatically.
How to set up a Vonage application for sending SMS?
Create an application in your Vonage API Dashboard, generate a private key file, and link your Vonage virtual number to the application. Then, enable the “Messages” capability within the application settings for SMS functionality.
What is the purpose of a private key with Vonage?
The private key, along with your Application ID, authenticates your Node.js application with the Vonage API. Keep this file secure and never commit it to version control, as it grants access to your Vonage account resources.
How to handle Vonage API errors in Node.js?
Implement a try...catch block around the vonageMessages.send() function. This allows you to capture and handle potential errors during the API call. Detailed error information is available in the error.response.data property of the error object.
What is the format for the 'to' phone number parameter?
Use E.164 formatting (e.g., 14155552671 or +14155552671) for recipient phone numbers in the 'to' parameter of your API requests to Vonage. Avoid spaces, parentheses, or other formatting.
Can I send international SMS messages with Vonage?
Yes, Vonage supports international SMS. However, different pricing and regulations may apply based on the destination country. Make sure your account allows international messaging before sending SMS globally.
How to test my Vonage SMS API endpoint locally?
Use tools like curl or Postman to send POST requests to your local endpoint (e.g., http://localhost:3000/send-sms). The request body should be JSON with 'to' and 'text' fields.
What are some security best practices for a Vonage SMS application?
Store API credentials in environment variables, use HTTPS, validate and sanitize user input, implement rate limiting to prevent abuse, and never expose your private key.
What should I do if I get a 'Non-Whitelisted Destination' error?
Trial Vonage accounts require whitelisting destination numbers. Add the recipient's number to your allowed list in the Vonage Dashboard. You will receive a verification code on the whitelisted number to confirm ownership.
What is the recommended way to deploy a Node.js Vonage SMS application?
Choose a platform like Heroku, AWS, or Google Cloud. Configure environment variables securely, and handle the private key by loading its content directly into an environment variable or storing it securely on the server filesystem. Ensure HTTPS for secure communication.