code examples
code examples
Build an SMS Marketing Campaign App with Next.js and MessageBird: Complete Tutorial
Learn how to build an SMS marketing campaign application using Next.js 15, MessageBird Node.js SDK, and Prisma. Complete guide with code examples for SMS subscriptions, broadcasts, and webhook handling.
Build an SMS Marketing Campaign App with Next.js and MessageBird
Build an SMS marketing campaign application using Next.js 15 and the MessageBird Node.js SDK. This comprehensive tutorial shows you how to handle SMS subscriptions (opt-in/opt-out) via keywords, enable broadcast messaging to subscribers, and integrate MessageBird webhooks with Next.js App Router. You'll use Prisma ORM for database management and learn proper error handling for production-ready SMS applications.
What You'll Build:
- Users text
SUBSCRIBEto your virtual mobile number (VMN) to opt in to SMS marketing messages - Subscribers receive SMS confirmation upon successful subscription
- Subscribers text
STOPto the same VMN to opt out - Opt-outs receive SMS confirmation
- An administrator uses a web interface to send custom SMS messages to all subscribed users
Prerequisites:
Before starting, ensure you have:
- Node.js (LTS version recommended) and npm or yarn installed
- A MessageBird account
- A text editor or IDE (e.g., VS Code)
- Terminal or command prompt access
- (Optional but Recommended) Git for version control
- (Optional but Recommended) Docker for running a local PostgreSQL database
Target Audience: Developers familiar with JavaScript, Node.js, and Next.js basics who want to integrate SMS functionality using MessageBird.
Technologies:
- Next.js: React framework for server-side rendered and static web applications – provides robust routing via App Router, excellent developer experience, and strong performance
- MessageBird SMS API: Send and receive SMS messages programmatically – reliable, global reach, developer-friendly API/SDK
- Prisma: Modern Node.js and TypeScript ORM for database access – type-safe with auto-completion and migration capabilities
- PostgreSQL (or SQLite/MySQL): Store subscriber information (this guide uses PostgreSQL, but Prisma supports others)
- Localtunnel or ngrok: Expose your local development server to the internet to receive MessageBird webhooks
System Architecture:
graph LR
User -- SMS (SUBSCRIBE/STOP) --> MessageBirdVMN[MessageBird VMN]
MessageBirdVMN -- Webhook --> NextAppWebhook[/api/messagebird/webhook]
NextAppWebhook -- Read/Write --> DB[(Database)]
NextAppWebhook -- Send Confirmation SMS --> MessageBirdAPI[MessageBird API]
MessageBirdAPI -- SMS --> User
Admin -- HTTP Request (Send Message) --> NextAppUI[Next.js UI (Admin Form)]
NextAppUI -- API Call --> NextAppSendAPI[/api/messagebird/send]
NextAppSendAPI -- Read Subscribers --> DB
NextAppSendAPI -- Send Broadcast SMS --> MessageBirdAPI
MessageBirdAPI -- SMS --> UserFinal Outcome: A functional Next.js application that manages SMS subscriptions and broadcasts messages via MessageBird – a strong foundation for a production system once you implement security features.
1. Set Up the Project
Initialize your Next.js project, install dependencies, set up the database connection with Prisma, and configure environment variables.
1.1 Initialize Next.js Project
Create a new Next.js application. Use the App Router when prompted (recommended).
npx create-next-app@latest messagebird-sms-campaigns
cd messagebird-sms-campaigns1.2 Install Dependencies
Install MessageBird SDK and Prisma Client.
npm install messagebird prisma @prisma/client
npm install --save-dev prismamessagebird: Official MessageBird Node.js SDK (v4.0.1 as of 2024). Note: This package hasn't received updates since 2021–2022 but remains functional for core MessageBird API operations.prisma: Prisma CLI (dev dependency) and Prisma Client@prisma/client: Prisma Client for database operations
Note: Next.js automatically loads variables from .env files – you don't need the dotenv package.
1.3 Set Up Prisma
Initialize Prisma in your project. This creates a prisma directory with a schema.prisma file and updates your .gitignore.
npx prisma init --datasource-provider postgresql(If you prefer SQLite or MySQL, replace postgresql accordingly.)
1.4 Configure Database Connection
Open the .env file (create it if it doesn't exist). Add your DATABASE_URL variable.
-
Using Docker for Local PostgreSQL:
Spin up a PostgreSQL container:
bashdocker run --name messagebird-db -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgresYour
.envfile should contain:dotenv# .env DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres?schema=public" # Add MessageBird variables later MESSAGEBIRD_API_KEY= MESSAGEBIRD_ORIGINATOR= -
Using Other Database Providers (e.g., Supabase, Neon, Railway): Obtain the connection string from your provider and add it here.
1.5 Define Prisma Schema:
Open prisma/schema.prisma and define the model for storing subscriber information.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // Or your chosen provider
url = env("DATABASE_URL")
}
model Subscriber {
id String @id @default(cuid())
phoneNumber String @unique // E.164 format recommended (e.g., +1xxxxxxxxxx)
subscribed Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([subscribed]) // Add index for filtering by subscription status
}phoneNumber: Stores the subscriber's phone number (ensure uniqueness). Storing in E.164 format is recommended for consistency.subscribed: A boolean flag indicating the current subscription status. An index is added for efficient querying.createdAt,updatedAt: Timestamps managed automatically by Prisma.
1.6 Apply Database Migrations:
Run the Prisma migrate command to create the Subscriber table in your database based on the schema. Prisma will prompt you to create a name for the migration (e.g., init).
npx prisma migrate dev --name initThis command:
- Creates an SQL migration file in
prisma/migrations. - Applies the migration to your database, creating the
Subscribertable and indexes. - Generates the Prisma Client based on your schema.
Your basic project structure and database setup are now complete.
2. Implementing Core Functionality: Receiving SMS
This section focuses on handling incoming SMS messages (SUBSCRIBE/STOP) via a Next.js API route acting as a webhook for MessageBird.
2.1 Create Prisma Client Utility:
To avoid creating multiple Prisma Client instances (which is inefficient), set up a singleton instance.
Create lib/prisma.ts:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
// Prevent multiple instances of Prisma Client in development
const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV === 'development') global.prisma = prisma;
export default prisma;2.2 Create MessageBird Client Utility:
Similarly, initialize the MessageBird client once.
Create lib/messagebird.ts:
// lib/messagebird.ts
import messagebird from 'messagebird';
// Next.js automatically loads environment variables from .env files
const apiKey = process.env.MESSAGEBIRD_API_KEY;
if (!apiKey) {
// Throw error during initialization if key is missing
throw new Error("MESSAGEBIRD_API_KEY environment variable not set.");
}
const messagebirdClient = messagebird(apiKey);
export default messagebirdClient;Make sure to add MESSAGEBIRD_API_KEY to your .env file later (see Section 4).
2.3 Create the Webhook API Route:
This API route will receive POST requests from MessageBird whenever an SMS is sent to your virtual number.
Create app/api/messagebird/webhook/route.ts (using App Router structure):
// app/api/messagebird/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma'; // Adjust path based on your structure
import messagebirdClient from '@/lib/messagebird'; // Adjust path based on your structure
// Define types for MessageBird SDK responses
interface MessageBirdResponse {
id: string;
// Add other relevant fields based on MessageBird's actual webhook response structure if needed
}
interface MessageBirdError {
errors: Array<{
code: number;
description: string;
parameter: string | null;
}>;
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const originator = body.originator; // Sender's phone number
const payload = body.payload?.trim().toUpperCase(); // Message content
if (!originator || !payload) {
console.warn('Webhook received invalid data:', body);
return NextResponse.json({ message: 'Missing originator or payload' }, { status: 400 });
}
console.log(`Webhook received: From ${originator}, Payload: ${payload}`);
// Ensure E.164 format for storage consistency. Check if '+' is missing.
// Adjust this logic based on the actual format MessageBird sends `originator`.
// Storing consistently (e.g., with '+') is recommended.
const originatorFormatted = originator.startsWith('+') ? originator : `+${originator}`;
let confirmationMessage = '';
let subscriber = await prisma.subscriber.findUnique({
where: { phoneNumber: originatorFormatted },
});
if (payload === 'SUBSCRIBE') {
if (!subscriber) {
// New subscriber
subscriber = await prisma.subscriber.create({
data: { phoneNumber: originatorFormatted, subscribed: true },
});
console.log(`New subscriber added: ${originatorFormatted}`);
confirmationMessage = 'Thanks for subscribing! Text STOP anytime to opt-out.';
} else if (!subscriber.subscribed) {
// Re-subscribing
subscriber = await prisma.subscriber.update({
where: { phoneNumber: originatorFormatted },
data: { subscribed: true },
});
console.log(`Subscriber re-subscribed: ${originatorFormatted}`);
confirmationMessage = 'Welcome back! You are now subscribed again.';
} else {
// Already subscribed
console.log(`Subscriber already subscribed: ${originatorFormatted}`);
confirmationMessage = 'You are already subscribed.';
}
} else if (payload === 'STOP') {
if (subscriber && subscriber.subscribed) {
// Opting out
subscriber = await prisma.subscriber.update({
where: { phoneNumber: originatorFormatted },
data: { subscribed: false },
});
console.log(`Subscriber opted-out: ${originatorFormatted}`);
confirmationMessage = 'You have successfully unsubscribed. Text SUBSCRIBE to join again.';
} else {
// Not subscribed or already opted out
console.log(`Received STOP from non-subscriber or already opted-out: ${originatorFormatted}`);
// Optionally send a message, or do nothing
// confirmationMessage = 'You were not subscribed to this list.';
}
} else {
// Handle unrecognized commands (optional)
console.log(`Received unrecognized command from ${originatorFormatted}: ${payload}`);
// confirmationMessage = 'Unrecognized command. Text SUBSCRIBE to join or STOP to leave.';
}
// Send confirmation SMS if needed
if (confirmationMessage && process.env.MESSAGEBIRD_ORIGINATOR) {
try {
await new Promise<void>((resolve, reject) => {
messagebirdClient.messages.create({
originator: process.env.MESSAGEBIRD_ORIGINATOR!,
// Use the original number format received from webhook for the recipient field here,
// as MessageBird expects it for replying.
recipients: [originator],
body: confirmationMessage,
}, (err: MessageBirdError | null, response: MessageBirdResponse | any) => { // Use defined types
if (err) {
console.error("Error sending confirmation SMS:", err);
// Log error but don't fail the webhook response if SMS fails
// Consider adding more robust error handling/retries if confirmation is critical
return reject(err); // Reject promise on error
}
console.log("Confirmation SMS sent:", response?.id);
resolve(); // Resolve promise on success
});
});
} catch (smsError) {
// Log error from the promise rejection or other issues
console.error('Failed to send confirmation SMS:', smsError);
}
}
// Respond to MessageBird promptly to acknowledge receipt
return NextResponse.json({ message: 'Webhook processed successfully' }, { status: 200 });
} catch (error) {
console.error('Webhook processing error:', error);
// Return a 500 error but still acknowledge receipt if possible
// Avoid sending detailed errors back which might expose info
return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
}
}
// Basic GET handler for testing reachability (optional)
export async function GET() {
return NextResponse.json({ message: 'Webhook endpoint is active. Use POST for events.' });
}Explanation:
- Import Dependencies: Import
NextRequest,NextResponse,prisma,messagebird, and types. - POST Handler: Defines an
asyncfunction to handle POST requests. - Parse Request: Reads the JSON body sent by MessageBird (
originator,payload). - Input Validation: Basic check if essential fields are present.
- Logging: Logs incoming requests for debugging.
- Format Number: Ensures the phone number (
originatorFormatted) is consistently formatted (e.g., E.164+1xxxxxxxxxx) for database storage. Adjust based on observed MessageBird format if needed. - Database Lookup: Checks if the formatted
originatorFormattedexists in theSubscribertable. - Command Logic (
SUBSCRIBE/STOP): Handles new subscriptions, re-subscriptions, and opt-outs by creating or updating the database record. Sets appropriateconfirmationMessagetext. - Send Confirmation SMS: If a message is set and
MESSAGEBIRD_ORIGINATORis configured, it usesmessagebird.messages.createto send an SMS back. Note the use ofnew Promiseto handle the SDK's callback within anasyncfunction. Uses the originaloriginatorformat for the recipient field when replying. - Error Handling: Includes
try...catchblocks for overall processing and specifically for SMS sending. Logs errors. - Acknowledge Receipt: Returns a
200 OKJSON response to MessageBird. This is crucial for preventing retries.
3. Implementing Core Functionality: Sending Broadcasts
This section covers creating a simple admin interface to send messages and the corresponding API route to handle the broadcast logic.
3.1 Create the Admin UI:
Create a simple page with a form for the administrator to type and send messages.
Create app/admin/page.tsx (App Router):
// app/admin/page.tsx
'use client'; // This component needs client-side interactivity
import { useState } from 'react';
export default function AdminPage() {
const [message, setMessage] = useState('');
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!message.trim()) {
setStatus('Please enter a message.');
return;
}
setLoading(true);
setStatus('Sending...');
try {
const response = await fetch('/api/messagebird/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
});
const result = await response.json();
if (response.ok) {
setStatus(`Message sent successfully to ${result.sentCount} subscribers.`);
setMessage(''); // Clear textarea on success
} else if (response.status === 207) { // Handle partial success
setStatus(`Message sent, but some batches failed. Estimated sent count: ${result.sentCount}. Check server logs for details.`);
} else {
setStatus(`Error: ${result.message || 'Failed to send message'}`);
}
} catch (error) {
console.error('Send message error:', error);
setStatus('An unexpected error occurred. Check the console or server logs.');
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: 'auto' }}>
<h1>Send Broadcast SMS</h1>
<form onSubmit={handleSubmit}>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Enter your message here..."
rows={5}
style={{ width: '100%', marginBottom: '10px', padding: '8px', display: 'block' }}
disabled={loading}
/>
<button type="submit" disabled={loading} style={{ padding: '10px 15px' }}>
{loading ? 'Sending...' : 'Send to All Subscribers'}
</button>
</form>
{status && <p style={{ marginTop: '15px' }}>{status}</p>}
{/* Basic Security Warning */}
<p style={{ marginTop: '30px', color: 'red', border: '1px solid red', padding: '10px' }}>
<strong>Warning:</strong> This admin page is currently unsecured. Anyone with the URL can send messages. Implement proper authentication in a production environment (see Section 7).
</p>
</div>
);
}Explanation:
- Uses React state (
useState) for message input, loading state, and status feedback. handleSubmitsends a POST request to/api/messagebird/send.- Provides user feedback, including handling partial success (HTTP 207).
- Includes a prominent warning about the lack of security.
3.2 Create the Send API Route:
This route fetches active subscribers and sends the message using the MessageBird SDK, handling batching.
Create app/api/messagebird/send/route.ts:
// app/api/messagebird/send/route.ts
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import messagebirdClient from '@/lib/messagebird';
// Define types for better error/response handling (optional but good practice)
interface MessageBirdResponse {
id: string;
recipients: {
totalSentCount: number;
totalDeliveredCount: number;
totalDeliveryFailedCount: number;
items: Array<{
recipient: number; // Note: MessageBird often uses numbers here
status: string;
statusDatetime: string;
}>;
};
// Add other fields as necessary based on actual API response
}
interface MessageBirdError {
errors: Array<{
code: number;
description: string;
parameter: string | null;
}>;
}
export async function POST(req: NextRequest) {
// IMPORTANT: Add Authentication/Authorization here in production!
// Example: Check session, API key, etc.
// const session = await getServerSession(authOptions); // Example using NextAuth
// if (!session || !session.user.isAdmin) { // Check for admin role
// return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
// }
try {
const body = await req.json();
const message = body.message;
if (!message || typeof message !== 'string' || message.trim().length === 0) {
return NextResponse.json({ message: 'Message content is required' }, { status: 400 });
}
if (!process.env.MESSAGEBIRD_ORIGINATOR) {
console.error("MESSAGEBIRD_ORIGINATOR is not set.");
return NextResponse.json({ message: 'Server configuration error: Originator not set' }, { status: 500 });
}
// Fetch subscribed users
const subscribers = await prisma.subscriber.findMany({
where: { subscribed: true },
select: { phoneNumber: true }, // Only fetch the phone number
});
if (subscribers.length === 0) {
console.log("Send API: No active subscribers found.");
return NextResponse.json({ message: 'No active subscribers found.', sentCount: 0 }, { status: 200 });
}
// Assuming E.164 format stored in DB (e.g., "+1...").
// Verify the exact format required by messagebirdClient.messages.create. E.164 is usually preferred/accepted.
const recipientNumbers = subscribers.map(sub => sub.phoneNumber);
const batchSize = 50; // MessageBird API allows up to 50 recipients per request (verified 2024)
let totalSentCount = 0;
let successfulBatches = 0;
let failedBatches = 0;
console.log(`Send API: Attempting to send message to ${recipientNumbers.length} subscribers in batches of ${batchSize}.`);
for (let i = 0; i < recipientNumbers.length; i += batchSize) {
const batch = recipientNumbers.slice(i, i + batchSize);
try {
await new Promise<void>((resolve, reject) => {
messagebirdClient.messages.create({
originator: process.env.MESSAGEBIRD_ORIGINATOR!,
recipients: batch, // Send the batch of numbers
body: message,
}, (err: MessageBirdError | null, response: MessageBirdResponse | any) => { // Use defined types
if (err) {
console.error(`Send API: Error sending SMS batch (start index ${i}):`, JSON.stringify(err, null, 2));
// Decide if you want to stop on error or continue with other batches
return reject(err); // Reject the promise on error
}
// Attempt to get actual count from response, fallback to batch length
const sentInBatch = response?.recipients?.totalSentCount ?? batch.length;
totalSentCount += sentInBatch;
console.log(`Send API: SMS batch sent (start index ${i}), recipients: ${batch.length}, API Response ID: ${response?.id}`);
successfulBatches++;
resolve(); // Resolve the promise on success
});
});
} catch (batchError) {
console.error(`Send API: Failed to process batch starting at index ${i}:`, batchError);
failedBatches++;
// Optional: Implement retry logic here for the failed batch, or log for manual retry
}
}
console.log(`Send API: Broadcast finished. Successful batches: ${successfulBatches}, Failed batches: ${failedBatches}, Total estimated sent: ${totalSentCount}`);
if (failedBatches > 0 && successfulBatches === 0) {
// If all batches failed
return NextResponse.json({ message: 'Failed to send message to any subscribers.' }, { status: 500 });
} else if (failedBatches > 0) {
// If some batches failed
return NextResponse.json({ message: `Message sent, but ${failedBatches} batches failed. Estimated sent count: ${totalSentCount}`, sentCount: totalSentCount }, { status: 207 }); // 207 Multi-Status
} else {
// If all batches succeeded
return NextResponse.json({ message: 'Message broadcast initiated successfully.', sentCount: totalSentCount }, { status: 200 });
}
} catch (error) {
console.error('Error in /api/messagebird/send:', error);
return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
}
}Explanation:
- Authentication Placeholder: Includes a critical comment reminder to add security checks.
- Parse Request & Validate: Gets the
messageand validates it. Checks forMESSAGEBIRD_ORIGINATOR. - Fetch Subscribers: Queries the database for active subscribers, selecting only
phoneNumber. - Handle No Subscribers: Returns early if the list is empty.
- Prepare Recipients: Maps the
phoneNumberarray. Crucially, confirms the required format for the SDK (E.164 with+is assumed here, matching storage). - Batching: Sets
batchSizeto 50 (verified as the maximum supported by MessageBird SMS API as of 2024) and loops through recipients. - Send Batch: Calls
messagebirdClient.messages.createfor each batch usingnew Promisefor the callback. - Logging & Error Handling: Logs success/errors per batch. Tracks counts. Handles promise rejection.
- Response: Returns appropriate status codes (200 OK, 207 Multi-Status for partial success, 500 Internal Server Error) with informative messages and the estimated sent count.
4. Integrating with MessageBird
This section details obtaining necessary credentials from MessageBird and configuring your application and the MessageBird platform.
4.1 Get MessageBird API Key:
-
Log in to your MessageBird Dashboard.
-
Navigate to Developers > API access.
-
Use an existing live API key or click Add access key.
-
Copy the Live API key. Keep it secret!
-
Add this key to your
.envfile:dotenv# .env DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres?schema=public" MESSAGEBIRD_API_KEY=live_YOUR_API_KEY_HERE # Paste your key here MESSAGEBIRD_ORIGINATOR=
4.2 Get a Virtual Mobile Number (Originator):
You need a number for users to text and to send messages from.
-
In the MessageBird Dashboard, go to Numbers.
-
Click Buy a number.
-
Select the Country, ensure SMS capability is checked, choose a number, and complete the purchase.
-
Copy the purchased number (in E.164 format, e.g.,
+12025550183). -
Add this number to your
.envfile:dotenv# .env DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres?schema=public" MESSAGEBIRD_API_KEY=live_YOUR_API_KEY_HERE MESSAGEBIRD_ORIGINATOR=+12025550183 # Paste your purchased number here
4.3 Expose Local Development Server:
MessageBird needs a public URL to send webhooks to your local machine. Use localtunnel or ngrok.
- Install localtunnel (if not already):
npm install -g localtunnel - Run your Next.js app:
npm run dev(usually on port 3000) - Start localtunnel: In a new terminal, run:
lt --port 3000 - Copy the public URL provided (e.g.,
https://your-subdomain.loca.lt). Keep this terminal running. This URL is temporary.
4.4 Configure MessageBird Flow Builder:
Connect your number to your webhook API route.
- Go to Numbers in the MessageBird Dashboard.
- Find your number and click the Flow icon or navigate to its Flows tab.
- Click Create new flow > Create Custom Flow.
- Name the flow (e.g., ""SMS Subscription Handler"").
- Select SMS as the trigger. Click Next.
- Click the + below the SMS trigger step and choose Forward to URL.
- Set Method to POST.
- In URL, paste your
localtunnelURL + webhook path:https://your-subdomain.loca.lt/api/messagebird/webhook - Click Save.
- Click Publish changes in the top-right.
Now, SMS messages sent to your MessageBird number will trigger a POST request to your local development server's webhook endpoint.
5. Error Handling, Logging, and Retries
Robust applications need proper error handling and logging.
5.1 Error Handling Strategy:
- API Routes: Use
try...catcharound major operations. - Log Errors: Log detailed errors server-side (
console.erroror a logger). Return generic client errors. - Specific Error Codes: Use appropriate HTTP status codes (400, 401, 500, 207).
- MessageBird Errors: Log the
errobject from the SDK callback to understand API issues (e.g., auth errors, invalid numbers).
5.2 Logging:
- Development:
console.log/erroris okay. Log key events (webhook receipt, DB actions, send attempts, errors). - Production: Use structured logging (e.g.,
pino) for better analysis and integration with log management services.- Install:
npm install pino pino-pretty - Setup (
lib/logger.ts):Replacetypescript// lib/logger.ts import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined, }); export default logger;console.*calls withlogger.info(),logger.error(), etc.
- Install:
5.3 Retry Mechanisms:
- Webhook Receiving: MessageBird retries if your endpoint fails or times out. Ensure your webhook responds quickly (within a few seconds) with a 2xx code. Acknowledge receipt first, then process if necessary (though current logic is fast enough).
- SMS Sending (Broadcasts): If
messagebird.messages.createfails for a batch (e.g., temporary network issue, MessageBird 5xx error), consider:- Logging Failures: The current code logs failed batches. An admin could potentially retry manually.
- Simple Retry: Add a small delay and retry the failed batch once or twice within the
catchblock. - Exponential Backoff: Use libraries like
async-retryfor more robust retries with increasing delays. This adds complexity. - Recommendation: For this app, logging failures is often sufficient for broadcasts, allowing manual investigation. Retrying individual confirmation SMS might be more practical if needed.
6. Database Schema and Data Layer
- Schema:
prisma/schema.prisma(Section 1.5) defines theSubscribermodel. Add related models (e.g.,MessageLog) as needed. - Data Access: Use the singleton Prisma Client (
lib/prisma.ts) for type-safe DB operations (findUnique,findMany,create,update). - Migrations: Use
npx prisma migrate devlocally andnpx prisma migrate deployin CI/CD for production schema changes. - Performance: Indexes on
phoneNumber(@unique) andsubscribed(@@index) are included. Analyze query performance (EXPLAIN) for complex queries if needed. - Seeding: Use
prisma/seed.tsfor test data (npx prisma db seed).
7. Adding Security Features
- Admin Authentication: CRITICAL. Protect
/adminand/api/messagebird/send.- Options: NextAuth.js (recommended for Next.js), Clerk, Lucia Auth. Simple HTTP Basic Auth or IP whitelisting are less secure alternatives.
- Implementation: Use Next.js Middleware (
middleware.ts) to intercept requests to protected routes, verify session/token/API key, and redirect/block unauthorized access. Check for specific admin roles if applicable.
- Input Validation:
- Webhook: Sanitize
payload(usingtoUpperCase). Validateoriginatorformat if strict E.164 is required (though standardization logic helps). - Send API: Ensure
messageis a non-empty string. Consider adding length validation/feedback in the UI.
- Webhook: Sanitize
- Rate Limiting: Protect API routes from abuse.
- Options:
rate-limiter-flexible,upstash/ratelimit, Vercel Edge Middleware rate limiting. - Implementation: Apply rate limits (e.g., per IP or user ID) to
/api/messagebird/webhookand/api/messagebird/send.
- Options:
- CSRF Protection: Generally handled well by Next.js for same-origin requests. Frameworks like NextAuth.js provide robust CSRF protection.
- Environment Variables: Keep secrets (
.env) out of Git. Use platform environment variable management (Vercel, Netlify, Docker secrets).
8. Handling Special Cases
- Case Insensitivity: Handled by
.toUpperCase()forpayloadin the webhook. - Number Formatting:
- Inconsistency: MessageBird might send
originatorwithout+, while the SDK might prefer+forrecipients. - Standardization: The code now attempts to standardize storage to E.164 (with
+) by checking/adding the prefix in the webhook (Section 2.3). When sending (Section 3.2), it uses the stored format. Always verify the exact format requirements of the MessageBird SDK'smessages.createfunction.
- Inconsistency: MessageBird might send
- Duplicate Subscriptions: Prevented by
@uniqueonphoneNumber. The code handles re-subscribe/re-stop attempts gracefully. - Message Length: Standard SMS messages are limited to 160 GSM-7 characters or 70 UCS-2 characters. Longer messages are split into multiple segments. Be mindful of this for cost and user experience. The MessageBird API handles segmentation.
Frequently Asked Questions (FAQ)
How do I install the MessageBird SDK for Node.js?
Install the MessageBird SDK using npm or yarn: npm install messagebird (not @messagebird/api). The correct package name is messagebird (v4.0.1 as of 2024). Also install Prisma dependencies: npm install prisma @prisma/client and npm install --save-dev prisma.
What package do I use for MessageBird in Next.js?
Use the messagebird npm package. Import it as: import messagebird from 'messagebird';. Initialize the client with your API key: const messagebirdClient = messagebird(apiKey);. This is the official MessageBird Node.js SDK compatible with Next.js projects.
How many recipients can I send to with MessageBird API?
The MessageBird SMS API supports up to 50 recipients per request (verified 2024). For larger campaigns, implement batching by splitting recipient lists into chunks of 50 and sending multiple API requests with proper error handling and logging for each batch.
How do I handle SMS webhooks in Next.js App Router?
Create an API route handler at app/api/messagebird/webhook/route.ts that exports an async POST function. Parse the webhook payload using await req.json(), extract originator and payload fields, process subscription logic (SUBSCRIBE/STOP), and return a NextResponse.json() with 200 status to acknowledge receipt.
What is the correct import statement for MessageBird in TypeScript?
Use import messagebird from 'messagebird'; (default import). Then initialize: const messagebirdClient = messagebird(process.env.MESSAGEBIRD_API_KEY);. Do not use import MessageBird from '@messagebird/api'; — that package name is incorrect.
How do I manage SMS subscriptions with Prisma and Next.js?
Define a Subscriber model in prisma/schema.prisma with fields: phoneNumber (unique, E.164 format), subscribed (boolean), and timestamps. Use prisma.subscriber.findUnique() to check existing subscribers, create() for new subscriptions, and update() to toggle subscription status when processing SUBSCRIBE/STOP commands.
Does MessageBird SDK work with Next.js 15 App Router?
Yes. The messagebird package (v4.0.1) works with Next.js 15 App Router. Use it in API route handlers (app/api/*/route.ts) on the server side. Initialize the client in a utility file (lib/messagebird.ts) and import it into your route handlers. Do not use it in client components.
How do I send confirmation SMS with MessageBird in Next.js?
Use messagebirdClient.messages.create() with parameters: originator (your virtual number), recipients (array of phone numbers), and body (message text). Wrap the callback-based SDK in a Promise for use with async/await: await new Promise((resolve, reject) => { messagebirdClient.messages.create({...}, (err, res) => err ? reject(err) : resolve()); }).
What environment variables do I need for MessageBird integration?
Set three environment variables in .env: MESSAGEBIRD_API_KEY (your live API key from the dashboard), MESSAGEBIRD_ORIGINATOR (your purchased virtual mobile number in E.164 format like +12025550183), and DATABASE_URL (PostgreSQL connection string for Prisma).
How do I test MessageBird webhooks locally with Next.js?
Run your Next.js dev server (npm run dev), then expose it using localtunnel (lt --port 3000) or ngrok. Copy the public URL and configure it in MessageBird Flow Builder: Numbers > Flow > Create Custom Flow > SMS trigger > Forward to URL > paste https://your-url.loca.lt/api/messagebird/webhook.
Related Resources and Next Steps
Next Steps:
- Implement authentication for the admin interface using NextAuth.js or Clerk
- Add rate limiting to protect API routes from abuse
- Set up structured logging with Pino for production monitoring
- Deploy to Vercel or your preferred hosting platform
MessageBird Documentation:
Related Tutorials:
- Next.js 15 App Router Documentation
- Prisma Getting Started Guide
- SMS Marketing Best Practices and Compliance Guidelines
Security Considerations:
- Review OWASP API Security Top 10
- Implement TCPA and GDPR compliance for SMS marketing
- Use environment variable management for production deployments
Frequently Asked Questions
How to send SMS messages with Next.js?
Use the MessageBird SMS API integrated with a Next.js API route. Create a server-side function that calls the MessageBird API to send messages, triggered by a user action like submitting a form in your Next.js application. This enables dynamic SMS functionality within your app.
What is MessageBird used for in Next.js?
MessageBird provides the SMS sending and receiving capability. Its API and SDK are used to programmatically send SMS messages, receive incoming messages via webhooks, and manage subscribers, enabling the core functionality of an SMS-based application within the Next.js framework.
Why does the code use Prisma in Next.js?
Prisma acts as a type-safe ORM for database access. It simplifies interactions with databases like PostgreSQL, MySQL, or SQLite, ensuring data consistency, and facilitates database operations like creating, reading, updating, and deleting subscriber information.
When should I set up a MessageBird webhook?
Set up a MessageBird webhook after obtaining a virtual mobile number and exposing your local development server. This lets MessageBird send incoming SMS messages (like SUBSCRIBE or STOP commands) to your application for processing, enabling user interactions.
Can I use localtunnel with MessageBird webhooks?
Yes, localtunnel creates a public URL for your local development server. This allows MessageBird to deliver webhooks to your machine even during development when your app is not publicly deployed, essential for testing interactions with the MessageBird API.
How to handle SMS subscriptions with MessageBird?
Users text keywords (e.g., SUBSCRIBE/STOP) to a MessageBird virtual mobile number. Your Next.js webhook receives these messages, updates the database based on the keywords, and sends confirmation SMS using the MessageBird API.
What is the role of the MESSAGEBIRD_API_KEY?
The `MESSAGEBIRD_API_KEY` authenticates your application with the MessageBird service. Keep this key secure and store it in a `.env` file (never commit to version control) to protect your account and prevent unauthorized API usage.
Why use an App Router for Next.js with MessageBird?
The Next.js App Router provides a streamlined approach to creating API routes and server components, simplifying the handling of server-side logic like interacting with the MessageBird API and managing webhooks.
How to broadcast SMS messages to subscribers?
The admin interface allows inputting a message, which a server-side function then sends to all active subscribers via the MessageBird API, sending in batches of up to 50 recipients per API request as specified by MessageBird.
What database is used for subscriber data?
The example uses PostgreSQL, but Prisma supports other providers like MySQL and SQLite. You'll need to configure the `DATABASE_URL` in your `.env` file accordingly for Prisma to connect and manage subscriber information.
When should I add authentication to the admin panel?
Adding authentication is crucial *before* deploying to production. The admin panel and send API route are currently unsecured; implement proper authentication to prevent unauthorized access and protect against malicious use.
How to test the MessageBird integration locally?
Use a tool like `localtunnel` or `ngrok` to expose your local development server. Then, configure the MessageBird Flow Builder to send webhooks to your public `localtunnel` URL to test incoming messages and subscription logic during development.
Why handle case insensitivity for subscription keywords?
Users might send SUBSCRIBE or Stop. Converting to uppercase (`toUpperCase()`) ensures the application handles variations consistently, avoiding issues with case mismatches and providing a better user experience.
What is the maximum number of recipients per MessageBird API request?
The MessageBird API allows sending to up to 50 recipients per request. The provided code implements batching to handle larger subscriber lists and ensures compliance with MessageBird's API limitations.
How does the code handle duplicate subscriptions or opt-outs?
The `@unique` constraint on the `phoneNumber` field in the Prisma schema prevents database-level duplicates. The application logic also gracefully handles re-subscribe or re-opt-out attempts, providing clear feedback to the user.