MessageBird SMS Appointment Reminders with Next.js: Complete Tutorial 2025
Build automated SMS appointment reminders using MessageBird API, Next.js 14, and Prisma. Step-by-step guide with scheduling, webhooks, and production deployment for Node.js applications.
Overview: Building an Appointment Reminder System with MessageBird SMS API
You'll build a complete appointment reminder system that sends automated SMS notifications using MessageBird's SMS API, Next.js 14, and Prisma ORM. This comprehensive tutorial takes you from initial setup through production deployment, teaching you how to schedule SMS messages, manage appointments in a PostgreSQL database, and handle delivery webhooks for real-time status tracking.
What You'll Build:
Full-stack Next.js 14 application with App Router and TypeScript
Appointment booking system with PostgreSQL database and Prisma ORM
Automated SMS reminders sent 24 hours before appointments using MessageBird
Webhook integration for delivery status tracking and failure handling
Production-ready error handling with exponential backoff retry logic
Prerequisites:
Node.js 18+ installed on your system
MessageBird account with API key (get one at messagebird.com)
Basic knowledge of React, TypeScript, and Next.js
PostgreSQL database (local or hosted on Supabase/Vercel)
Why MessageBird for Appointment Reminders?
MessageBird stands out for scheduled messaging:
Global SMS Reach: Send messages to 200+ countries with reliable delivery rates
Scheduled Messaging: Schedule SMS messages up to 30 days in advance using RFC3339 timestamps
Delivery Tracking: Track message status through webhooks and delivery reports
Developer Experience: Clean REST API with official Node.js SDK and comprehensive documentation
Rate Limits: Handle 500 requests per second for POST operations and 50 req/s for GET operations
Lookup API Integration: Verify phone numbers before sending to reduce costs and improve delivery
Section 1: Project Setup and MessageBird Configuration
Step 1: Create Your Next.js Project
Create a new Next.js 14 project with TypeScript and App Router:
Bash
npx create-next-app@latest messagebird-reminders
cd messagebird-reminders
Select these options during setup:
TypeScript: Yes
ESLint: Yes
Tailwind CSS: Yes
src/ directory: No
App Router: Yes
Import alias: Default (@/*)
Step 2: Install Required Dependencies
Install MessageBird SDK, Prisma ORM, and utility libraries:
The MESSAGEBIRD_ORIGINATOR (Sender ID) can be either a phone number or alphanumeric string (max 11 characters). Alphanumeric Sender IDs are not supported in the US and Canada—you must use a purchased phone number instead. Many countries require pre-registration of alphanumeric Sender IDs before use:
Pre-registration required: United Arab Emirates (promotional messages need "AD-" prefix), India, Saudi Arabia, Turkey, Singapore, and others. Check MessageBird's country restrictions documentation for your target countries.
No registration needed: Most European countries (UK, Germany, Netherlands, France), Australia, and many others allow instant provisioning.
Sender ID format: Alphanumeric IDs support upper/lowercase ASCII letters, digits 0-9, and spaces (11 characters max). Examples: "YourBrand", "Acme Corp", "Shop24".
Get your MessageBird API key:
Sign up at messagebird.com
Navigate to Developers → API Keys
Copy your Live API key (starts with live_)
Use Test API key for development (starts with test_)
Important: Never commit .env.local to version control. Add it to .gitignore.
Section 2: Database Schema with Prisma
Step 1: Initialize Prisma
Set up Prisma in your project:
Bash
npx prisma init
This creates prisma/schema.prisma and adds DATABASE_URL to your .env file.
Step 2: Define Your Appointment Schema
Open prisma/schema.prisma and replace the contents:
2. Rate Limiting:
MessageBird enforces rate limits: 500 requests per second for POST operations (message creation) and 50 req/s for GET operations. Implement exponential backoff for production systems.
3. Phone Number Format:
Always use E.164 format: +[country code][number]
Note: MessageBird doesn't support canceling scheduled messages via API. Mark appointments as canceled in your database and filter them when processing reminders.
Why node-cron doesn't work on Vercel:
Vercel uses serverless functions that spin up on-demand and shut down after handling requests. Node-cron requires a persistent Node.js process to run scheduled tasks, which is incompatible with Vercel's stateless, ephemeral execution model. For production on Vercel, always use Vercel Cron Jobs configured via vercel.json.
Add to app/layout.tsx:
TypeScript
import { startCronJobs } from'@/lib/cron';
// Only run in developmentif (process.env.NODE_ENV === 'development') {
startCronJobs();
}
Section 6: Webhook Integration for Delivery Status
Add customer preferences for reminder timing and channels:
Prisma
model Customer {
id String @id @default(cuid())
phone String @unique
name String
email String?
preferredChannel String @default("SMS") // SMS, EMAIL, BOTH
reminderHoursBefore Int @default(24)
timezone String @default("America/New_York")
createdAt DateTime @default(now())
}
Query customer preferences when scheduling reminders and adjust timing accordingly.
Best Practices and Production Tips
1. Phone Number Validation:
Use MessageBird Lookup API to verify numbers before sending:
TypeScript
exportasyncfunctionvalidatePhoneNumber(phone: string): Promise<boolean> {
returnnewPromise((resolve, reject) => {
messagebird.lookup.read(phone, (err, response) => {
if (err) {
resolve(false); // Invalid number
} else {
resolve(response.type === 'mobile'); // Only mobile numbers
}
});
});
}
MessageBird Lookup API Costs:
Approximately $0.01-0.02 per lookup depending on country
Use strategically: validate on signup, not on every message
Cache validation results for known numbers
Only validate numbers that failed delivery previously
2. Message Templates:
Store message templates in database for easy updates:
TypeScript
const templates = {
reminder: (name: string, date: string) =>`Hi ${name}, this is a reminder about your appointment on ${date}. Reply CANCEL to cancel.`,
confirmation: (name: string) =>`Thanks ${name}! Your appointment is confirmed. You'll receive a reminder 24 hours before.`
};
SMS Length Limits:
GSM-7 encoding (plain): 160 characters per message
Unicode (special characters): 70 characters per message
Concatenated messages: Automatically split with 7-byte header (153/67 char segments)
MessageBird charges per message segment
Use datacoding: 'auto' to automatically select optimal encoding
3. Timezone Handling:
Always store dates in UTC and convert to customer timezone for display:
TypeScript
import moment from'moment-timezone';
functionformatAppointmentTime(date: Date, timezone: string): string {
returnmoment(date).tz(timezone).format('MMM DD, YYYY [at] h:mm A z');
}
Note: Consider migrating from Moment.js (maintenance mode) to Day.js or date-fns for new projects.
4. Cost Optimization:
Validate phone numbers before sending to reduce failed deliveries
Use MessageBird Lookup API to verify number validity
Implement message deduplication to prevent double-sending
Monitor delivery rates by country and adjust strategy
Consider alternative channels (Email, WhatsApp) for non-urgent reminders
Example Cost Calculation:
US SMS: $0.0075 per message
1,000 appointments/month with 24h reminders: $7.50/month
How do I get a MessageBird API key for sending SMS?
Sign up for a free MessageBird account at messagebird.com, navigate to Developers → API Keys in your dashboard, and copy your Live API key (starts with live_). For development and testing, use your Test API key (starts with test_) to simulate message sending without charges or actual SMS delivery.
What phone number format does MessageBird require?
MessageBird requires E.164 international format: +[country code][number] with no spaces or special characters. Examples: +14155552671 (US), +447700900123 (UK). Remove parentheses, hyphens, and spaces before sending. Invalid formats like (415) 555-2671 or 415-555-2671 will cause API errors.
Can I schedule SMS messages more than 24 hours in advance with MessageBird?
Yes, MessageBird supports scheduling SMS messages up to 30 days in advance. Use the scheduledDatetime parameter with RFC3339 format (e.g., 2024-01-15T10:30:00Z) when creating messages. Schedule your appointment reminders days or weeks ahead to ensure timely delivery without manual intervention.
How do I handle failed SMS deliveries in my appointment system?
Implement webhook integration to receive real-time delivery status updates from MessageBird. When a message fails, your webhook endpoint updates the database with the failure status. Add retry logic with exponential backoff (1s, 2s, 4s delays) for network errors, and consider fallback channels like email for persistent failures.
What are MessageBird's rate limits for SMS sending?
MessageBird enforces 500 requests per second for POST operations (message creation) and 50 requests per second for GET operations (status checks). For bulk sending, batch your requests and implement exponential backoff retry logic. Monitor your API usage in the MessageBird dashboard to avoid rate limit errors.
How much does it cost to send SMS with MessageBird?
MessageBird pricing varies by destination country, typically ranging from $0.01 to $0.15 per SMS. US SMS costs approximately $0.0075 per message, while international rates vary. Check current pricing at messagebird.com/pricing. Use the Test API key during development to avoid charges while building your application.
Can I cancel a scheduled SMS message in MessageBird?
MessageBird doesn't support API-based cancellation of scheduled messages. Instead, mark appointments as canceled in your database and filter them when your cron job runs. Your scheduler should check the reminderStatus field and skip sending messages for canceled appointments before calling the MessageBird API.
How do I verify phone numbers before sending SMS?
Use MessageBird's Lookup API to verify phone numbers before sending messages. The Lookup API returns number validity, carrier information, and number type (mobile vs. landline). This reduces failed deliveries, lowers costs, and improves your sender reputation by avoiding invalid destinations.
What's the best way to handle timezones for international appointments?
Store all appointment dates in UTC format in your database. When sending reminders, convert UTC timestamps to the customer's local timezone using moment-timezone or date-fns-tz. Include timezone information in reminder messages (e.g., "Your appointment is Jan 15, 2024 at 2:00 PM EST") to prevent confusion.
How do I deploy a Next.js MessageBird application to production?
Deploy to Vercel (recommended for Next.js), Railway, or AWS Amplify. Configure production environment variables (MessageBird Live API key, database URL, webhook secrets), set up Vercel Cron for scheduled jobs, enable database connection pooling for serverless environments, and implement webhook signature verification for security.
What are the common MessageBird error codes and how do I handle them?
Common errors:
EC_UNKNOWN_SUBSCRIBER (code 1): Invalid phone number, remove from database
EC_SENDER_UNREGISTERED (code 104): Sender ID not registered in destination country, check country restrictions
Conclusion: Your Production-Ready Appointment System
You've built a complete SMS appointment reminder system with MessageBird API, Next.js 14, and Prisma ORM. Your application now handles scheduled SMS delivery, database management, webhook processing, and production-grade error handling with comprehensive monitoring and security features.
What You Accomplished:
✅ MessageBird SMS API integration with scheduled messaging and RFC3339 format
✅ Next.js 14 App Router with TypeScript API routes and server components
✅ Prisma ORM with PostgreSQL database and optimized connection pooling
✅ Automated cron jobs for reminder scheduling (Vercel Cron and node-cron)
✅ Webhook integration for real-time delivery status tracking
✅ Production-ready error handling with exponential backoff retry logic
✅ Timezone support for international customers using moment-timezone
✅ Security features including webhook signature verification and rate limiting
Next Steps for Production:
Set up monitoring: Implement Winston logging and connect to monitoring service (DataDog, New Relic, Sentry)
Add customer portal: Build a frontend where customers manage their appointments and preferences
Implement analytics: Track delivery rates, engagement metrics, and cost per message by country
Scale infrastructure: Set up Redis caching for database queries and rate limiting with Upstash
Add alternative channels: Integrate Email (Resend, SendGrid) and WhatsApp as fallback options
Internationalization: Support multiple languages based on customer preferences using i18next
Production Deployment Checklist:
Configure production environment variables with secure secrets (use openssl rand -base64 32)
Set up database connection pooling for serverless environments (Vercel, AWS Lambda)
Enable webhook signature verification for security against spoofing attacks
Configure rate limiting on public API endpoints using Upstash Rate Limit
Set up error tracking and monitoring (Sentry, LogRocket, or DataDog)
Implement database backup strategy (automated daily backups with point-in-time recovery)
Configure custom domain with HTTPS certificate (automatic on Vercel)
Test cron jobs in production environment with MessageBird Test API key
Document API endpoints with OpenAPI/Swagger specification
Set up uptime monitoring and alerting (UptimeRobot, Pingdom, or StatusCake)
Your appointment reminder system is now ready to scale and serve thousands of customers with reliable, automated SMS notifications. Start testing with MessageBird's test API key, then deploy to production when you're ready to send real messages to your customers.
Frequently asked questions
One effective way to reduce appointment no-shows is to implement automated SMS reminders. This guide details building an application with Next.js and MessageBird to send timely reminders, improving customer communication and minimizing missed appointments.
MessageBird, a Communication Platform as a Service (CPaaS), is used for validating phone numbers via its Lookup API and sending scheduled SMS reminders through its Messages API. Its developer-friendly SDK and direct scheduling capabilities make it ideal for this purpose.
Next.js is chosen for this project due to its excellent developer experience, performance optimizations, and built-in API route capabilities, making it suitable for both frontend UI and backend logic.
SMS appointment reminders should be sent a few hours before the appointment, giving enough notice but not too far in advance. This application is set up to send reminders 3 hours prior to the appointment time.
The MessageBird Lookup API is used to validate phone numbers. The API route checks number validity and format, ensuring it's a mobile number capable of receiving SMS. It also uses a default country code for convenience.
PostgreSQL is used as the database for storing appointment data. Its robustness and reliability make it suitable for storing and managing this information.
Prisma is a next-generation ORM for Node.js and TypeScript. It simplifies database interactions, manages migrations, and enhances type safety in the application.
Obtain your MessageBird API key from the MessageBird Dashboard under Developers > API access > Live API Key. Then, paste this key into the .env file as MESSAGEBIRD_API_KEY, ensuring it's kept confidential.
The MessageBird originator is the sender ID that recipients see on their phones when they receive the SMS. This can be an alphanumeric string (with restrictions) or an E.164 formatted phone number.
Alphanumeric sender IDs are possible but not universally supported. Check MessageBird's documentation for country-specific restrictions. Numeric sender IDs (phone numbers) are generally recommended for broader compatibility.
The application stores the user's time zone and uses moment-timezone to accurately calculate reminder times in UTC for MessageBird. This ensures reminders are delivered at the correct local time for each user.
You'll need Node.js v18 or later, npm/yarn, a MessageBird account and API key, access to a PostgreSQL database, and basic knowledge of React, Next.js, and asynchronous JavaScript.
The MessageBird Messages API is used for scheduling SMS messages. The messagebird.messages.create function is called with the recipient, message body, and scheduled time in ISO 8601 format.
The application uses extensive try...catch blocks and logs errors using console.error. It returns informative JSON error responses with appropriate HTTP status codes for client and server errors.
Overview: Building an Appointment Reminder System with MessageBird SMS API
You'll build a complete appointment reminder system that sends automated SMS notifications using MessageBird's SMS API, Next.js 14, and Prisma ORM. This comprehensive tutorial takes you from initial setup through production deployment, teaching you how to schedule SMS messages, manage appointments in a PostgreSQL database, and handle delivery webhooks for real-time status tracking.
What You'll Build:
Prerequisites:
Why MessageBird for Appointment Reminders?
MessageBird stands out for scheduled messaging:
Section 1: Project Setup and MessageBird Configuration
Step 1: Create Your Next.js Project
Create a new Next.js 14 project with TypeScript and App Router:
npx create-next-app@latest messagebird-reminders cd messagebird-remindersSelect these options during setup:
src/directory: NoStep 2: Install Required Dependencies
Install MessageBird SDK, Prisma ORM, and utility libraries:
Package breakdown:
messagebird: Official Node.js SDK for MessageBird API (requires Node.js >= 0.10)prismaand@prisma/client: Type-safe database ORMmoment-timezone: Handle timezone conversions for scheduled messagestsx: Run TypeScript files directly during developmentStep 3: Configure Environment Variables
Create
.env.localin your project root:# MessageBird Configuration MESSAGEBIRD_API_KEY=your_messagebird_live_api_key_here MESSAGEBIRD_ORIGINATOR=YourBrand # Sender ID (max 11 alphanumeric chars) # Database Configuration DATABASE_URL="postgresql://user:password@localhost:5432/appointments?schema=public" # Application Configuration NEXT_PUBLIC_APP_URL=http://localhost:3000 WEBHOOK_SECRET=your_webhook_secret_hereSender ID Configuration:
The
MESSAGEBIRD_ORIGINATOR(Sender ID) can be either a phone number or alphanumeric string (max 11 characters). Alphanumeric Sender IDs are not supported in the US and Canada—you must use a purchased phone number instead. Many countries require pre-registration of alphanumeric Sender IDs before use:Get your MessageBird API key:
live_)test_)Important: Never commit
.env.localto version control. Add it to.gitignore.Section 2: Database Schema with Prisma
Step 1: Initialize Prisma
Set up Prisma in your project:
This creates
prisma/schema.prismaand addsDATABASE_URLto your.envfile.Step 2: Define Your Appointment Schema
Open
prisma/schema.prismaand replace the contents:Schema features:
customerPhonestores E.164 formatted numbers (+1234567890)appointmentDateuses ISO 8601 datetime formatmessageBirdIdtracks the MessageBird message ID for status updatesappointmentDateandreminderStatusoptimize queriesReminderStatusenum tracks the full lifecycle of reminder messagesStep 3: Run Database Migrations
Create and apply your database schema:
This creates your PostgreSQL tables and generates the Prisma Client with full TypeScript types.
Section 3: MessageBird SMS Service Implementation
Step 1: Create the MessageBird Service
Create
lib/messagebird.ts:import messagebird from 'messagebird'; // Initialize MessageBird client const client = messagebird(process.env.MESSAGEBIRD_API_KEY!); export interface SendSMSParams { to: string; body: string; scheduledDatetime?: Date; reference?: string; } export interface SMSResponse { id: string; recipients: { recipient: string; status: string; }[]; } /** * Send SMS via MessageBird API * @param params - SMS parameters * @returns Promise with MessageBird response */ export async function sendSMS(params: SendSMSParams): Promise<SMSResponse> { return new Promise((resolve, reject) => { const messageParams: any = { originator: process.env.MESSAGEBIRD_ORIGINATOR!, recipients: [params.to], body: params.body, }; // Add scheduled datetime in RFC3339 format if provided if (params.scheduledDatetime) { messageParams.scheduledDatetime = params.scheduledDatetime.toISOString(); } // Add reference for webhook tracking if (params.reference) { messageParams.reference = params.reference; } client.messages.create(messageParams, (err, response) => { if (err) { reject(new Error(`MessageBird API Error: ${err.errors?.[0]?.description || err.message}`)); } else { resolve({ id: response.id, recipients: response.recipients.items.map((item: any) => ({ recipient: item.recipient.toString(), status: item.status, })), }); } }); }); } /** * Send scheduled reminder SMS * @param phone - E.164 formatted phone number * @param name - Customer name * @param appointmentDate - Appointment datetime * @param appointmentId - Database appointment ID for reference * @returns Promise with MessageBird message ID */ export async function sendAppointmentReminder( phone: string, name: string, appointmentDate: Date, appointmentId: string ): Promise<string> { const formattedDate = appointmentDate.toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'America/New_York', // Adjust based on customer timezone }); const message = `Hi ${name}, this is a reminder about your appointment on ${formattedDate}. Reply CANCEL to cancel.`; // Calculate reminder time (24 hours before appointment) const reminderTime = new Date(appointmentDate.getTime() - 24 * 60 * 60 * 1000); const response = await sendSMS({ to: phone, body: message, scheduledDatetime: reminderTime, reference: appointmentId, }); return response.id; }Key implementation details:
1. Scheduled Message Format: MessageBird requires RFC3339 format for
scheduledDatetime:// Correct: 2024-01-15T10:30:00Z (JavaScript .toISOString()) // Incorrect: 2024-01-15 10:30:002. Rate Limiting: MessageBird enforces rate limits: 500 requests per second for POST operations (message creation) and 50 req/s for GET operations. Implement exponential backoff for production systems.
3. Phone Number Format: Always use E.164 format:
+[country code][number]// Correct: +14155552671 // Incorrect: (415) 555-2671, 415-555-26714. Recipient Limits: Maximum 50 recipients per request. For bulk sending, batch your requests.
Step 2: Implement Retry Logic with Exponential Backoff
Add retry functionality for failed API calls in
lib/messagebird.ts:/** * Retry wrapper with exponential backoff * @param fn - Async function to retry * @param maxRetries - Maximum retry attempts (default: 3) * @param initialDelay - Initial delay in ms (default: 1000) * @returns Promise with function result */ export async function withRetry<T>( fn: () => Promise<T>, maxRetries: number = 3, initialDelay: number = 1000 ): Promise<T> { let lastError: Error; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; // Don't retry on client errors (4xx) - only network/server errors if (error instanceof Error && error.message.includes('4')) { throw error; } if (attempt < maxRetries - 1) { const delay = initialDelay * Math.pow(2, attempt); console.log(`Retry attempt ${attempt + 1} after ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError!; } /** * Send SMS with automatic retry */ export async function sendSMSWithRetry(params: SendSMSParams): Promise<SMSResponse> { return withRetry(() => sendSMS(params)); }Retry strategy:
Section 4: Appointment API Routes
Step 1: Create Appointment Booking Endpoint
Create
app/api/appointments/route.ts:import { NextRequest, NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; import { sendAppointmentReminder } from '@/lib/messagebird'; const prisma = new PrismaClient(); export async function POST(req: NextRequest) { try { const body = await req.json(); const { customerName, customerPhone, appointmentDate, serviceType } = body; // Validation if (!customerName || customerName.length < 2) { return NextResponse.json( { error: 'Customer name must be at least 2 characters' }, { status: 400 } ); } // Validate E.164 phone format const phoneRegex = /^\+[1-9]\d{1,14}$/; if (!phoneRegex.test(customerPhone)) { return NextResponse.json( { error: 'Phone number must be in E.164 format (+1234567890)' }, { status: 400 } ); } // Parse and validate appointment date const apptDate = new Date(appointmentDate); if (isNaN(apptDate.getTime())) { return NextResponse.json( { error: 'Invalid appointment date format. Use ISO 8601.' }, { status: 400 } ); } // Ensure appointment is at least 1 hour in the future const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); if (apptDate < oneHourFromNow) { return NextResponse.json( { error: 'Appointment must be at least 1 hour in the future' }, { status: 400 } ); } // Create appointment in database const appointment = await prisma.appointment.create({ data: { customerName, customerPhone, appointmentDate: apptDate, serviceType: serviceType || null, }, }); // Schedule SMS reminder via MessageBird try { const messageBirdId = await sendAppointmentReminder( customerPhone, customerName, apptDate, appointment.id ); // Update appointment with MessageBird message ID await prisma.appointment.update({ where: { id: appointment.id }, data: { messageBirdId, reminderStatus: 'SENT' }, }); return NextResponse.json( { success: true, appointment: { ...appointment, messageBirdId }, message: 'Appointment created and reminder scheduled' }, { status: 201 } ); } catch (smsError) { // Appointment created but SMS failed console.error('SMS scheduling failed:', smsError); return NextResponse.json( { success: true, appointment, warning: 'Appointment created but reminder scheduling failed' }, { status: 201 } ); } } catch (error) { console.error('Appointment creation error:', error); return NextResponse.json( { error: 'Failed to create appointment' }, { status: 500 } ); } } export async function GET(req: NextRequest) { try { const appointments = await prisma.appointment.findMany({ orderBy: { appointmentDate: 'asc' }, where: { appointmentDate: { gte: new Date(), // Only future appointments }, }, }); return NextResponse.json({ appointments }); } catch (error) { console.error('Error fetching appointments:', error); return NextResponse.json( { error: 'Failed to fetch appointments' }, { status: 500 } ); } }Validation rules:
+and contain 10–15 digitsResponse codes:
201: Appointment created successfully400: Validation error or invalid input500: Server error (database or MessageBird API failure)Step 2: Implement Appointment Cancellation
Create
app/api/appointments/[id]/route.ts:import { NextRequest, NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); export async function DELETE( req: NextRequest, { params }: { params: { id: string } } ) { try { const { id } = params; // Mark appointment as canceled (MessageBird doesn't support API cancellation) const appointment = await prisma.appointment.update({ where: { id }, data: { reminderStatus: 'CANCELED' }, }); return NextResponse.json({ success: true, message: 'Appointment canceled. Reminder will not be sent.', appointment, }); } catch (error) { console.error('Cancellation error:', error); return NextResponse.json( { error: 'Failed to cancel appointment' }, { status: 500 } ); } } export async function GET( req: NextRequest, { params }: { params: { id: string } } ) { try { const { id } = params; const appointment = await prisma.appointment.findUnique({ where: { id }, }); if (!appointment) { return NextResponse.json( { error: 'Appointment not found' }, { status: 404 } ); } return NextResponse.json({ appointment }); } catch (error) { console.error('Error fetching appointment:', error); return NextResponse.json( { error: 'Failed to fetch appointment' }, { status: 500 } ); } }Note: MessageBird doesn't support canceling scheduled messages via API. Mark appointments as canceled in your database and filter them when processing reminders.
Section 5: Scheduled Job for Sending Reminders
Step 1: Create Reminder Scheduler
Create
lib/scheduler.ts:import { PrismaClient } from '@prisma/client'; import { sendAppointmentReminder } from './messagebird'; const prisma = new PrismaClient(); export async function sendScheduledReminders() { const now = new Date(); const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); // Find appointments 24 hours from now that need reminders const appointments = await prisma.appointment.findMany({ where: { appointmentDate: { gte: tomorrow, lte: new Date(tomorrow.getTime() + 60 * 60 * 1000), // 1 hour window }, reminderStatus: 'PENDING', }, }); console.log(`Found ${appointments.length} appointments needing reminders`); const results = { success: 0, failed: 0, errors: [] as string[], }; for (const appointment of appointments) { try { const messageBirdId = await sendAppointmentReminder( appointment.customerPhone, appointment.customerName, appointment.appointmentDate, appointment.id ); await prisma.appointment.update({ where: { id: appointment.id }, data: { messageBirdId, reminderStatus: 'SENT', }, }); results.success++; console.log(`✓ Reminder sent for appointment ${appointment.id}`); } catch (error) { results.failed++; const errorMsg = `Failed to send reminder for ${appointment.id}: ${error}`; results.errors.push(errorMsg); console.error('✗', errorMsg); await prisma.appointment.update({ where: { id: appointment.id }, data: { reminderStatus: 'FAILED' }, }); } } return results; }Scheduler logic:
Step 2: Set Up Cron Job
Option A: Vercel Cron (Production)
Create
app/api/cron/send-reminders/route.ts:import { NextRequest, NextResponse } from 'next/server'; import { sendScheduledReminders } from '@/lib/scheduler'; export async function GET(req: NextRequest) { // Verify Vercel Cron secret const authHeader = req.headers.get('authorization'); if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { const result = await sendScheduledReminders(); return NextResponse.json(result); } catch (error) { console.error('Cron job error:', error); return NextResponse.json( { error: 'Cron job failed' }, { status: 500 } ); } }Vercel Cron Limitations:
CRON_SECRETenvironment variable for authenticationAdd to
vercel.json:{ "crons": [{ "path": "/api/cron/send-reminders", "schedule": "0 * * * *" }] }Option B: Node-Cron (Development)
Install node-cron:
Create
lib/cron.ts:import cron from 'node-cron'; import { sendScheduledReminders } from './scheduler'; // Run every hour export function startCronJobs() { cron.schedule('0 * * * *', async () => { console.log('Running scheduled reminder job...'); await sendScheduledReminders(); }); console.log('Cron jobs started'); }Why node-cron doesn't work on Vercel: Vercel uses serverless functions that spin up on-demand and shut down after handling requests. Node-cron requires a persistent Node.js process to run scheduled tasks, which is incompatible with Vercel's stateless, ephemeral execution model. For production on Vercel, always use Vercel Cron Jobs configured via
vercel.json.Add to
app/layout.tsx:import { startCronJobs } from '@/lib/cron'; // Only run in development if (process.env.NODE_ENV === 'development') { startCronJobs(); }Section 6: Webhook Integration for Delivery Status
Create Webhook Handler
Create
app/api/webhooks/messagebird/route.ts:import { NextRequest, NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; import crypto from 'crypto'; const prisma = new PrismaClient(); /** * Verify webhook signature (if configured in MessageBird dashboard) */ function verifyWebhookSignature( payload: string, signature: string, secret: string ): boolean { const hmac = crypto.createHmac('sha256', secret); const digest = hmac.update(payload).digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(digest) ); } export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); // MessageBird sends status via query parameters const messageBirdId = searchParams.get('id'); const status = searchParams.get('status'); // delivered, buffered, sent, expired, delivery_failed const reference = searchParams.get('reference'); // appointment ID const statusReason = searchParams.get('statusReason'); if (!messageBirdId) { return NextResponse.json({ error: 'Missing message ID' }, { status: 400 }); } console.log(`Webhook received: ${messageBirdId} - ${status} (${statusReason})`); try { // Map MessageBird status to our ReminderStatus enum let reminderStatus: 'SENT' | 'DELIVERED' | 'FAILED' = 'SENT'; if (status === 'delivered') { reminderStatus = 'DELIVERED'; } else if (status === 'delivery_failed' || status === 'expired') { reminderStatus = 'FAILED'; } else if (status === 'sent' || status === 'buffered') { reminderStatus = 'SENT'; } // Update appointment status await prisma.appointment.updateMany({ where: { messageBirdId }, data: { reminderStatus }, }); return NextResponse.json({ success: true, message: `Status updated to ${reminderStatus}` }); } catch (error) { console.error('Webhook processing error:', error); return NextResponse.json( { error: 'Failed to process webhook' }, { status: 500 } ); } }MessageBird SMS Status Values: According to MessageBird's API documentation, the possible status values are:
statusReasonfor details)Common Status Reasons:
successfully delivered: Message delivered successfullypending DLR: Awaiting delivery report from carrierunknown subscriber: Invalid/non-existent phone number (permanent failure)unavailable subscriber: Recipient temporarily unavailable (temporary failure)carrier rejected: Carrier blocked message (check sender ID registration)capacity limit reached: Carrier rate limit exceeded (e.g., USA 10DLC throttling)Configure webhook URL in MessageBird dashboard:
If signature verification fails, MessageBird will retry delivery up to 10 times with exponential backoff.
Section 7: Testing Your Application
1. Test MessageBird Integration
MessageBird provides a TEST API key that simulates message sending without delivering real SMS messages:
# Add to .env.local for testing MESSAGEBIRD_API_KEY=test_your_test_key_hereTest Mode Behavior:
To validate your integration, verify the API response structure matches production and that your database records are created correctly.
2. Verify Database Operations
Check your appointments table using Prisma Studio:
This opens a web interface at
http://localhost:5555where you inspect database records, verify scheduled messages, and check reminder status.3. Integration Testing
Install Jest and testing dependencies:
Create
__tests__/appointments.test.ts:import { POST } from '@/app/api/appointments/route'; describe('Appointment API', () => { it('creates appointment with valid data', async () => { const mockRequest = { json: async () => ({ customerName: 'John Doe', customerPhone: '+14155552671', appointmentDate: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(), serviceType: 'Haircut' }) } as any; const response = await POST(mockRequest); expect(response.status).toBe(201); }); it('rejects invalid phone format', async () => { const mockRequest = { json: async () => ({ customerName: 'John Doe', customerPhone: '415-555-2671', // Invalid format appointmentDate: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(), }) } as any; const response = await POST(mockRequest); expect(response.status).toBe(400); }); it('rejects appointments less than 1 hour away', async () => { const mockRequest = { json: async () => ({ customerName: 'John Doe', customerPhone: '+14155552671', appointmentDate: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30 minutes }) } as any; const response = await POST(mockRequest); expect(response.status).toBe(400); }); });Run tests:
npm test4. Test Webhook Delivery
Use ngrok to expose your local server for webhook testing:
Copy the ngrok URL and configure it in MessageBird dashboard under Webhooks.
Section 8: Deployment Considerations
1. Environment Variables
Set these variables in your production environment:
Generate secure secrets:
openssl rand -base64 322. Database Connection Pooling
Update
lib/db.tsfor production:import { PrismaClient } from '@prisma/client'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; export const prisma = globalForPrisma.prisma || new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;For serverless deployments (Vercel): Configure connection pooling in
schema.prisma:Connection Pool Sizing:
connection_limit=10in your DATABASE_URL for Vercel3. Hosting Platform Options
Vercel (Recommended):
vercel.jsonRailway:
AWS Amplify:
4. Monitoring and Logging
Implement production logging with Winston:
Create
lib/logger.ts:import winston from 'winston'; export const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], }); if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), })); }Cloud Logging Integration Examples:
For DataDog:
For AWS CloudWatch:
Replace all
console.logcalls withlogger.info,logger.error, etc.Monitor key metrics:
5. Security Best Practices
Webhook Signature Verification: MessageBird signs webhook payloads. Verify signatures to prevent spoofing:
import crypto from 'crypto'; function verifyWebhookSignature( payload: string, signature: string, secret: string ): boolean { const hmac = crypto.createHmac('sha256', secret); const digest = hmac.update(payload).digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(digest) ); } export async function POST(req: NextRequest) { const signature = req.headers.get('messagebird-signature'); const body = await req.text(); if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET!)) { return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }); } // Process webhook... }If signature verification fails:
WEBHOOK_SECRETmatches the value in MessageBird dashboardRate Limiting: Implement rate limiting on public API endpoints:
import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute }); export async function POST(req: NextRequest) { const ip = req.ip ?? '127.0.0.1'; const { success } = await ratelimit.limit(ip); if (!success) { return NextResponse.json( { error: 'Too many requests' }, { status: 429 } ); } // Process request... }Input Sanitization: Validate and sanitize all user inputs:
import validator from 'validator'; function sanitizeInput(input: string): string { return validator.escape(validator.trim(input)); }Section 9: Extending the Application
1. Message Cancellation
Implement cancellation for scheduled messages:
// lib/messagebird.ts export async function cancelScheduledMessage(messageId: string): Promise<boolean> { try { // MessageBird doesn't support API-based cancellation // Mark as canceled in database and filter during sending await prisma.appointment.update({ where: { messageBirdId: messageId }, data: { reminderStatus: 'CANCELED' } }); return true; } catch (error) { logger.error('Cancel error:', error); return false; } }2. Multiple Reminders
Send reminders at multiple intervals (7 days, 1 day, 1 hour):
enum ReminderType { WEEK_BEFORE = 'WEEK_BEFORE', DAY_BEFORE = 'DAY_BEFORE', HOUR_BEFORE = 'HOUR_BEFORE' } // Add to schema.prisma model Reminder { id String @id @default(cuid()) appointmentId String appointment Appointment @relation(fields: [appointmentId], references: [id]) reminderType ReminderType sentAt DateTime? status ReminderStatus @default(PENDING) messageBirdId String? }Update scheduler to handle multiple reminder types:
export async function sendScheduledReminders() { const intervals = [ { type: 'WEEK_BEFORE', hours: 7 * 24 }, { type: 'DAY_BEFORE', hours: 24 }, { type: 'HOUR_BEFORE', hours: 1 } ]; for (const interval of intervals) { const targetTime = new Date(Date.now() + interval.hours * 60 * 60 * 1000); const endWindow = new Date(targetTime.getTime() + 60 * 60 * 1000); const appointments = await prisma.appointment.findMany({ where: { appointmentDate: { gte: targetTime, lte: endWindow, }, }, include: { reminders: true, }, }); for (const appointment of appointments) { // Check if this reminder type already sent const existingReminder = appointment.reminders.find( r => r.reminderType === interval.type && r.status === 'SENT' ); if (!existingReminder) { // Send reminder and create record const messageBirdId = await sendAppointmentReminder( appointment.customerPhone, appointment.customerName, appointment.appointmentDate, appointment.id ); await prisma.reminder.create({ data: { appointmentId: appointment.id, reminderType: interval.type, status: 'SENT', messageBirdId, sentAt: new Date(), }, }); } } } }3. Customer Preferences
Add customer preferences for reminder timing and channels:
Query customer preferences when scheduling reminders and adjust timing accordingly.
Best Practices and Production Tips
1. Phone Number Validation: Use MessageBird Lookup API to verify numbers before sending:
export async function validatePhoneNumber(phone: string): Promise<boolean> { return new Promise((resolve, reject) => { messagebird.lookup.read(phone, (err, response) => { if (err) { resolve(false); // Invalid number } else { resolve(response.type === 'mobile'); // Only mobile numbers } }); }); }MessageBird Lookup API Costs:
2. Message Templates: Store message templates in database for easy updates:
const templates = { reminder: (name: string, date: string) => `Hi ${name}, this is a reminder about your appointment on ${date}. Reply CANCEL to cancel.`, confirmation: (name: string) => `Thanks ${name}! Your appointment is confirmed. You'll receive a reminder 24 hours before.` };SMS Length Limits:
datacoding: 'auto'to automatically select optimal encoding3. Timezone Handling: Always store dates in UTC and convert to customer timezone for display:
import moment from 'moment-timezone'; function formatAppointmentTime(date: Date, timezone: string): string { return moment(date).tz(timezone).format('MMM DD, YYYY [at] h:mm A z'); }Note: Consider migrating from Moment.js (maintenance mode) to Day.js or date-fns for new projects.
4. Cost Optimization:
Example Cost Calculation:
5. Error Handling: Implement comprehensive error tracking:
class MessageBirdError extends Error { constructor( message: string, public code: string, public statusCode: number ) { super(message); this.name = 'MessageBirdError'; } } // Log errors to monitoring service function handleError(error: MessageBirdError) { logger.error('MessageBird API Error', { code: error.code, status: error.statusCode, message: error.message, timestamp: new Date().toISOString() }); // Send to error tracking service (Sentry, Rollbar, etc.) if (process.env.SENTRY_DSN) { Sentry.captureException(error); } }Frequently Asked Questions (FAQ)
How do I get a MessageBird API key for sending SMS?
Sign up for a free MessageBird account at messagebird.com, navigate to Developers → API Keys in your dashboard, and copy your Live API key (starts with
live_). For development and testing, use your Test API key (starts withtest_) to simulate message sending without charges or actual SMS delivery.What phone number format does MessageBird require?
MessageBird requires E.164 international format:
+[country code][number]with no spaces or special characters. Examples:+14155552671(US),+447700900123(UK). Remove parentheses, hyphens, and spaces before sending. Invalid formats like(415) 555-2671or415-555-2671will cause API errors.Can I schedule SMS messages more than 24 hours in advance with MessageBird?
Yes, MessageBird supports scheduling SMS messages up to 30 days in advance. Use the
scheduledDatetimeparameter with RFC3339 format (e.g.,2024-01-15T10:30:00Z) when creating messages. Schedule your appointment reminders days or weeks ahead to ensure timely delivery without manual intervention.How do I handle failed SMS deliveries in my appointment system?
Implement webhook integration to receive real-time delivery status updates from MessageBird. When a message fails, your webhook endpoint updates the database with the failure status. Add retry logic with exponential backoff (1s, 2s, 4s delays) for network errors, and consider fallback channels like email for persistent failures.
What are MessageBird's rate limits for SMS sending?
MessageBird enforces 500 requests per second for POST operations (message creation) and 50 requests per second for GET operations (status checks). For bulk sending, batch your requests and implement exponential backoff retry logic. Monitor your API usage in the MessageBird dashboard to avoid rate limit errors.
How much does it cost to send SMS with MessageBird?
MessageBird pricing varies by destination country, typically ranging from $0.01 to $0.15 per SMS. US SMS costs approximately $0.0075 per message, while international rates vary. Check current pricing at messagebird.com/pricing. Use the Test API key during development to avoid charges while building your application.
Can I cancel a scheduled SMS message in MessageBird?
MessageBird doesn't support API-based cancellation of scheduled messages. Instead, mark appointments as canceled in your database and filter them when your cron job runs. Your scheduler should check the
reminderStatusfield and skip sending messages for canceled appointments before calling the MessageBird API.How do I verify phone numbers before sending SMS?
Use MessageBird's Lookup API to verify phone numbers before sending messages. The Lookup API returns number validity, carrier information, and number type (mobile vs. landline). This reduces failed deliveries, lowers costs, and improves your sender reputation by avoiding invalid destinations.
What's the best way to handle timezones for international appointments?
Store all appointment dates in UTC format in your database. When sending reminders, convert UTC timestamps to the customer's local timezone using moment-timezone or date-fns-tz. Include timezone information in reminder messages (e.g., "Your appointment is Jan 15, 2024 at 2:00 PM EST") to prevent confusion.
How do I deploy a Next.js MessageBird application to production?
Deploy to Vercel (recommended for Next.js), Railway, or AWS Amplify. Configure production environment variables (MessageBird Live API key, database URL, webhook secrets), set up Vercel Cron for scheduled jobs, enable database connection pooling for serverless environments, and implement webhook signature verification for security.
What are the common MessageBird error codes and how do I handle them?
Common errors:
statusErrorCodein delivery reports and taking appropriate actionRelated Resources: SMS APIs and Appointment Systems
MessageBird Integration Guides:
Next.js Development:
Appointment & Scheduling Systems:
Conclusion: Your Production-Ready Appointment System
You've built a complete SMS appointment reminder system with MessageBird API, Next.js 14, and Prisma ORM. Your application now handles scheduled SMS delivery, database management, webhook processing, and production-grade error handling with comprehensive monitoring and security features.
What You Accomplished: ✅ MessageBird SMS API integration with scheduled messaging and RFC3339 format ✅ Next.js 14 App Router with TypeScript API routes and server components ✅ Prisma ORM with PostgreSQL database and optimized connection pooling ✅ Automated cron jobs for reminder scheduling (Vercel Cron and node-cron) ✅ Webhook integration for real-time delivery status tracking ✅ Production-ready error handling with exponential backoff retry logic ✅ Timezone support for international customers using moment-timezone ✅ Security features including webhook signature verification and rate limiting
Next Steps for Production:
Production Deployment Checklist:
openssl rand -base64 32)Additional Resources:
Your appointment reminder system is now ready to scale and serve thousands of customers with reliable, automated SMS notifications. Start testing with MessageBird's test API key, then deploy to production when you're ready to send real messages to your customers.