code examples
code examples
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.
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.
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
reportUrlandreferenceparameters - 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)
Prerequisites
- Node.js 18+ and npm (download)
- MessageBird account (sign up)
- Access key from dashboard
- Virtual phone number or approved sender ID
- Supabase account (sign up)
- Project URL and anon key
- Public URL for webhooks (use ngrok for local development)
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:
- TypeScript: No (or Yes)
- ESLint: Yes
- Tailwind CSS: Optional
src/directory: No- App Router: No (use Pages Router)
- Import alias: No
1.2. Install Dependencies
npm install messagebird @supabase/supabase-js dotenvPackages:
messagebird– Official MessageBird SDK (npm)@supabase/supabase-js– Supabase clientdotenv– Environment variables
1.3. Configure Environment Variables
Create .env.local in 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.local is automatically ignored in .gitignore. Never commit API keys.
1.4. Set Up Supabase Database for SMS Tracking
- Create a Supabase project at https://supabase.com/dashboard
- Open the SQL Editor
- Create the messages table:
-- 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);- Copy your project URL and anon key from Settings > API to
.env.local
2. 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:
WEBHOOK_BASE_URL=https://abc123.ngrok.ioRestart 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
| Status | Description |
|---|---|
scheduled | Message scheduled for future delivery |
sent | Message sent to carrier, pending delivery report |
buffered | Message queued by carrier (recipient temporarily unavailable) |
delivered | Message successfully delivered to recipient |
expired | Message failed to deliver within validity period |
delivery_failed | Message delivery failed (see statusErrorCode) |
9.2. Common Error Codes
| Code | Name | Description | Action |
|---|---|---|---|
| 1 | EC_UNKNOWN_SUBSCRIBER | Invalid/inactive number | Remove from list |
| 27 | EC_ABSENT_SUBSCRIBER | Recipient out of coverage | Retry later |
| 31 | EC_SUBSCRIBER_BUSY_FOR_MT_SMS | Recipient busy | Retry later |
| 103 | EC_SUBSCRIBER_OPTEDOUT | Recipient opted out | Remove from list permanently |
| 104 | EC_SENDER_UNREGISTERED | Originator not registered | Register sender ID with MessageBird |
| 105 | EC_CONTENT_UNREGISTERED | Content requires registration | Contact MessageBird compliance |
| 106 | EC_CAMPAIGN_VOLUME_EXCEEDED | Daily volume limit exceeded | Wait 24 hours or upgrade plan |
| 107 | EC_CAMPAIGN_THROUGHPUT_EXCEEDED | Rate limit exceeded | Reduce sending rate |
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
- Never commit
.env.localto version control - Use different access keys for development and production
- Rotate API keys periodically
10.2. MessageBird Webhook Security and Validation
MessageBird sends webhooks via HTTP GET with query parameters. Implement these protections:
- HTTPS Only: Always use HTTPS for webhook URLs (required by MessageBird)
- IP Whitelisting: Restrict webhook endpoint to MessageBird IP ranges (contact MessageBird support for current IPs)
- Secret Token: Add secret token to webhook URL path:
javascript
reportUrl: `${process.env.WEBHOOK_BASE_URL}/api/webhooks/status/${process.env.WEBHOOK_SECRET}` - Rate Limiting: Implement rate limiting on webhook endpoint using middleware
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.local variables in Vercel:
MESSAGEBIRD_ACCESS_KEYNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYWEBHOOK_BASE_URL(set to production domain)
11.2. Alternative Deployment Platforms
- Netlify: Use Next.js Runtime plugin
- AWS Amplify: Native Next.js support
- Railway: One-click Next.js deployment
- DigitalOcean App Platform: Managed Next.js hosting
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:
npm install @sentry/nextjs
npx @sentry/wizard@latest -i nextjs13. Troubleshooting MessageBird SMS Delivery Issues
Common SMS Webhook and Delivery Problems
Webhook not received
- Verify
WEBHOOK_BASE_URLis a public HTTPS URL - Check ngrok is running (development)
- Verify Next.js server is running
- 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)
- Implement idempotent webhook processing
For production, add:
- Error handling and logging
- Rate limiting and security
- Monitoring and alerting
- Message queuing for bulk sends
- User authentication
Learn more about implementing SMS API integrations and webhook handling best practices to enhance your messaging infrastructure.