code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / MessageBird

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:

  1. Setting up a Next.js project with API routes for SMS webhooks
  2. Installing the MessageBird Node.js SDK
  3. Configuring Supabase for SMS message tracking
  4. Sending SMS via MessageBird API with delivery report URLs
  5. Receiving delivery status webhooks via Next.js API route (HTTP GET)
  6. Storing delivery reports in Supabase database
  7. 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

  1. Your Next.js app sends an SMS via MessageBird API with reportUrl and reference parameters
  2. MessageBird returns a message object with a unique id
  3. As the delivery status changes, MessageBird sends HTTP GET requests to your reportUrl
  4. Your Next.js API route receives the GET request with query parameters
  5. The webhook handler stores the delivery status in Supabase
  6. 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

bash
npx create-next-app@latest messagebird-sms-tracker
cd messagebird-sms-tracker

Configuration:

  • 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

bash
npm install messagebird @supabase/supabase-js dotenv

Packages:

  • messagebird – Official MessageBird SDK (npm)
  • @supabase/supabase-js – Supabase client
  • dotenv – Environment variables

1.3. Configure Environment Variables

Create .env.local in project root:

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

  1. Create a Supabase project at https://supabase.com/dashboard
  2. Open the SQL Editor
  3. Create the messages table:
sql
-- 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);
  1. Copy your project URL and anon key from Settings > API to .env.local

2. Creating Supabase Client

Create lib/supabase.js:

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

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

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

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

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

bash
# Start Next.js dev server
npm run dev

# In another terminal, start ngrok
ngrok http 3000

Copy the HTTPS forwarding URL (e.g., https://abc123.ngrok.io) and update .env.local:

bash
WEBHOOK_BASE_URL=https://abc123.ngrok.io

Restart Next.js server after updating environment variables.


8. Testing the Implementation

8.1. Send Test SMS via API

bash
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

StatusDescription
scheduledMessage scheduled for future delivery
sentMessage sent to carrier, pending delivery report
bufferedMessage queued by carrier (recipient temporarily unavailable)
deliveredMessage successfully delivered to recipient
expiredMessage failed to deliver within validity period
delivery_failedMessage delivery failed (see statusErrorCode)

9.2. Common Error Codes

CodeNameDescriptionAction
1EC_UNKNOWN_SUBSCRIBERInvalid/inactive numberRemove from list
27EC_ABSENT_SUBSCRIBERRecipient out of coverageRetry later
31EC_SUBSCRIBER_BUSY_FOR_MT_SMSRecipient busyRetry later
103EC_SUBSCRIBER_OPTEDOUTRecipient opted outRemove from list permanently
104EC_SENDER_UNREGISTEREDOriginator not registeredRegister sender ID with MessageBird
105EC_CONTENT_UNREGISTEREDContent requires registrationContact MessageBird compliance
106EC_CAMPAIGN_VOLUME_EXCEEDEDDaily volume limit exceededWait 24 hours or upgrade plan
107EC_CAMPAIGN_THROUGHPUT_EXCEEDEDRate limit exceededReduce 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:

javascript
// 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.local to 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:

  1. HTTPS Only: Always use HTTPS for webhook URLs (required by MessageBird)
  2. IP Whitelisting: Restrict webhook endpoint to MessageBird IP ranges (contact MessageBird support for current IPs)
  3. Secret Token: Add secret token to webhook URL path:
    javascript
    reportUrl: `${process.env.WEBHOOK_BASE_URL}/api/webhooks/status/${process.env.WEBHOOK_SECRET}`
  4. 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 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

bash
# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

# Set environment variables in Vercel dashboard
# Settings > Environment Variables

Update .env.local variables in Vercel:

  • MESSAGEBIRD_ACCESS_KEY
  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY
  • WEBHOOK_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

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

bash
npm install @sentry/nextjs
npx @sentry/wizard@latest -i nextjs

13. Troubleshooting MessageBird SMS Delivery Issues

Common SMS Webhook and Delivery Problems

Webhook not received

  • Verify WEBHOOK_BASE_URL is 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.


Additional Resources