code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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.

text
[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:

  1. Queued/Buffered: MessageBird queues your message for delivery
  2. Sent: The message is handed off to the mobile carrier network
  3. Delivered: The carrier confirms the recipient's device received the message
  4. 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:

json
{
  "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 updated
  • recipients.items[].recipient: The phone number that received (or failed to receive) the message
  • reference: 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:

  1. Extract the MessageBird-Signature header value
  2. Retrieve the raw request body as a string (before parsing JSON)
  3. Compute HMAC-SHA256 hash of the body using your signing key
  4. Compare your computed signature with the header value
  5. Reject requests with invalid or missing signatures (return 401 Unauthorized)

Example verification code (we'll implement this in detail later):

javascript
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:

bash
mkdir messagebird-delivery-tracker
cd messagebird-delivery-tracker
npm init -y

2. Install Backend Dependencies

Install the required packages for the Node.js backend:

bash
npm install express messagebird dotenv body-parser

Package overview:

  • express: Web framework for REST API and webhook endpoints
  • messagebird: Official MessageBird Node.js SDK
  • dotenv: Environment variable management
  • body-parser: Middleware to parse webhook JSON payloads (also need raw body for signature verification)

For database integration (optional but recommended), install:

bash
# PostgreSQL
npm install pg

# Or MongoDB
npm install mongodb

# Or Prisma (works with PostgreSQL, MySQL, SQLite)
npm install prisma @prisma/client
npx prisma init

3. Configure Environment Variables

Create a .env file in your project root with your MessageBird credentials:

dotenv
# 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_tracker

Important: Never commit the .env file to version control. Add it to .gitignore:

gitignore
node_modules/
.env
*.log
.DS_Store

4. Obtain MessageBird Credentials

  1. Access Key: Log into MessageBird Dashboard → Developers → API access → Show Live API Keys
  2. Signing Key: Same location, scroll to "Signing key" section
  3. 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:

javascript
// 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:

  1. Signature Verification Middleware: The verifyWebhookSignature function runs before the webhook handler, rejecting invalid requests
  2. reportUrl Parameter: When sending SMS via messagebird.messages.create(), we include reportUrl pointing to our webhook endpoint
  3. In-Memory Storage: For simplicity, this example uses a Map. In production, replace with PostgreSQL, MongoDB, or Redis
  4. Status Update Logic: When webhooks arrive, we update the stored message status
  5. 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)

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:

javascript
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

bash
# 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 axios

React Component: Message Status Dashboard

Create src/components/MessageDashboard.jsx:

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:

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

bash
# 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 3000

ngrok will provide a public URL like https://abc123.ngrok.io.

2. Update Environment Variables

Update your .env file with the ngrok URL:

dotenv
WEBHOOK_URL=https://abc123.ngrok.io/webhooks/messagebird/delivery

3. Configure MessageBird Dashboard (Optional)

MessageBird also allows configuring default webhook URLs in the dashboard:

  1. Go to MessageBird Dashboard → Developers → API access
  2. Scroll to "Webhooks" section
  3. 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

  1. Start your backend: node server.js
  2. Start your frontend: cd frontend && npm run dev
  3. Open the frontend in your browser (typically http://localhost:5173)
  4. Send a test SMS to your phone number
  5. 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:

javascript
// 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:

javascript
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 .env files 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:

  1. Returns 200 OK quickly: Process webhooks asynchronously if database operations are slow
  2. Is idempotent: Handle duplicate webhooks gracefully (use message ID as unique key)
  3. Logs failures: Track webhook processing errors for debugging
javascript
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:

javascript
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:

bash
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 main

Railway:

bash
railway login
railway init
railway add
railway up

AWS Lambda + API Gateway:

Use the Serverless Framework or AWS SAM to deploy the Express app as Lambda functions.

Frontend Deployment (Vite)

Vercel:

bash
cd frontend
vercel deploy --prod

Netlify:

bash
cd frontend
npm run build
netlify deploy --prod --dir=dist

Update your frontend API calls to use the production backend URL.

Monitoring and Analytics

Track Delivery Metrics

Monitor key SMS delivery metrics:

javascript
// 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:

javascript
// 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:

  1. Firewall blocking incoming requests: Ensure your server allows incoming HTTP/HTTPS traffic
  2. Wrong webhook URL: Verify the reportUrl parameter matches your actual endpoint
  3. ngrok tunnel expired: Free ngrok tunnels expire; restart ngrok and update the URL
  4. SSL certificate issues: MessageBird requires valid HTTPS certificates (not self-signed)

Solutions:

bash
# 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 server

Signature Verification Failures

Possible causes:

  1. Wrong signing key: Double-check the key from MessageBird dashboard
  2. Request body modified: Body parsers might transform the body; use raw body
  3. Encoding issues: Ensure UTF-8 encoding when computing HMAC

Solution: Enable debug logging:

javascript
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:

  1. Invalid recipient number: Wrong format or non-existent number
  2. Carrier delays: Some carriers take minutes to confirm delivery
  3. MessageBird not sending webhooks: Check dashboard webhook configuration

Solution: Query MessageBird API directly:

javascript
messagebird.messages.read(messageId, (err, response) => {
  console.log('Current status:', response.recipients.items[0].status);
});

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.