MessageBird SMS Delivery Status and Callbacks with Next.js and Supabase
Implement real-time SMS delivery tracking and webhook callbacks using MessageBird, Next.js API routes, and Supabase PostgreSQL for comprehensive message status monitoring.
Implement real-time SMS delivery tracking and webhook callbacks using MessageBird, Next.js API routes, and Supabase PostgreSQL for comprehensive message status monitoring.
This guide covers:
Setting up a Next.js project with API routes for SMS webhooks
Installing the MessageBird Node.js SDK
Configuring Supabase for SMS message tracking
Sending SMS via MessageBird API with delivery report URLs
Receiving delivery status webhooks via Next.js API route (HTTP GET)
Storing delivery reports in Supabase database
Handling MessageBird status codes and error scenarios
Technologies Used
Node.js 18+ (JavaScript runtime)
Next.js (React framework with API routes)
MessageBird SMS API (messaging platform)
messagebird (official Node.js SDK)
Supabase (PostgreSQL database)
@supabase/supabase-js (Supabase client)
dotenv (environment variables)
How SMS Delivery Tracking Works
Your Next.js app sends an SMS via MessageBird API with reportUrl and reference parameters
MessageBird returns a message object with a unique id
As the delivery status changes, MessageBird sends HTTP GET requests to your reportUrl
Your Next.js API route receives the GET request with query parameters
The webhook handler stores the delivery status in Supabase
Return 200 OK (or MessageBird retries up to 10 times)
Rate Limiting: Implement rate limiting on webhook endpoint using middleware
10.3. Supabase Row Level Security
Enable RLS policies to restrict database access:
SQL
-- Allow inserts only from authenticated usersCREATE POLICY "Allow insert for authenticated" ON messages
FORINSERTWITHCHECK (auth.role() ='authenticated');
-- Allow updates only for specific columnsCREATE POLICY "Allow status updates" ON messages
FORUPDATEUSING (true)
WITHCHECK (true);
11. Deploying Next.js SMS Application to Production
Check MessageBird dashboard > Logs for delivery attempts
Database insert fails
Verify Supabase environment variables
Check RLS policies allow inserts
Verify table schema matches insert data
SMS not sending
Verify MESSAGEBIRD_ACCESS_KEY
Check account balance
Verify originator is approved for destination country
Check recipient number is in E.164 format (+1234567890)
Authentication errors
MessageBird uses AccessKey header (not JWT)
Verify access key has SMS permissions
Check for trailing spaces in access key
Conclusion
You've successfully built a production-ready SMS delivery tracking system using MessageBird webhooks, Next.js API routes, and Supabase database for real-time message status monitoring.
Key takeaways:
MessageBird webhooks use HTTP GET (not POST)
Always respond with 200 OK to prevent retries
Store delivery reports for analytics and compliance
Handle error codes appropriately (permanent vs. temporary)
Use the Vonage Messages API with the @vonage/server-sdk in your Node.js application. Initialize the Vonage client with your API key, secret, application ID, and private key. Then, use vonage.messages.send() with the recipient's number, your Vonage virtual number, and the message text to send SMS messages.
The Vonage Messages API is a multi-channel communication platform that allows you to send and receive SMS, MMS, WhatsApp messages, and more. This guide demonstrates its use for sending SMS messages and receiving delivery status updates.
Webhooks provide a real-time, asynchronous mechanism for Vonage to push delivery status updates to your application. Instead of constantly polling the API, your application receives immediate notifications when the message status changes, like being delivered or failing.
Use a Vonage Application ID and its associated private key when using the Messages API. This ensures secure authentication using JWT (JSON Web Tokens), which are important when handling webhooks for security reasons.
Alphanumeric Sender IDs might be allowed, but you must check country-specific regulations. In some regions (e.g., the US), you must use a Vonage virtual number. If allowed, ensure it's pre-registered with Vonage.
Configure a 'Status URL' in your Vonage Application settings. This URL should point to a publicly accessible endpoint in your application (e.g., using ngrok for development). Vonage will send an HTTP POST request to this URL with delivery status updates.
ngrok creates a temporary public URL that tunnels to your local development server. This allows Vonage to send webhooks to your locally running application during development, essential for receiving delivery status updates.
Ensure your webhook endpoint returns a 2xx HTTP status code (ideally 200 OK or 204 No Content) as quickly as possible. This tells Vonage that you've received the webhook. Your endpoint logic should be idempotent to handle potential duplicate webhook deliveries.
Common statuses include 'submitted', 'delivered', 'failed', 'accepted', 'buffered', 'expired', and 'unknown'. 'Buffered' often means the recipient's device is offline. Each status describes a stage in the delivery process.
Verify that ngrok is running and correctly forwarding to your server's port, the Status URL in your Vonage Application settings is accurate (including the '/webhooks/status' path), and your server application is running without errors.
The @vonage/server-sdk simplifies interaction with Vonage APIs in Node.js. It handles authentication, request construction, and response parsing for sending SMS messages and other Vonage services.
Sensitive data, such as API keys, secrets, and private keys, should never be hardcoded. Environment variables (like in a .env file) offer a secure way to manage these credentials locally, keeping them out of your codebase.
A suggested schema includes columns for message_uuid, to_number, from_number, message_body, submission and status update timestamps, current status, error codes, and usage details.
Respond to the webhook request with a 200 OK immediately. Perform database updates or other time-consuming tasks asynchronously in the background to prevent blocking the webhook response and causing Vonage retries.
Use HTTPS, potentially IP whitelisting or secrets in URLs. Currently, simple signature validation is not supported for messages API webhooks. Cryptographically signed webhooks are more complex to implement for these particular APIs.
Implement real-time SMS delivery tracking and webhook callbacks using MessageBird, Next.js API routes, and Supabase PostgreSQL for comprehensive message status monitoring.
This guide covers:
Technologies Used
messagebird(official Node.js SDK)@supabase/supabase-js(Supabase client)dotenv(environment variables)How SMS Delivery Tracking Works
reportUrlandreferenceparametersidreportUrlPrerequisites
1. Setting Up Next.js Project with MessageBird
1.1. Create Next.js Application
npx create-next-app@latest messagebird-sms-tracker cd messagebird-sms-trackerConfiguration:
src/directory: No1.2. Install Dependencies
Packages:
messagebird– Official MessageBird SDK (npm)@supabase/supabase-js– Supabase clientdotenv– Environment variables1.3. Configure Environment Variables
Create
.env.localin project root:# .env.local # MessageBird API Credentials MESSAGEBIRD_ACCESS_KEY=your_messagebird_access_key_here # Supabase Configuration NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here # Webhook Configuration WEBHOOK_BASE_URL=https://your-ngrok-url.ngrok.io # or production domain # Message Configuration MESSAGEBIRD_ORIGINATOR=YourBrand # or phone number RECIPIENT_NUMBER=+1234567890 # Test recipient (E.164 format)Security:
.env.localis automatically ignored in.gitignore. Never commit API keys.1.4. Set Up Supabase Database for SMS Tracking
-- Create messages table for SMS tracking CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), message_id VARCHAR(255) UNIQUE NOT NULL, reference VARCHAR(255), originator VARCHAR(50), recipient VARCHAR(50), body TEXT, status VARCHAR(50) DEFAULT 'pending', status_reason VARCHAR(255), status_error_code INTEGER, status_datetime TIMESTAMP, mccmnc VARCHAR(10), ported BOOLEAN, message_length INTEGER, message_part_count INTEGER, price_amount DECIMAL(10,4), price_currency VARCHAR(3), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- Create indexes for fast lookups CREATE INDEX idx_message_id ON messages(message_id); CREATE INDEX idx_reference ON messages(reference); CREATE INDEX idx_status ON messages(status); CREATE INDEX idx_created_at ON messages(created_at DESC); -- Enable Row Level Security (optional, recommended for production) ALTER TABLE messages ENABLE ROW LEVEL SECURITY; -- Create policy to allow all operations (adjust for production security needs) CREATE POLICY "Allow all operations" ON messages FOR ALL USING (true);.env.local2. Creating Supabase Client
Create
lib/supabase.js:// lib/supabase.js import { createClient } from '@supabase/supabase-js'; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseAnonKey) { throw new Error('Missing Supabase environment variables'); } export const supabase = createClient(supabaseUrl, supabaseAnonKey);3. Creating MessageBird Client Wrapper
Create
lib/messagebird.js:// lib/messagebird.js const messagebird = require('messagebird'); const accessKey = process.env.MESSAGEBIRD_ACCESS_KEY; if (!accessKey) { throw new Error('Missing MESSAGEBIRD_ACCESS_KEY environment variable'); } // Initialize MessageBird client const client = messagebird(accessKey); module.exports = client;4. Sending SMS Messages with Delivery Status Reports
Create
lib/sendSMS.js:// lib/sendSMS.js const messagebirdClient = require('./messagebird'); const { supabase } = require('./supabase'); /** * Send SMS via MessageBird with delivery report tracking * @param {Object} params * @param {string} params.recipient - Phone number in E.164 format (e.g., +1234567890) * @param {string} params.body - Message content * @param {string} params.originator - Sender ID or phone number * @param {string} params.reference - Optional client reference for tracking * @param {string} params.reportUrl - Webhook URL for delivery reports * @returns {Promise<Object>} MessageBird message object */ async function sendSMS({ recipient, body, originator, reference, reportUrl }) { try { // Validate required fields if (!recipient || !body || !originator) { throw new Error('Missing required fields: recipient, body, originator'); } // Generate reference if not provided const messageReference = reference || `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Send SMS via MessageBird const message = await new Promise((resolve, reject) => { messagebirdClient.messages.create( { originator, recipients: [recipient], body, reference: messageReference, reportUrl: reportUrl || `${process.env.WEBHOOK_BASE_URL}/api/webhooks/status`, }, (err, response) => { if (err) { reject(err); } else { resolve(response); } } ); }); console.log('SMS sent successfully:', { messageId: message.id, reference: messageReference, recipient, }); // Store initial message record in Supabase const { data, error } = await supabase .from('messages') .insert([ { message_id: message.id, reference: messageReference, originator, recipient, body, status: message.recipients?.items?.[0]?.status || 'sent', status_reason: message.recipients?.items?.[0]?.statusReason, message_length: message.recipients?.items?.[0]?.messageLength, message_part_count: message.recipients?.items?.[0]?.messagePartCount, created_at: new Date().toISOString(), }, ]) .select(); if (error) { console.error('Error storing message in database:', error); // Don't fail SMS send if database insert fails } else { console.log('Message stored in database:', data); } return message; } catch (error) { console.error('Error sending SMS:', error); throw error; } } module.exports = { sendSMS };5. Creating MessageBird Webhook Handler for Delivery Status Updates
Create
pages/api/webhooks/status.js:// pages/api/webhooks/status.js import { supabase } from '../../../lib/supabase'; /** * MessageBird Status Report Webhook Handler * Receives HTTP GET requests from MessageBird with delivery status updates * * Query Parameters (from MessageBird): * - id: Message ID * - reference: Client reference * - recipient: Recipient phone number * - status: scheduled|sent|buffered|delivered|expired|delivery_failed * - statusReason: Detailed status description * - statusErrorCode: Error code (optional) * - statusDatetime: RFC3339 timestamp * - mccmnc: Mobile operator code * - ported: Whether number was ported (0 or 1) * - messageLength: Character count * - messagePartCount: Number of SMS segments * - datacoding: plain or unicode * - price[amount]: Cost (optional) * - price[currency]: Currency code (optional) */ export default async function handler(req, res) { // MessageBird sends status reports via GET requests if (req.method !== 'GET') { return res.status(405).json({ error: 'Method not allowed' }); } try { // Extract parameters from query string const { id, reference, recipient, status, statusReason, statusErrorCode, statusDatetime, mccmnc, ported, messageLength, messagePartCount, datacoding, } = req.query; // Extract price (MessageBird sends as price[amount] and price[currency]) const priceAmount = req.query['price[amount]']; const priceCurrency = req.query['price[currency]']; console.log('Received delivery status webhook:', { id, reference, recipient, status, statusReason, statusErrorCode, statusDatetime, }); // Validate required fields if (!id || !status) { console.error('Missing required fields in webhook'); return res.status(400).json({ error: 'Missing required fields' }); } // Update message status in Supabase const { data, error } = await supabase .from('messages') .update({ status, status_reason: statusReason, status_error_code: statusErrorCode ? parseInt(statusErrorCode, 10) : null, status_datetime: statusDatetime, mccmnc, ported: ported === '1', message_length: messageLength ? parseInt(messageLength, 10) : null, message_part_count: messagePartCount ? parseInt(messagePartCount, 10) : null, price_amount: priceAmount ? parseFloat(priceAmount) : null, price_currency: priceCurrency, updated_at: new Date().toISOString(), }) .eq('message_id', id) .select(); if (error) { console.error('Error updating message in database:', error); // Still return 200 OK to prevent MessageBird retries return res.status(200).send('OK'); } console.log('Message status updated in database:', data); // IMPORTANT: MessageBird requires 200 OK response // Failure to respond with 200 will trigger retries (up to 10 times) return res.status(200).send('OK'); } catch (error) { console.error('Error processing webhook:', error); // Still return 200 OK to prevent infinite retries return res.status(200).send('OK'); } }6. Creating Next.js API Endpoint to Send SMS
Create
pages/api/send-sms.js:// pages/api/send-sms.js const { sendSMS } = require('../../lib/sendSMS'); export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { const { recipient, body, originator, reference } = req.body; // Validate required fields if (!recipient || !body) { return res.status(400).json({ error: 'Missing required fields: recipient, body' }); } // Use default originator from env if not provided const senderOriginator = originator || process.env.MESSAGEBIRD_ORIGINATOR; if (!senderOriginator) { return res.status(400).json({ error: 'Missing originator (sender ID)' }); } // Send SMS const message = await sendSMS({ recipient, body, originator: senderOriginator, reference, reportUrl: `${process.env.WEBHOOK_BASE_URL}/api/webhooks/status`, }); return res.status(200).json({ success: true, messageId: message.id, reference: message.reference, status: message.recipients?.items?.[0]?.status, }); } catch (error) { console.error('Error in send-sms API:', error); return res.status(500).json({ error: 'Failed to send SMS', details: error.message, }); } }7. Exposing Local Server with ngrok for Webhook Testing
For development, use ngrok to expose your local Next.js server:
# Start Next.js dev server npm run dev # In another terminal, start ngrok ngrok http 3000Copy the HTTPS forwarding URL (e.g.,
https://abc123.ngrok.io) and update.env.local:Restart Next.js server after updating environment variables.
8. Testing the Implementation
8.1. Send Test SMS via API
curl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{ "recipient": "+1234567890", "body": "Test message from MessageBird + Next.js", "originator": "YourBrand" }'8.2. Verify Database Storage
Check Supabase dashboard > Table Editor > messages for the new record.
8.3. Monitor Webhook Delivery
Watch your Next.js console for incoming webhooks as the status changes (sent → delivered).
9. MessageBird Error Handling and Delivery Status Codes
9.1. SMS Delivery Status Values
scheduledsentbuffereddeliveredexpireddelivery_failed9.2. Common Error Codes
Full error code list: https://developers.messagebird.com/api/sms-messaging#sms-error-codes
9.3. Webhook Retry Logic
MessageBird retries webhook delivery up to 10 times without a 200 OK response. Implement idempotent handlers:
// Check if status update already processed const { data: existing } = await supabase .from('messages') .select('status, status_datetime') .eq('message_id', id) .single(); if (existing && existing.status_datetime >= statusDatetime) { console.log('Status already up to date, skipping'); return res.status(200).send('OK'); }10. Security Best Practices for SMS Webhooks
10.1. Protecting API Keys and Environment Variables
.env.localto version control10.2. MessageBird Webhook Security and Validation
MessageBird sends webhooks via HTTP GET with query parameters. Implement these protections:
reportUrl: `${process.env.WEBHOOK_BASE_URL}/api/webhooks/status/${process.env.WEBHOOK_SECRET}`10.3. Supabase Row Level Security
Enable RLS policies to restrict database access:
-- Allow inserts only from authenticated users CREATE POLICY "Allow insert for authenticated" ON messages FOR INSERT WITH CHECK (auth.role() = 'authenticated'); -- Allow updates only for specific columns CREATE POLICY "Allow status updates" ON messages FOR UPDATE USING (true) WITH CHECK (true);11. Deploying Next.js SMS Application to Production
11.1. Deploy to Vercel
# Install Vercel CLI npm install -g vercel # Deploy vercel # Set environment variables in Vercel dashboard # Settings > Environment VariablesUpdate
.env.localvariables in Vercel:MESSAGEBIRD_ACCESS_KEYNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYWEBHOOK_BASE_URL(set to production domain)11.2. Alternative Deployment Platforms
12. SMS Delivery Monitoring and Analytics
12.1. Query SMS Delivery Statistics
// Get delivery rate by status const { data: stats } = await supabase .from('messages') .select('status, count(*)') .group('status'); // Get messages by date range const { data: recentMessages } = await supabase .from('messages') .select('*') .gte('created_at', '2025-01-01') .order('created_at', { ascending: false }); // Calculate delivery rate const { data: delivered } = await supabase .from('messages') .select('count(*)') .eq('status', 'delivered') .single(); const { data: total } = await supabase .from('messages') .select('count(*)') .single(); const deliveryRate = (delivered.count / total.count) * 100;12.2. Error Tracking
Integrate error monitoring:
13. Troubleshooting MessageBird SMS Delivery Issues
Common SMS Webhook and Delivery Problems
Webhook not received
WEBHOOK_BASE_URLis a public HTTPS URLDatabase insert fails
SMS not sending
MESSAGEBIRD_ACCESS_KEYAuthentication errors
Conclusion
You've successfully built a production-ready SMS delivery tracking system using MessageBird webhooks, Next.js API routes, and Supabase database for real-time message status monitoring.
Key takeaways:
For production, add:
Learn more about implementing SMS API integrations and webhook handling best practices to enhance your messaging infrastructure.
Additional Resources