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 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 SUBSCRIBE to your virtual mobile number (VMN) to opt in to SMS marketing messages
Subscribers receive SMS confirmation upon successful subscription
Subscribers text STOP to 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
Final 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).
Bash
npx create-next-app@latest messagebird-sms-campaigns
cd messagebird-sms-campaigns
messagebird: 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.
Bash
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:
Bash
docker run --name messagebird-db -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
Your .env file should contain:
.env
# .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
// 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).
Bash
npx prisma migrate dev --name init
This command:
Creates an SQL migration file in prisma/migrations.
Applies the migration to your database, creating the Subscriber table 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.
// app/api/messagebird/webhook/route.tsimport { NextRequest, NextResponse } from'next/server';
import prisma from'@/lib/prisma'; // Adjust path based on your structureimport messagebirdClient from'@/lib/messagebird'; // Adjust path based on your structure// Define types for MessageBird SDK responsesinterfaceMessageBirdResponse {
id: string;
// Add other relevant fields based on MessageBird's actual webhook response structure if needed
}
interfaceMessageBirdError {
errors: Array<{
code: number;
description: string;
parameter: string | null;
}>;
}
exportasyncfunctionPOST(req: NextRequest) {
try {
const body = await req.json();
const originator = body.originator; // Sender's phone numberconst payload = body.payload?.trim().toUpperCase(); // Message contentif (!originator || !payload) {
console.warn('Webhook received invalid data:', body);
returnNextResponse.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.';
} elseif (!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 subscribedconsole.log(`Subscriber already subscribed: ${originatorFormatted}`);
confirmationMessage = 'You are already subscribed.';
}
} elseif (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 outconsole.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 neededif (confirmationMessage && process.env.MESSAGEBIRD_ORIGINATOR) {
try {
awaitnewPromise<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 typesif (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 criticalreturnreject(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 issuesconsole.error('Failed to send confirmation SMS:', smsError);
}
}
// Respond to MessageBird promptly to acknowledge receiptreturnNextResponse.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 inforeturnNextResponse.json({ message: 'Internal server error' }, { status: 500 });
}
}
// Basic GET handler for testing reachability (optional)exportasyncfunctionGET() {
returnNextResponse.json({ message: 'Webhook endpoint is active. Use POST for events.' });
}
Explanation:
Import Dependencies: Import NextRequest, NextResponse, prisma, messagebird, and types.
POST Handler: Defines an async function 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 originatorFormatted exists in the Subscriber table.
Command Logic (SUBSCRIBE/STOP): Handles new subscriptions, re-subscriptions, and opt-outs by creating or updating the database record. Sets appropriate confirmationMessage text.
Send Confirmation SMS: If a message is set and MESSAGEBIRD_ORIGINATOR is configured, it uses messagebird.messages.create to send an SMS back. Note the use of new Promise to handle the SDK's callback within an async function. Uses the originaloriginator format for the recipient field when replying.
Error Handling: Includes try...catch blocks for overall processing and specifically for SMS sending. Logs errors.
Acknowledge Receipt: Returns a 200 OK JSON response to MessageBird. This is crucial for preventing retries.
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.
Use an existing live API key or click Add access key.
Copy the Live API key. Keep it secret!
Add this key to your .env file:
.env
# .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 .env file:
.env
# .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 localtunnel URL + 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...catch around major operations.
Log Errors: Log detailed errors server-side (console.error or a logger). Return generic client errors.
Specific Error Codes: Use appropriate HTTP status codes (400, 401, 500, 207).
MessageBird Errors: Log the err object from the SDK callback to understand API issues (e.g., auth errors, invalid numbers).
5.2 Logging:
Development:console.log/error is 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.
Replace console.* calls with logger.info(), logger.error(), etc.
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.create fails 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 catch block.
Exponential Backoff: Use libraries like async-retry for 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 the Subscriber model. 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 dev locally and npx prisma migrate deploy in CI/CD for production schema changes.
Performance: Indexes on phoneNumber (@unique) and subscribed (@@index) are included. Analyze query performance (EXPLAIN) for complex queries if needed.
Seeding: Use prisma/seed.ts for test data (npx prisma db seed).
7. Adding Security Features
Admin Authentication:CRITICAL. Protect /admin and /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 (using toUpperCase). Validate originator format if strict E.164 is required (though standardization logic helps).
Send API: Ensure message is a non-empty string. Consider adding length validation/feedback in the UI.
Implementation: Apply rate limits (e.g., per IP or user ID) to /api/messagebird/webhook and /api/messagebird/send.
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() for payload in the webhook.
Number Formatting:
Inconsistency: MessageBird might send originator without +, while the SDK might prefer + for recipients.
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's messages.create function.
Duplicate Subscriptions: Prevented by @unique on phoneNumber. 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
Implement TCPA and GDPR compliance for SMS marketing
Use environment variable management for production deployments
Frequently asked questions
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
SUBSCRIBEto your virtual mobile number (VMN) to opt in to SMS marketing messagesSTOPto the same VMN to opt outPrerequisites:
Before starting, ensure you have:
Target Audience: Developers familiar with JavaScript, Node.js, and Next.js basics who want to integrate SMS functionality using MessageBird.
Technologies:
System Architecture:
Final 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.
messagebird: 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 operationsNote: Next.js automatically loads variables from
.envfiles – you don't need thedotenvpackage.1.3 Set Up Prisma
Initialize Prisma in your project. This creates a
prismadirectory with aschema.prismafile and updates your.gitignore.(If you prefer SQLite or MySQL, replace
postgresqlaccordingly.)1.4 Configure Database Connection
Open the
.envfile (create it if it doesn't exist). Add yourDATABASE_URLvariable.Using Docker for Local PostgreSQL:
Spin up a PostgreSQL container:
Your
.envfile should contain: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.prismaand define the model for storing subscriber information.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
Subscribertable in your database based on the schema. Prisma will prompt you to create a name for the migration (e.g.,init).This command:
prisma/migrations.Subscribertable and indexes.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_KEYto your.envfile 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:
NextRequest,NextResponse,prisma,messagebird, and types.asyncfunction to handle POST requests.originator,payload).originatorFormatted) is consistently formatted (e.g., E.164+1xxxxxxxxxx) for database storage. Adjust based on observed MessageBird format if needed.originatorFormattedexists in theSubscribertable.SUBSCRIBE/STOP): Handles new subscriptions, re-subscriptions, and opt-outs by creating or updating the database record. Sets appropriateconfirmationMessagetext.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.try...catchblocks for overall processing and specifically for SMS sending. Logs errors.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:
useState) for message input, loading state, and status feedback.handleSubmitsends a POST request to/api/messagebird/send.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:
messageand validates it. Checks forMESSAGEBIRD_ORIGINATOR.phoneNumber.phoneNumberarray. Crucially, confirms the required format for the SDK (E.164 with+is assumed here, matching storage).batchSizeto 50 (verified as the maximum supported by MessageBird SMS API as of 2024) and loops through recipients.messagebirdClient.messages.createfor each batch usingnew Promisefor the callback.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: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:4.3 Expose Local Development Server:
MessageBird needs a public URL to send webhooks to your local machine. Use
localtunnelorngrok.npm install -g localtunnelnpm run dev(usually on port 3000)lt --port 3000https://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.
localtunnelURL + webhook path:https://your-subdomain.loca.lt/api/messagebird/webhookNow, 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:
try...catcharound major operations.console.erroror a logger). Return generic client errors.errobject from the SDK callback to understand API issues (e.g., auth errors, invalid numbers).5.2 Logging:
console.log/erroris okay. Log key events (webhook receipt, DB actions, send attempts, errors).pino) for better analysis and integration with log management services.npm install pino pino-prettylib/logger.ts):// 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.5.3 Retry Mechanisms:
messagebird.messages.createfails for a batch (e.g., temporary network issue, MessageBird 5xx error), consider:catchblock.async-retryfor more robust retries with increasing delays. This adds complexity.6. Database Schema and Data Layer
prisma/schema.prisma(Section 1.5) defines theSubscribermodel. Add related models (e.g.,MessageLog) as needed.lib/prisma.ts) for type-safe DB operations (findUnique,findMany,create,update).npx prisma migrate devlocally andnpx prisma migrate deployin CI/CD for production schema changes.phoneNumber(@unique) andsubscribed(@@index) are included. Analyze query performance (EXPLAIN) for complex queries if needed.prisma/seed.tsfor test data (npx prisma db seed).7. Adding Security Features
/adminand/api/messagebird/send.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.payload(usingtoUpperCase). Validateoriginatorformat if strict E.164 is required (though standardization logic helps).messageis a non-empty string. Consider adding length validation/feedback in the UI.rate-limiter-flexible,upstash/ratelimit, Vercel Edge Middleware rate limiting./api/messagebird/webhookand/api/messagebird/send..env) out of Git. Use platform environment variable management (Vercel, Netlify, Docker secrets).8. Handling Special Cases
.toUpperCase()forpayloadin the webhook.originatorwithout+, while the SDK might prefer+forrecipients.+) 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.@uniqueonphoneNumber. The code handles re-subscribe/re-stop attempts gracefully.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 ismessagebird(v4.0.1 as of 2024). Also install Prisma dependencies:npm install prisma @prisma/clientandnpm install --save-dev prisma.What package do I use for MessageBird in Next.js?
Use the
messagebirdnpm 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.tsthat exports an asyncPOSTfunction. Parse the webhook payload usingawait req.json(), extractoriginatorandpayloadfields, process subscription logic (SUBSCRIBE/STOP), and return aNextResponse.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 useimport MessageBird from '@messagebird/api';— that package name is incorrect.How do I manage SMS subscriptions with Prisma and Next.js?
Define a
Subscribermodel inprisma/schema.prismawith fields:phoneNumber(unique, E.164 format),subscribed(boolean), and timestamps. Useprisma.subscriber.findUnique()to check existing subscribers,create()for new subscriptions, andupdate()to toggle subscription status when processing SUBSCRIBE/STOP commands.Does MessageBird SDK work with Next.js 15 App Router?
Yes. The
messagebirdpackage (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), andbody(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), andDATABASE_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 > pastehttps://your-url.loca.lt/api/messagebird/webhook.Related Resources and Next Steps
Next Steps:
MessageBird Documentation:
Related Tutorials:
Security Considerations: